⬆️ ⬇️

Create a REST service on Rust. Part 5: Handlers, Refactoring, and Macros

Hello!



We continue to write a web service on Rust. Table of contents:



Part 1: prototype

Part 2: we read INI; multirust

Part 3: update the database from the console

Part 4: go to the REST API

Part 5 (this): handlers, refactoring, and macros

')

Now we will look at the actual API request handlers and rewrite the previous, terrible code. And in general, this is the last article in the series, so there will be refactoring, style, macros and all. This is the longest part.



Why we cloned Arc twice



Here's what the code that sets API paths now looks like:



let sdb = Arc::new(Mutex::new(db)); let mut router = router::Router::new(); { let sdb_ = sdb.clone(); router.get("/api/v1/records", move |req: &mut Request| handlers::get_records(sdb_.clone(), req)); } { let sdb_ = sdb.clone(); router.get("/api/v1/records/:id", move |req: &mut Request| handlers::get_record(sdb_.clone(), req)); } … 


For starters, the handlers themselves. Here, for example, handlers :: get_records ():



handlers :: get_records
 pub fn get_records(sdb: Arc<Mutex<Connection>>, req: &mut Request) -> IronResult<Response> { let url = req.url.clone().into_generic_url(); let mut name: Option<String> = None; if let Some(qp) = url.query_pairs() { for (key, value) in qp { match (&key[..], value) { ("name", n) => { if let None = name { name = Some(n); } else { return Ok(Response::with((status::BadRequest, "passed name in query more than once"))); } } _ => return Ok(Response::with((status::BadRequest, "unexpected query parameters"))), } } } else { return Ok(Response::with((status::BadRequest, "passed names don't parse as application/x-www-form-urlencoded or there are no parameters"))); } let json_records; if let Ok(recs) = ::db::read(sdb, name.as_ref().map(|s| &s[..])) { use rustc_serialize::json; if let Ok(json) = json::encode(&recs) { json_records = Some(json); } else { return Ok(Response::with((status::InternalServerError, "couldn't convert records to JSON"))); } } else { return Ok(Response::with((status::InternalServerError, "couldn't read records from database"))); } let content_type = Mime( TopLevel::Application, SubLevel::Json, Vec::new()); Ok(Response::with( (content_type, status::Ok, json_records.unwrap()))) } 




Its signature is the reason that we had to clone Arc with the database inside the closure:



 pub fn get_records(sdb: Arc<Mutex<Connection>>, req: &mut Request) -> IronResult<Response> { 


As you can see, Arc is passed here by value (i.e., with possession), and it is not a type that is trivially copied. For this reason, we cloned Arc for transmission to the handler.



What happens in the handlers



In general, the handlers are of the same type, so I will only look at get_records in detail - it is the most complex. I want to note that in the handlers, pattern matching is actively used to determine erroneous situations.



First we get a rust-url url from iron url.



  let url = req.url.clone().into_generic_url(); 


We do this to then use the query_pairs method, which parses the URL as application / x-www-form-urlencoded data and (possibly) returns an iterator over the key-value pairs.



if let



Now I will show the new syntax “if let”, and then I will tell you what its essence is.



  if let Some(qp) = url.query_pairs() { for (key, value) in qp { 


You may have already guessed what this entry means. The if let statement attempts to match the pattern, and if it is successful, passes the execution to the block for if let. In this block, the name that we just associated the value with will be available - in this case, qp. If it was not possible to compare the value with the template (query_pairs () returned None), then the else branch is executed - it looks like a normal if.



Return erroneous HTTP statuses



Accordingly, if the iterator is not returned to us, this is an error:



  } else { return Ok(Response::with((status::BadRequest, “passed names don't parse as application/x-www-form-urlencoded or there are no parameters”))); } 


Here we have a tuple in parentheses describing the server's response: HTTP status and message.



Get the request parameters



If we are returned an iterator, we go around it to get the name parameter and save it to the name variable:



  let mut name: Option<String> = None; if let Some(qp) = url.query_pairs() { for (key, value) in qp { match (&key[..], value) { ("name", n) => { if let None = name { name = Some(n); } else { return Ok(Response::with((status::BadRequest, "passed name in query more than once"))); } } _ => return Ok(Response::with((status::BadRequest, "unexpected query parameters"))), } } } 


The cycle is needed here to bypass the iterator, pull the desired element from the pair vector, and not have problems with ownership. But in fact, any situation where exactly one request parameter, which is called name, was not passed to us, is erroneous. Let's try to remove the loop.



We remove the cycle parameters



.query_pairs () actually returns Option <Vec <(String, String) >>. Therefore, we can simply check the length of the vector and the name of a single parameter:



  let mut name: Option<String> = None; if let Some(mut qp) = url.query_pairs() { if qp.len() != 1 { return Ok(Response::with((status::BadRequest, "passed more than one parameter or no parameters at all"))); } let (key, value) = qp.pop().unwrap(); if key == "name" { name = Some(value); } } else { 


Now we do not go around the vector, but check its length and immediately refer to the parameter that interests us.



Here is an important point:



  let (key, value) = qp.pop().unwrap(); 


It’s important to use pop () - it sends us a vector element with a hold. A normal reference by index (qp [0]) would give a reference, and we would not be able to move the value from the pair to Some (value) to put all this in the name.



Why does string comparison with & str work?



It is also worth noting that pairs are stored in our vector (String, String). But then we directly compare key with “name” - a string literal:



  if key == "name" { name = Some(value); } 


It, as you remember, has type & 'static str. This works because the String implements the PartialEq type for comparison with & 'a str:



 impl<'a> PartialEq<&'a str> for String 


Therefore, no type conversion occurs here.



If there was no such type, we could convert the String to & str using the slice syntax: & key [..] will return the slice over the entire string, i.e. link- & str with the same content.



Next, we make the actual access to the database.



Uninitialized variables - is it dangerous?



First, we declare a name for the JSON records that our REST access point should return:



  let json_records; 


Hmm, do we initialize it with any value? Want to shoot yourself in the leg?



No, Rust will not let us use the declared name until it is initialized. For example, in this code



 fn main() { let a; if true { a = 5; } else { println!("{}", a); } } 


an error will occur:



 test.rs:6:24: 6:25 error: use of possibly uninitialized variable: `a` [E0381] test.rs:6 println!("{}", a); ^ 




We read records from a DB. We use Option :: map



Next we read the records from the database:



  if let Ok(recs) = ::db::read(sdb, name.as_ref().map(|s| &s[..])) { 


Why is there something strange going on in the arguments?



  name.as_ref().map(|s| &s[..]) 


Now I will explain. First, we look at our signature :: db :: read ():



 pub fn read(sdb: Arc<Mutex<Connection>>, name: Option<&str>) -> Result<Vec<Record>, ()> { 


As you can see, it takes a name in the form of Option<&str> . Our name is of type Option<String> . But it doesn't matter: the .as_ref() method turns Option<T> into Option<&T> - so we get Option<&String> .



Unfortunately, because &String wrapped in Option, it is not converted to &str automatically. Therefore, we use the aforementioned slice syntax in an anonymous function:



  .map(|s| &s[..]) 


.map applies the function to the contents of the Option and converts T from Option<T> to some other type. In this case, we will convert &String to &str . This is similar to haskel fmap :: Functor f => (a -> b) -> fa -> fb .



There is a subtlety: we could not call .map immediately on name: Option<T> , since then the link will be valid only in the scope of the function parameters in the call. In this case, we will get a link inside the closure, and it will live only as long as the closure lives. But it is not saved anywhere and will be destroyed after the parameter is passed to the function. Such a link will be a temporary object:



 handlers.rs:25:53: 25:54 error: `s` does not live long enough
 handlers.rs:25 if let Ok (recs) = :: db :: read (sdb, name.map (| s | & s [..])) {
                                                                    ^
 handlers.rs:25:23: 25:60 note: reference must be valid for the call at 25:22 ...
 handlers.rs:25 if let Ok (recs) = :: db :: read (sdb, name.map (| s | & s [..])) {
                                      ^ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 handlers.rs: 25: 47: 25:58 note: ...
 handlers.rs:25 if let Ok (recs) = :: db :: read (sdb, name.map (| s | & s [..])) {
                                                                   ^ ~~~~~ 


In the case of .as_ref (), the link lives while Option itself lives, so everything works.



And what about multithreading?



Let's take a look at :: db :: read and see how the vaunted protection against data races works.



  if let Ok(rs) = show(&*sdb.lock().unwrap(), name) { Ok(rs) } else { Err(()) } 


We want to call show:



 pub fn show(db: &Connection, arg: Option<&str>) -> ::postgres::Result<Vec<Record>> { 


This function accepts a reference to Connection, and we have Arc<Mutex<Connection>> . We will not be able to get to the database connection of interest to us, except by expanding the reference counter and taking control of the mutex. The type system makes invalid states unrepresentable.



Almost magic



So we want to take over the mutex. It is nested in the reference counter. Here two things come in: deconversion conversion and auto dereference when calling methods.



For now, ignore the strange & * and look at sdb.lock () itself. sdb is Arc, but Arc<T> implements Deref<T> .



 impl<T> Deref for Arc<T> where T: ?Sized type Target = T fn deref(&self) -> &T 


Thus, Arc<T> will be automatically converted to & T if necessary. This will give us a &Mutex<Connection> .



Next comes the auto-dereference when calling methods. In short, the compiler will insert as many dereferences into the method call as necessary.



Here is a simple example:



 struct Foo; impl Foo { fn foo(&self) { println!("Foo"); } } let f = Foo; f.foo(); (&f).foo(); (&&f).foo(); (&&&&&&&&f).foo(); 


All four last lines do the same thing.



Safely releasing a mutex using RAII



Mutex :: lock will give us LockResult<MutexGuard<T>> . Result allows us to handle the error, and MutexGuard<T> is a RAII value that will automatically open the mutex as soon as we stop working with it.



That &* translates MutexGuard<T> into & T — first we dereference it and get T, and then we take the address to get the usual link, & T.



Why can lock () work directly with Arc<Mutex<Connection>> , but does MutexGuard need to be converted manually? Because lock is a method, and a call to methods will actually not only dereference references, but also convert some references into others (that is, make an analog of &* ). When passing arguments to a function, this must be done manually.



Serialization



After receiving our records, we want to serialize them into JSON. To do this, use rustc_serialize :



  use rustc_serialize::json; 


As you can see, we can import modules not only globally, but also in the scope of a single function or block. This helps not to clutter up the global namespace.



The serialization itself is done with this code:



  if let Ok(json) = json::encode(&recs) { json_records = Some(json); } ... 


In this case, the serializer code is generated automatically! We only need to declare the type of our records as serializable (and at the same time, deserializable):



 #[derive(RustcEncodable, RustcDecodable)] pub struct Record { id: Option<i32>, pub name: String, pub phone: String, } 




Send all back



Finally, we wrap our JSON in the correct HTTP with the appropriate headers and return it:



  let content_type = Mime( TopLevel::Application, SubLevel::Json, Vec::new()); Ok(Response::with( (content_type, status::Ok, json_records.unwrap()))) 


The remaining handlers work in a similar way, so instead of repeating, we will refactor our code.



In general, our program is over! Now our wonderful phone book can be updated not only from the command line, but also through a modern web API. If you want to see how everything works, get a version of the code for the feature-complete tag from GitHub .



Refactoring is not so complicated, and I show this process just to convince you that the code on Rust can also be beautiful. That unreadable mess that we had in the process of implementing the functional is simply the result of haste. Rust is not to blame - you can write elegant code on it.



Do not clone clones



First of all, let's deal with the dual cloning of Arc, which I mentioned in the previous section:



  { let sdb_ = sdb.clone(); router.get("/api/v1/records", move |req: &mut Request| handlers::get_records(sdb_.clone(), req)); } 


It is very easy to win. Change handlers :: get_records signature with



 pub fn get_records(sdb: Arc<Mutex<Connection>>, req: &mut Request) -> IronResult<Response> { 


on



 pub fn get_records(sdb: &Mutex<Connection>, req: &mut Request) -> IronResult<Response> { 


And in general, we use &Mutex<Connection> everywhere - in handlers and in database functions. Everything, double cloning is no longer necessary:



  { let sdb = sdb.clone(); router.get("/api/v1/records", move |req: &mut Request| handlers::get_records(&*sdb, req)); } 


With a huge main, too, should understand. Just take all the actions into its functions and get a cool compact main:



 fn main() { let (params, sslmode) = params(); let db = Connection::connect(params, &sslmode).unwrap(); init_db(&db); let args: Vec<String> = std::env::args().collect(); match args.get(1) { Some(text) => { match text.as_ref() { "add" => add(&db, &args), "del" => del(&db, &args), "edit" => edit(&db, &args), "show" => show(&db, &args), "help" => println!("{}", HELP), "serve" => serve(db), command => panic!( format!("Invalid command: {}", command)) } } None => panic!("No command supplied"), } } 




rustfmt!



Finally, sweet: rustfmt ! The source code formatting utility on Rust is not finished yet, but it is already suitable for decorating the code of our small project.



Cloning the repository, do the cargo build --release, and then copy the resulting executable file somewhere in $ PATH. Then, in the root of our project we will do



 $ rustfmt src / main.rs 


And everything, the code of the whole project is instantly formatted! rustfmt follows links to other modules and formats them too.



Unlike gofmt, rustfmt allows you to quite a bit to customize the style in which the source will be rewritten.



The current default style is something like the one in which the compiler itself is written. However, as the official style guide is finalized, rustfmt will also be finished.



At this "reasonable" refactoring ends, and begins ... something controversial, but definitely fun: let's remove the remaining repetition of similar code using macros.



Macros



What kind of repetition am I talking about? About it:



  { let sdb = sdb.clone(); router.get("/api/v1/records", move |req: &mut Request| handlers::get_records(&*sdb, req)); } { let sdb = sdb.clone(); router.get("/api/v1/records/:id", move |req: &mut Request| handlers::get_record(&*sdb, req)); } { let sdb = sdb.clone(); router.post("/api/v1/records", move |req: &mut Request| handlers::add_record(&*sdb, req)); } { let sdb = sdb.clone(); router.put("/api/v1/records/:id", move |req: &mut Request| handlers::update_record(&*sdb, req)); } { let sdb = sdb.clone(); router.delete("/api/v1/records/:id", move |req: &mut Request| handlers::delete_record(&*sdb, req)); } 


Obviously, there is some high-level structure that we could not reflect in the code. Since these blocks differ in the method that the router needs to call, in order to process all these options in a normal function, it would be necessary to match some enumeration, which would call the appropriate method depending on the argument.



This is, generally speaking, an option, and most likely I would try to do it if I wrote this code at work, but here we have fun, and I have long wanted to try macros in Rust. So let's get started.



To begin with, the duplicate structure here is a block that clones Arc and then executes an operator. Let's try to write the corresponding macro:



 macro_rules! clone_pass_bound { ($arc:ident, $stmt:stmt) => { { let $arc = $arc.clone(); $stmt; } } } 


The first line says that we started defining a macro called clone_pass_bound. Stupid name, but better come up with failed. This is in itself a symptom of the fact that you probably should not do this in your working code. But oh well - this is not our case now.



The macros in Rust are typed, and ours takes two arguments - $ arc of type “identifier” (ident) and $ stmt of type “operator” (statement, stmt). If you take a closer look, you can notice the similarity of the definition of a macro to match - here a certain combination of arguments is compared to a specific body. Macro branches can have many branches, like match - and this is useful in the case of recursion.



After the arrow are two pairs of curly braces. Some are required according to the syntax of the macro description - in general, as in the usual match.



With the help of the second pair, we say that our macro expands into a block. Inside the block, we write almost the usual code, replacing sdb with $ arc. This is a trivial generalization. Cloning follows our operator.



Here is how this macro is called:



  clone_pass_bound!( sdb, router.get("/api/v1/records", move |req: &mut Request| handlers::get_records(&*sdb, req))); 


So far we have not saved anything in terms of volume, just received an incomprehensible challenge. But do not despair - we just started!



Macro on macro



Now it becomes clear that one handler can be described using four parameters: a connection to the database, a router, which method to add to it (get, post, etc.), and how the handler defined by us is called. Let's write a macro for this:



 macro_rules! define_handler { ($connection:ident, $router: ident.$method:ident, $route:expr, $handler:path) => { clone_pass_bound!( $connection, $router.$method( $route, move |req: &mut Request| $handler(&*$connection, req))); } } 


Here, first of all, it is worth emphasizing again the similarity of calling a macro with the usual pattern matching. As you can see, the macro argument argument separator does not have to be a comma - the router and we divided its method with a dot, for greater similarity with the usual code.



Then stupidly replacing all specific names with meta variables of the macro and calling our previous macro is not that scary and difficult. I wrote both of these macros on the first try.



Now we have written two dozen lines of insane macros, and the code we wanted to cut finally began to decrease:



  define_handler!(sdb, router.get, "/api/v1/records", handlers::get_records); define_handler!(sdb, router.get, "/api/v1/records/:id", handlers::get_record); define_handler!(sdb, router.post, "/api/v1/records", handlers::add_record); define_handler!(sdb, router.put, "/api/v1/records/:id", handlers::update_record); define_handler!(sdb, router.delete, "/api/v1/records/:id", handlers::delete_record); 


This is not the limit - now we will define the last macro, which will make our definition very compact and quite understandable. Now the changing parts of the code are quite obvious, and nothing can hurt to make the code completely DRY.



In our last macro there will be exactly 1 (one) non-trivial moment.



Macro chases



This is what the last macro looks like:



 macro_rules! define_handlers_family { ($connection:ident, $router:ident, $( [$method:ident, $route:expr, $handler:path]),+ ) => { $( define_handler!($connection, $router.$method, $route, $handler); )+ } } 


It is rather small. A nontrivial moment is that we introduced repeatability in the arguments:



  ($connection:ident, $router:ident, $( [$method:ident, $route:expr, $handler:path]),+ ) => { 


$( … ),+ means that the enclosed group in brackets should be repeated one or more times when calling this macro. It looks like regular expressions.



Next is the body of our macro monster. At first I wrote this:



  define_handler!($connection, $router.$method, $route, $handler); 


To which the compiler countered:



 main.rs:134:46: 134:53 error: variable 'method' is still repeating at this depth main.rs:134 define_handler!($connection, $router.$method, $route, $handler); ^~~~~~~ 


As I said, the call part that defines $ method, $ route, and $ handler can be repeated. In Rust macros, the rule is that a meta-variable that is at a certain “level” of repetitions in a call must be at the same level of repetition when used.



This can be thought of as follows: the tuples of parameters of macro calls are being moved simultaneously with the corresponding bodies. Those. one set of parameters must correspond to one body. Thus it becomes easier to understand the structure of the macro - the body becomes like a challenge.



And now we have the macro recorded as if it has only one body - it turns out that the call parameters are repeated, and the bodies cannot be repeated. Then what kind of $ method should be in the body? Unclear. In order to avoid such situations, a rule has been devised for sorting parameters “in step” with the bodies.



For us, this all means that you need to wrap the body in the same repeatability modifier as the parameters:



  $( define_handler!($connection, $router.$method, $route, $handler); )+ 


Now $ method, $ route and $ handler match duplicate parameters. And $ connection and $ router are “global” - they are not under one repeatability modifier, so they will be duplicated in each body.



As a reward for this brainwave, we get a beautiful definition of all the paths in our API:



  define_handlers_family!( sdb, router, [get, "/api/v1/records", handlers::get_records], [get, "/api/v1/records/:id", handlers::get_record], [post, "/api/v1/records", handlers::add_record], [put, "/api/v1/records/:id", handlers::update_record], [delete, "/api/v1/records/:id", handlers::delete_record]); 


No unnecessary duplication, and in the final version looks even relatively understandable to the uninitiated.



I want to note that macros in Rust are hygienic - collisions of names inside a macro with names outside are excluded.



Oh yeah, I almost forgot - the compiler option is very helpful in debugging macros - pretty-print = expand. So it will print the code after macros are expanded into the standard output stream. It looks like the -E option on C and C ++ compilers.



See you again!



That's all. Now everything is completely - I think this series of articles has told you enough so that you yourself can start building your code on Rust, including for the web.



If you have already started doing something on Rust - write about it in the comments. And also come to our chat with questions that arise along the way - they are happy to help you there.

Source: https://habr.com/ru/post/269903/



All Articles