This post shows a simple implementation of the Chain of Responsibility pattern in Rust. For example, we have a set of struct instances that check for a file, read it, and finally, display it. We could imagine these instances as machines, e.g., ReadJsonMachine.
Three Handlers Each With Unique Responsibility
For this post, we use three struct instances to represent a set of handlers for our Chain of Responsibility pattern example in Rust. Consider the following diagram. The three handlers are CheckFileTypeMachine, ReadJSONMachine, and DisplayJSONMachine.
Meanwhile, the ResponseToRequestConverter converts an output from one handler into an input for the next handler. This item allows for loosely coupling our three handlers.
Chain of Responsibility in Rust Codes – Groundwork
Before we get to the handler codes, let us define strut and traits that will underpin our actual handler Rust codes. First, we two traits that represent the incoming and outcoming data to and from a handler. We can think of them as request and response, respectively.
1 2 3 4 5 6 7 8 9 | // Incoming data to a handler (request data) trait MachineRequest { fn get_parameters(&self) -> HashMap<String, String>; } // Incoming data from a handler (response data) trait MachineResponse { fn get_output(&self) -> HashMap<String, String>; } |
Along with these traits come two implementing structs that use HashMap. Therefore, we need to import the appropriate collection type, e.g., use std::collections::HashMap;.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | // Request implementation #[derive(Clone)] struct MachineRequestImpl { parameters: HashMap<String, String> } impl MachineRequest for MachineRequestImpl { fn get_parameters(&self) -> HashMap<String, String> { self.parameters.clone() } } // Response implementation #[derive(Clone)] struct MachineResponseImpl { output: HashMap<String, String> } impl MachineResponse for MachineResponseImpl { fn get_output(&self) -> HashMap<String, String> { self.output.clone() } } |
Then, we define a trait that represents a handler.
1 2 3 4 5 | // Trait representing a handler trait Machine<T:MachineRequest, U: MachineResponse> { fn process(&self, machine_request: T) -> U; } |
Chain of Responsibility Handlers
Next, we define three structs that implement that Machine trait. First, we have a handler that checks for a file type. Suppose the file name does not end with .json, the Rust codes panic and return an error. Otherwise, it creates a response with the file type and path to the JSON file.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | struct CheckFileTypeMachine {} impl Machine<MachineRequestImpl, MachineResponseImpl> for CheckFileTypeMachine { fn process(&self, machine_request: MachineRequestImpl) -> MachineResponseImpl { let mut response = MachineResponseImpl{ output: Default::default() }; let file_path = machine_request.parameters.get("filePath"); if None == file_path { panic!("Parameter 'filePath' is missing"); } let option = file_path.unwrap().find(".json"); if None == option { panic!("Expecting json file"); } response.output.insert(String::from("fileType"), String::from("json")); response.output.insert(String::from("filePath"), file_path.unwrap().to_owned()); return response.clone(); } } |
Second, we have another handler that reads the content of a JSON file. When it reads the JSON file successfully, it creates a response that contains the JSON data.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | struct ReadJsonMachine { } impl Machine<MachineRequestImpl, MachineResponseImpl> for ReadJsonMachine { fn process(&self, machine_request: MachineRequestImpl) -> MachineResponseImpl { let mut response = MachineResponseImpl{ output: Default::default() }; let file_path = machine_request.parameters.get("filePath"); if None == file_path { panic!("Parameter 'filePath' is missing"); } let contents = fs::read_to_string(file_path.unwrap().to_owned()) .expect("Something went wrong reading the file"); response.output.insert(String::from("fileContent"), contents); return response.clone(); } } |
Note that we need to import the fs module, e.g., use std::fs; to read a JSON file.
Third, we have the last handler that prints out the JSON data.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | struct DisplayJsonMachine { } impl Machine<MachineRequestImpl, MachineResponseImpl> for DisplayJsonMachine { fn process(&self, machine_request: MachineRequestImpl) -> MachineResponseImpl { let response = MachineResponseImpl{ output: Default::default() }; let file_content = machine_request.parameters.get("fileContent"); if None == file_content { panic!("Parameter 'fileContent' is missing"); } println!("{}", file_content.unwrap()); return response.clone(); } } |
Now, how do we chain the handlers?
Loose Coupling in Chain of Responsibility Design Pattern
For this post, we couple the handlers by using a loop and a response-to-request converter. First, we define the direction in which the data (request) flows using a loop. Second, the response-to-request converter allows us to transform a response from one handler into a request format for the next handler. As a result, the handlers do not know anything about the others in the chain.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | fn main() { let mut machines:Vec<Box<dyn Machine<MachineRequestImpl, MachineResponseImpl>>> = Vec::new(); let machine1 = CheckFileTypeMachine{}; let machine2 = ReadJsonMachine{}; let machine3 = DisplayJsonMachine{}; let mut request: MachineRequestImpl = MachineRequestImpl { parameters: Default::default() }; // Configure the handlers in such a way that CheckFileTypeMachine is the first handler, // ReadJsonMachine is the second, and soon. machines.push(Box::new(machine1)); machines.push(Box::new(machine2)); machines.push(Box::new(machine3)); // Provide the initial data to the first handler request.parameters.insert(String::from("filePath"), String::from("C:\\Users\\karldev\\Desktop\\myjson.json")); for machine in machines { let response = machine.process(request.clone()); request = MachineRequestImpl{ parameters: Default::default() }; // Transform a response to a request for the next handler request.parameters = response.output; } } |
When we run all the codes, we get the following output.
1 2 3 | { "name":"karl" } |
We tested the codes using Rust 1.59.00.
Other Design Patterns
For other design patterns in Rust, please read the post Design Patterns Are The Ultimate Toolkit For Programmers.