Create a REST service in Rust. Part 3: updating the database from the console

  • Tutorial
In the previous part, we parsed the database configuration file in order to read connection parameters from it.

Now let's directly implement the database update operations: creating, updating, deleting our records and the corresponding command line interface.

To get started, let's look at the arguments of the program. Its interface will look like this:

const HELP: &'static str = "Usage: phonebook COMMAND [ARG]...
Commands:
    add NAME PHONE - create new record;
    del ID1 ID2... - delete record;
    edit ID        - edit record;
    show           - display all records;
    show STRING    - display records which contain a given substring in the name;
    help           - display this help.";

There are already a couple of interesting points. const declares a constant, and such that it simply is built in a place of use. Thus, it does not have its own address in memory - it looks like #define in C. The type of constant must always be specified - and in this case it may look a little frightening. & 'static str? What is it?

If my memory serves me, we have not yet seen the explicitly specified lifetimes. So, this is a link, & str, and it can be written differently as & 'foo str. Usually we do not have to explicitly indicate the lifetime, because the compiler can output it itself - i.e. 'foo just drops.

I also note that 'foo could be' bar or whatever else - it's just the name of a variable. In our case, you might think like this: the HELP: & str link has a lifetime called 'foo,

Now about 'static. This is the lifetime equal to the lifetime of the program. Our line is directly embedded in the image of the program, and it does not require any initialization or explicit destruction. Therefore, it is always available while the program is running. Read more about 'static here .

Thus, we declared a string constant, which is always available.

And here is the code for parsing the arguments - as always, first in its entirety. Then we will consider it in more detail.

Command line parsing code
    let args: Vec = std::env::args().collect();
    match args.get(1) {
        Some(text) => {
            match text.as_ref() {
                "add" => {
                    if args.len() != 4 {
                        panic!("Usage: phonebook add NAME PHONE");
                    }
                    let r = db::insert(db, &args[2], &args[3])
                        .unwrap();
                    println!("{} rows affected", r);
                        },
                "del" => {
                    if args.len() < 3 {
                        panic!("Usage: phonebook del ID...");
                    }
                    let ids: Vec = args[2..].iter()
                        .map(|s| s.parse().unwrap())
                        .collect();
                    db::remove(db, &ids)
                        .unwrap();
                },
                "edit" => {
                    if args.len() != 5 {
                        panic!("Usage: phonebook edit ID NAME PHONE");
                    }
                    let id = args[2].parse().unwrap();
                    db::update(db, id, &args[3], &args[4])
                        .unwrap();
                },
                "show" => {
                    if args.len() > 3 {
                        panic!("Usage: phonebook show [SUBSTRING]");
                    }
                    let s;
                    if args.len() == 3 {
                            s = args.get(2);
                    } else {
                        s = None;
                    }
                    let r = db::show(db, s.as_ref().map(|s| &s[..])).unwrap();
                    db::format(&r);
                },
                "help" => {
                    println!("{}", HELP);
                },
                command @ _  => panic!(
                    format!("Invalid command: {}", command))
            }
        }
        None => panic!("No command supplied"),
    }


Let's look at the first line:

    let args: Vec<_> = std::env::args().collect();

std :: env :: args () just returns an iterator over command line arguments. Why is it an iterator and not some static array? Because we may not need all the arguments, but potentially there may be many. Therefore, an iterator is used - it is "lazy." This is in the spirit of Rust - you do not pay for what you do not need.

So, here we knowingly have few arguments and it will be easier for us to still have a normal vector from which the arguments can be taken from the indices. We do .collect () to go around all the elements and assemble them into a specific collection.

Which collection? There is a subtle point. In fact, .collect () calls the from_iter () methodthe collection into which the elements are placed. It turns out that we need to know its type. That is why we cannot omit the args type and write this:

    let args = std::env::args().collect();

Here is what the compiler will say:

main.rs:61:9: 61:13 error: unable to infer enough type information about `_`; type annotations or generic parameter binding required [E0282]
main.rs:61     let args = std::env::args().collect();
                   ^~~~
main.rs:61:9: 61:13 help: run `rustc --explain E0282` to see a detailed explanation

However, note that type inference does its job: it is enough for us to specify Vec <_> as the type: what type lies in the vector, the compiler already knows. It is only necessary to clarify which collection we want.

Well, why all these difficulties? Then, that we can, for example, collect the arguments into a linked list (or some other collection) if we want:

    let args: std::collections::LinkedList<_> = std::env::args().collect();

List of collections that implement from_iter, there on the page documentation facial features .

Next we see:

    match args.get(1) {

.get () returns Ok (element) if the vector element exists, and None otherwise. We use this to detect a situation where the user did not specify a command:

        }
        None => panic!("No command supplied"),
    }

If the command does not match any of the predefined ones, we display an error:

                command @ _  => panic!(
                    format!("Invalid command: {}", command))

We want to get into this branch for any value of text - therefore, _, "any value" is used as the value of this branch. However, we want to output this very incorrect command, so we associate the match expression with the name command using the command @ _ construct. See here and here for more on this syntax .

Further analysis looks like this:

        Some(text) => {
            match text.as_ref() {
                "add" => {
                    // handle add
                },

If we have a team, we will fall into the Some (text) branch. Next, we use match again to match the team name - as you can see, match is pretty universal.

Commands are pretty much the same, so let's look at the most interesting: delete. It accepts a list of record identifiers to be deleted.

                "del" => {
                    if args.len() < 3 {
                        panic!("Usage: phonebook del ID...");
                    }
                    let ids: Vec = args[2..].iter()
                        .map(|s| s.parse().unwrap())
                        .collect();
                    db::remove(db, &ids)
                        .unwrap();
                },

First we need identifiers: we get them from the command line arguments as follows:

                    let ids: Vec = args[2..].iter()
                        .map(|s| s.parse().unwrap())
                        .collect();

Let foo: Vec <_> = ... .collect () is already familiar with us. It remains to understand what is happening inside this line.

args [2 ..] gets the slice of the vector - from the third element to the end of the vector. Looks like slices in Python.

.iter () gets an iterator over this slice, to which we apply an anonymous function using .map ():

                        .map(|s| s.parse().unwrap())

Our anonymous function takes a single argument - s - and parses it as an integer. How does she know that this must be a whole? From here:

                    let ids: Vec = 

(Hehe, in fact, not even from here, but from the signature of the db :: remove function - it takes a slice & [i32]. Type inference uses this information to understand that FromStr :: from_str needs to be called on i32. Therefore we could be here to use Vec <_> - but for the purpose of documenting the code, we specified the type explicitly. For db :: remove itself - below.)

In general, using iterator adapters like .map () is a common pattern in Rust code . It allows you to get controlled laziness of execution where it is most often needed - when streaming reading some data.

Great, we did all the preparatory work. It remains to update the database itself. insert looks really boring . Let's take a look at remove.

By the way, why is it written as db :: remove? Because it is in a separate module. At the file level, this means that it is in a separate source: src / db.rs. How is this module included in our main file? Like this:

mod db;

Simply! This instruction is equivalent to pasting the entire source code of the module in the place where it is written. (But in fact this does not happen, it's not a preprocessor. The entire container is compiled here at once, so the compiler can read the modules into memory and establish links at the intermediate representation level, rather than stupidly copy the source code as text.) that the compiler will look for a module in the files src / db.rs and src / db / mod.rs - this allows you to neatly organize the hierarchy of modules.

Now the code of our function:

pub fn remove(db: Connection, ids: &[i32]) -> ::postgres::Result {
    let stmt = db.prepare("DELETE FROM phonebook WHERE id=$1").unwrap();
    for id in ids {
        try!(stmt.execute(&[id]));
    }
    Ok(0)
}

So, here we know almost everything. In order.

pub means that the function is accessible from outside the module. Otherwise, we would not be able to call it from main, because by default, all functions inside the modules are hidden:

main.rs:81:21: 81:31 error: function `remove` is private
main.rs:81                     db::remove(db, &ids)
                               ^~~~~~~~~~

The type of the return value looks strange. :: postgres :: Result?

Two colons mean that the postgres module needs to be searched from the root of our container, and not from the current module. This module is automatically declared in main.rs when we do extern crate postgres. But it does not become visible in db.rs automatically! Therefore, we go to the root of the namespace using :: postgres. We could also re-request the binding of the postgres container in db.rs, but this is not considered good practice - it is better if all the binding requests are in one place, and the rest of the modules use what is available in the main one.

Ok, sorted out a bit with the modules. See here for more details .

Next we see an unprecedented hitherto macro: try!.

He, as his name suggests, is trying to perform some kind of operation. If it succeeds, the value of try! () Will be the value embedded in Ok (_). If not, it does something similar to return Err (error). This is an alternative to our constant .unwrap () - now the program will not end in a panic in case of an error, but will return the error up for processing by the calling function.

This macro can be used in functions that themselves return Result - otherwise the macro will not be able to return Err, because the type of the return value and the type of the value in return do not match.

With the removal of everything. Next, I will selectively go through the rest of the operations, describing what we do not yet know.

Here, for example, how transactions are handled:

{
    let tx: ::postgres::Transaction = db.transaction().unwrap();
    tx.execute(
            "UPDATE phonebook SET name = $1, phone = $2 WHERE id = $3",
            &[&name, &phone, &id]).unwrap();
    tx.set_commit();
}

As you can see, this is a typical application of RAII. We simply do not transfer tx anywhere, and it is destroyed upon exit from the block. Implementing its destructor saves or rolls back the transaction, depending on the success flag. If we had not done tx.set_commit (), the tx destructor would have rolled it back .

Here's how to format a string without printing to the screen:

    Some(s) => format!("WHERE name LIKE '%{}%'", s),

When we create a vector, we can immediately indicate how many elements it should allocate memory for:

    let mut results = Vec::with_capacity(size);

And finally, another example of functional style code:

    let max = rs.iter().fold(
        0,
        |acc, ref item|
        if item.name.len() > acc { item.name.len() } else { acc });

This code could be written easier if we compared the types for which the Ord trait was implemented :

    let max = rs.iter().max();

Or, we can implement this trait for Record. It requires the implementation of PartialOrd and Eq, and Eq, in turn, PartialEq. Therefore, in fact, 4 traits will have to be implemented. Fortunately, the implementation is trivial.

Implementing traits
use std::cmp::Ordering;
impl Ord for Record {
    fn cmp(&self, other: &Self) -> Ordering {
            self.name.len().cmp(&other.name.len())
    }
}
impl PartialOrd for Record {
    fn partial_cmp(&self, other: &Self) -> Option {
            Some(self.name.len().cmp(&other.name.len()))
    }
}
impl Eq for Record { }
impl PartialEq for Record {
    fn eq(&self, other: &Self) -> bool {
        self.id == other.id
                && self.name == other.name
                && self.phone == other.phone
    }
}
pub fn format(rs: &[Record]) {
    let max = rs.iter().max().unwrap();
    for v in rs {
        println!("{:3}   {:.*}   {}", v.id, max.name.len(), v.name, v.phone);
    }
}


It is worth noting that the meaningfulness of such an implementation is in question - it is still hardly worth comparing database records along the length of one of the fields.

By the way, the Eq type is one example of marker types: it does not require the implementation of any methods, but simply tells the compiler that some type has a certain property. Other examples of these types are Send and Sync, which we will talk more about.

That's all for today - the post turned out to be the longest of the series.

Now our application really works, but it does not have a REST interface yet. Next time we’ll get into the web part.

Also popular now: