Creating a REST Web Service

Historically, a lot of technologies have been developed and used to create a client-server system. In recent decades, though, all client-server architectures tend to be web-based—that is, based on the HyperText Transfer Protocol (HTTP). HTTP is based on the Transfer Control Protocol (TCP) and the Internet Protocol (IP). In particular, two web-based architectures have become popular—the Simple Object Access Protocol (SOAP) and Representational State Transfer (REST).

While SOAP is an actual protocol, REST is only a collection of principles. The web services adhering to the REST principles are said to be RESTful. In this chapter, we'll see how to build RESTful services using the popular Actix web framework.

Any web service (REST web services included) can be used by any web client—that is, any program that can send HTTP requests over a TCP/IP network. The most typical web clients are web pages running in a web browser, and containing JavaScript code. Any program written in any programming language and running in any operating system implementing the TCP/IP protocols can act as a web client.

The web servers are also known as the backend, while the web client is known as the frontend.

The following topics will be covered in this chapter:

  • The REST architecture
  • Building a stub of a web service using the Actix web framework and implementing the REST principles
  • Building a complete web service capable of uploading files, downloading files, and deleting files on client request
  • Handling an inner state as a memory database or a pool of connections to a database
  • Using JavaScript Object Notation (JSON) format to send data to clients

Technical requirements

To easily understand this chapter, you should have beginner knowledge of HTTP. The required concepts are as follows:

  • Uniform Resource Identifiers (URIs)
  • Methods (such as GET)
  • Headers
  • Body
  • Content type (such as plain/text)
  • Status code (such as Not Found=404)

Before starting the projects in this chapter, a generic HTTP client should be installed on your computer. The tool used in the examples is the command-line tool curl, freely available for many operating systems. The official download page ishttps://curl.haxx.se/download.html. In particular, the page for Microsoft Windows is https://curl.haxx.se/windows/.

Alternatively, you can use one of the several good, freeweb-browser utilities, such as Advanced REST Client for Chrome, or RESTED and RESTer for Firefox.

The complete source code for this chapter is in the Chapter03 folder of the repository, located at https://github.com/PacktPublishing/Creative-Projects-for-Rust-Programmers.

The REST architecture

The REST architecture is strongly based on the HTTP protocol but does not require any specific kind of data format, and so it can transmit data in several formats such as plain text, JSON, Extensible Markup Language (XML), or binary (encoded as Base64).

Many web resources describe what the REST architectural paradigm is. One such can be found at https://en.wikipedia.org/wiki/Representational_state_transfer.

However, the concept of the REST architecture is quite simple. It is the purest extension of the ideas behind the World Wide Web (WWW) project.

The WWW project was born in 1989 as a global library of hypertexts. A hypertext is a document that contains links to other documents so that, by clicking repeatedly on the links, you can see many documents by using only your mouse. Such documents are scattered over the internet and are identified by a unique description, the Uniform Resource Locator (URL). The protocol to share such documents is HTTP, and the documents are written in HyperText Markup Language (HTML). A document can embed images, referenced by URL addresses too.

The HTTP protocol allows you to download pages to your document viewer (the web browser), but also to upload new documents to be shared with other people. You can also replace existing documents with a new version, or delete existing documents.

If the concept of a document or file is replaced by that of named data, or a resource, you get the concept of REST. Any interaction with a RESTful server is a manipulation of a piece of data, referencing it by its name. Of course, such data can be a disk file, but it can also be a set of records in a database that is identified by a query, or even a variable kept in memory.

A peculiar aspect of RESTful servers is the absence of server-side client sessions. As with any hypertext server, RESTful servers do not store the fact that a client has logged in. If there is some data associated with a session, such as the current user or the previously visited pages, that data belongs only to the client side. As a consequence, any time the client needs access to privileged services, or to user-specific data, the request must contain the credentials of the user.

To improve performance, the server can store session information in a cache, but that should be transparent. The server (except for its performance) should behave as if it doesn't keep any session information.

Project overview

We are going to build several projects, introducing new features in every project. Let's look at each one, as follows:

  • The first project will build a stub of a service that should allow any client to upload, download, or delete files from the server. This project shows how to create a REST application programming interface (API), but it does no useful work.
  • The second project will implement the API described in the previous project. It will build a service that actually allows any client to upload, download, or delete files from the server filesystem.
  • The third project will build a service that allows clients to add key-value records to a memory database residing in the server process, and to recall some predefined queries built into the server. The result of such queries will be sent back to the client in plain text format.
  • The fourth project will be similar to the third one, but the results will be encoded in JSON format.

Our source code is small, but it includes the Actix web crate, which in turn includes around 200 crates, and so the first build of any project will take around 10 minutes. Following any changes to the application code, a build will take from 12 to 30 seconds.

The Actix web crate has been chosen as it is the most feature-full, reliable, high-performance, and well-documented server-side web application framework for Rust.

This framework is not limited to RESTful services, as it can be used to build different kinds of server-side web software. It is an extension of the Actix net framework, which is a framework designed to implement different kinds of network services.

Essential background theory and context

Previously, we said that a RESTful service is based on the HTTP protocol. This is a rather complex protocol, but its most important parts are quite simple. Here is a simplified version of it.

The protocol is based on a pair of messages. First, the client sends a request to the server, and after the server receives this request, it replies by sending a response to the client. Both messages are in American Standard Code for Information Interchange (ASCII) text, and so they are easily manipulated.

The HTTP protocol is usually based on the TCP/IP protocol, which guarantees that these messages arrive at the addressed process.

Let's see a typical HTTP request message, as follows:

GET /users/susan/index.html HTTP/1.1
Host: www.acme.com
Accept: image/png, image/jpeg, */*
Accept-Language: en-us
User-Agent: Mozilla/5.0

This message contains six lines because there is an empty line at the end.

The first line begins with the word GET. This word is the method that specifies which operation is requested. Then, there is a Unix-style path of a resource, and then the version of the protocol (here, it is 1.1).

Then, there are four lines containing rather simple attributes. These attributes are name headers. There are many possible optional headers.

What follows the first empty line is the body. Here, the body is empty. The body is used to send raw data—even a lot of data.

So, any request from the HTTP protocol sends a command name (the method) to a specific server, followed by an identifier of a resource (the path). Then, there are a few attributes (one per line), then an empty line, and, finally, the possible raw data (the body).

The most important methods are detailed as follows:

  • GET: This requests a resource to be downloaded from the server (typically an HTML file or an image file, but also any data). The path specifies where the resource should be read.
  • POST: This sends some data to the server that the server should consider as new. The path specifies where to add this data. If the path identifies any existing data, the server should return an error code. The contents of the data to post are in the body section.
  • PUT: This is similar to the POST command, but it is meant to replace existing data.
  • DELETE: This requests the resource to be removed specified by the path. It has an empty body.

Here is a typical HTTP response message:

HTTP/1.1 200 OK
Date: Wed, 15 Apr 2020 14:03:39 GMT
Server: Apache/2.2.14
Accept-Ranges: bytes
Content-Length: 42
Connection: close
Content-Type: text/html

<html><body><p>Some text</p></body></html>

The first line of any response message begins with the protocol version, followed by the status code both in text format and in numeric format. Success is represented by 200 OK.

Then, there are several headers—six, in this example—then an empty line, and then the body, which may be empty. In this case, the body contains some HTML code.

You can find more information regarding the HTTP protocol at: https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol.

Building a stub of a REST web service

The typical example of a REST service is a web service designed for uploading and downloading text files. As it would be too complex to understand, first we will look at a simpler project, the file_transfer_stub project, which mimics this service without actually doing anything on the filesystem.

You will see how an API of a RESTless web service is structured, without being overwhelmed by the details regarding the implementation of the commands.

In the next section, this example will be completed with the needed implementation, to obtain a working file-managing web app.

Running and testing the service

To run this service, it is enough to type the commandcargo run in a console. After building the program, it will print Listening at address 127.0.0.1:8080 ..., and it will remain listening for incoming requests.

To test it, we need a web client. You can use a browser extension if you prefer, but in this chapter, the curl command-line utility will be used.

Thefile_transfer_stub service and the file_transfer service (we'll see them in the next section) have the same API, containing the following four commands:

  1. Download a file with a specified name.
  2. Upload a file with a specified name and specified contents.
  3. Upload a file with a specified name prefix and specified contents, obtaining the complete name as a response.
  4. Delete a file with a specified name.

Getting a resource using the GET method

To download a resource in the REST architecture, the GET method should be used. For these commands, the URL should specify the name of the file to download. No additional data should be passed, and the response should contain the contents of the file and the status code, which can be 200, 404, or 500:

  1. Type the following command into a console:
          curl -X GET http://localhost:8080/datafile.txt
        
  1. In that console, the following mock line should be printed, and then the prompt should appear immediately:
          Contents of the file.
        
  1. Meanwhile, on the other console, the following line should be printed:
          Downloading file "datafile.txt" ... Downloaded file "datafile.txt"
        

This command mimics the request to download thedatafile.txt file from the filesystem of the server.

  1. The GET method is the default one for curl, and hence you can simply type the following:
          curl http://localhost:8080/datafile.txt
        
  1. In addition, you can redirect the output to any file by typing the following:
          curl http://localhost:8080/datafile.txt >localfile.txt
        

So, we have now seen how our web service can be used by curl to download a remote file, to print it on the console, or to save it in a local file.

Sending a named resource to the server using the PUT method

To upload a resource in the REST architecture, either the PUT or POST methods should be used. The PUT method is used when the client knows where the resource should be stored, in essence, what will be its identifying key. If there is already a resource that has this key, that resource will be replaced by the newly uploaded resource:

  1. Type the following command into a console:
          curl -X PUT http://localhost:8080/datafile.txt -d "File contents."
        
  1. In that console, the prompt should appear immediately. Meanwhile, on the other console, the following line should be printed:
          Uploading file "datafile.txt" ... Uploaded file "datafile.txt"
        

This command mimics the request to send a file to the server, with the client specifying the name of that resource, so that if a resource with that name already exists, it is overwritten.

  1. You can use curl to send the data contained in a specified local file in the following way:
          curl -X PUT http://localhost:8080/datafile.txt -d @localfile.txt
        
Here, the curl command has an additional argument, -d, which allows us to specify the data we want to send to the server. If it is followed by an @ symbol, the text following this symbol is used as the path of the uploaded file.

For these commands, the URI should specify the name of the file to upload and also the contents of the file, and the response should contain only the status code, which can be 200, 201 (Created), or 500. The difference between 200 and 201 is that in the first case, an existing file is overwritten, and in the second case, a new file is created.

So, we have now learned how our web service can be used by curl to upload a string into a remote file, while also specifying the name of the file.

Sending a new resource to the server using the POST method

In the REST architecture, the POST method is the one to use when it is the responsibility of the service to generate an identifier key for the new resource. Thus, the request does not have to specify it. The client can specify a pattern or prefix for the identifier, though. As the key is automatically generated and unique, there cannot be another resource that has the same key. The generated key should be returned to the client, though, because otherwise, it cannot reference that resource afterward:

  1. To upload a file with an unknown name, type the following command into the console:
          curl -X POST http://localhost:8080/data -d "File contents."
        
  1. In that console, the text data17.txt should be printed, and then the prompt should appear. This text is the simulated name of the file, received from the server. Meanwhile, on the other console, the following line should be printed:
          Uploading file "data*.txt" ... Uploaded file "data17.txt"
        

This command represents the request to send a file to the server, with the server specifying a new unique name for that resource so that no other resource will be overwritten.

For this command, the URI should not specify the full name of the file to upload, but only a prefix; of course, the request should also contain the contents of the file. The response should contain the complete name of the newly created file and the status code. In this case, the status code can only be 201 or 500, because the possibility of a file already existing is ruled out.

We have now learned how our web service can be used by curl to upload a string into a new remote file, leaving the task of inventing a new name for that file to the server. We have also seen that the generated filename is sent back as a response.

Deleting a resource using the DELETE method

In the REST architecture, to delete a resource, the DELETE method should be used:

  1. Type the following command into a console (don't worry—no file will be deleted!):
          curl -X DELETE http://localhost:8080/datafile.txt
        
  1. After typing that command, the prompt should appear immediately. Meanwhile, in the server console, the following line should be printed:
          Deleting file "datafile.txt" ... Deleted file "datafile.txt"
        

This command represents the request to delete a file from the filesystem of the server. For such a command, the URL should specify the name of the file to delete. No additional data needs to be passed, and the only response is the status code, which can be 200, 404, or 500. So, we have seen how our web service can be used by curl to delete a remote file.

As a summary, the possible status codes of this service are as follows:

  • 200: OK
  • 201: Created
  • 404: Not Found
  • 500: Internal Server Error

Also, the four commands of our API are as follows:

Method URI Request data format Response data format Status codes
GET /{filename} --- text/plain 200, 404, 500
PUT /{filename} text/plain --- 200, 201, 500
POST /{filename prefix} text/plain text/plain 201, 500
DELETE /{filename} --- --- 200, 404, 500

Sending an invalid command

Let's see the behavior of the server when an invalid command is received:

  1. Type the following command into a console:
          curl -X GET http://localhost:8080/a/b
        
  1. In that console, the prompt should appear immediately. Meanwhile, in the other console, the following line should be printed:
          Invalid URI: "/a/b"
        

This command represents the request to get the /a/b resource from the server, but, as our API does not permit this method of specifying a resource, the service rejects the request.

Examining the code

The main function contains the following statements:

HttpServer::new(|| ... )
.bind(server_address)?
.run()

The first line creates an instance of an HTTP server. Here, the body of the closure is omitted.

The second line binds the server to an IP endpoint, which is a pair composed of an IP address and an IP port, and returns an error if such a binding fails.

The third line puts the current thread in listening mode on that endpoint. It blocks the thread, waiting for incoming TCP connection requests.

The argument of the HttpServer::new call is a closure, shown here:

App::new()
.service(
web::resource("/{filename}")
.route(web::delete().to(delete_file))
.route(web::get().to(download_file))
.route(web::put().to(upload_specified_file))
.route(web::post().to(upload_new_file)),
)
.default_service(web::route().to(invalid_resource))

In this closure, a new web app is created, and then one call to theservice function is applied to it. Such a function contains a call to theresourcefunction, which returns an object on which four calls to theroutefunction are applied. Lastly, a call to thedefault_service function is applied to the application object.

This complex statement implements a mechanism to decide which function to call based on the path and method of the HTTP request. In web programming parlance, such a kind of mechanism is named routing.

The request routing first performs pattern matching between the address URI and one or several patterns. In this case, there is only one pattern, /{filename}, which describes a URI that has an initial slash and then a word. The word is associated with the filename name.

The four calls to theroutemethod proceed with the routing, based on the HTTP method (DELETE, GET, PUT, POST). There is a specific function for every possible HTTP method, followed by a call to the to function that has a handling function as an argument.

Such calls to route mean that the following applies:

  • If the request method of the current HTTP command isDELETE, then such a request should be handled by going to thedelete_file function.
  • If the request method of the current HTTP command isGET, then such a request should be handled by going to thedownload_file function.
  • If the request method of the current HTTP command isPUT, then such a request should be handled by going to theupload_specified_file function.
  • If the request method of the current HTTP command isPOST, then such a request should be handled by going to theupload_new_file function.

Such four handling functions, named handlers, must of course be implemented in the current scope. In actuality, they are defined, albeit interleaved withTODOcomments, recalling what is missing to have a working application instead of a stub. Nevertheless, such handlers contain much functionality.

Such a routing mechanism can be read in English, in this way—for example, for a DELETE command:

Create a service to manage the web::resource named /{filename}, to route a delete command to the delete_file handler.

After all of the patterns, there is the call to thedefault_service function that represents a catch-all pattern, typically to handle invalid URIs, such as /a/bin the previous example.

The argument of the catch-all statement—that is, web::route().to(invalid_resource), causes the routing to the invalid_resource function. You can read it as follows:

For this web command, route it to the invalid_resource function.

Now, let's see the handlers, starting with the simplest one, as follows:

fn invalid_resource(req: HttpRequest) -> impl Responder {
println!("Invalid URI: "{}"", req.uri());
HttpResponse::NotFound()
}

This function receives an HttpRequest object and returns something implementing the Responder trait. It means that it processes an HTTP request, and returns something that can be converted to an HTTP response.

This function is quite simple because it does so little. It prints the URI to the console and returns a Not Found HTTP status code.

The other four handlers get a different argument, though. It is the following: info: Path<(String,)>. Such an argument contains a description of the path matched before, with the filename argument put into a single-value tuple, inside a Path object. This is because such handlers do not need the whole HTTP request, but they need the parsed argument of the path.

Notice that we have one handler receiving an argument of theHttpRequest type, and the others receiving an argument of the Path<(String,)> type. This syntax is possible because the to function, called in the main function, expects as an argument a generic function, whose arguments can be of several different types.

All four handlers begin with the following statement:

let filename = &info.0;

Such a statement extracts a reference to the first (and only) field of the tuple containing the parameters resulting from the pattern matching of the path. This works as long as the path contained exactly one parameter. The /a/b path cannot be matched with the pattern, because it has two parameters. Also, the / path cannot be matched, because it has no parameters. Such cases end in the catch-all pattern.

Now, let's examine the delete_file function specifically. It continues with the following lines:

print!("Deleting file "{}" ... ", filename);
flush_stdout();

// TODO: Delete the file.

println!("Deleted file "{}"", filename);
HttpResponse::Ok()

It has two informational printing statements, and it ends returning a success value. In the middle, the actual statement to delete the file is still missing. The call to the flush_stdout function is needed to emit the text on the console immediately.

The download_file function is similar, but, as it has to send back the contents of the file, it has a more complex response, as illustrated in the following code snippet:

HttpResponse::Ok().content_type("text/plain").body(contents)

The object returned by the call to Ok() is decorated, first by calling content_type and setting text/plain as the type of the returned body, and then by calling body and setting the contents of the file as the body of the response.

The upload_specified_file function is quite simple, as its two main jobs are missing: getting the text to put in the file from the body of the request, and saving that text into the file, as illustrated in the following code block:

print!("Uploading file "{}" ... ", filename);
flush_stdout();

// TODO: Get from the client the contents to write into the file.
let _contents = "Contents of the file. ".to_string();

// TODO: Create the file and write the contents into it.

println!("Uploaded file "{}"", filename);
HttpResponse::Ok()

The upload_new_file function is similar, but it should have another step that is still missing: to generate a unique filename for the file to save, as illustrated in the following code block:

print!("Uploading file "{}*.txt" ... ", filename_prefix);
flush_stdout();

// TODO: Get from the client the contents to write into the file.
let _contents = "Contents of the file. ".to_string();

// TODO: Generate new filename and create that file.
let file_id = 17;

let filename = format!("{}{}.txt", filename_prefix, file_id);

// TODO: Write the contents into the file.

println!("Uploaded file "{}"", filename);
HttpResponse::Ok().content_type("text/plain").body(filename)

So, we have examined all of the Rust code of the stub of the web service. In the next section, we'll look at the complete implementation of this service.

Building a complete web service

The file_transfer project completes the file_transfer_stub project, by filling in the missing features.

The features were omitted in the previous project for the following reasons:

  • To have a very simple service that actually does not really access the filesystem
  • To have only synchronous processing
  • To ignore any kind of failure, and keep the code simple

Here, these restrictions have been removed. First of all, let's see what happens if you compile and run the file_transfer project, and then test it using the same commands as in the previous section.

Downloading a file

Let's try the following steps on how to download a file:

  1. Type the following command into the console:
          curl -X GET http://localhost:8080/datafile.txt
        
  1. If the download is successful, the server prints the following line to the console:
          Downloading file "datafile.txt" ... Downloaded file "datafile.txt"
        
In the console of the client, curl prints the contents of that file.

In the case of an error, the service prints the following:

          Downloading file "datafile.txt" ... Failed to read file "datafile.txt": No such file or directory (os error 2)
        

We have now seen how our web service can be used by curl to download a file. In the next sections, we'll learn how our web service can perform other operations on remote files.

Uploading a string to a specified file

Here is the command to upload a string into a remote file with a specified name:

          curl -X PUT http://localhost:8080/datafile.txt -d "File contents."
        

If the upload is successful, the server prints the following to the console:

          Uploading file "datafile.txt" ... Uploaded file "datafile.txt"
        

If the file already existed, it is overwritten. If it didn't exist, it is created.

In the case of an error, the web service prints the following line:

          Uploading file "datafile.txt" ... Failed to create file "datafile.txt"
        

Alternatively, it prints the following line:

          Uploading file "datafile.txt" ... Failed to write file "datafile.txt"
        

This is how our web service can be used by curl to upload a string into a remote file while specifying the name of the file.

Uploading a string to a new file

Here is the command to upload a string into a remote file with a name chosen by the server:

          curl -X POST http://localhost:8080/data -d "File contents."
        

If the upload is successful, the server prints to the console something similar to the following:

          Uploading file "data*.txt" ... Uploaded file "data917.txt"
        

This output shows that the name of the file contains a pseudo-random number— for this example, this is 917, but you'll probably see some other number.

In the console of the client, curl prints the name of that new file, as the server has sent it back to the client.

In the case of an error, the server prints the following line:

          Uploading file "data*.txt" ... Failed to create new file with prefix "data", after 100 attempts.
        

Alternatively, it prints the following line:

          Uploading file "data*.txt" ... Failed to write file "data917.txt"
        

This is how our web service can be used by curl to upload a string into a new remote file, leaving the task of inventing a new name for that file to the server. The curl tool receives this new name as a response.

Deleting a file

Here is the command to delete a remote file:

          curl -X DELETE http://localhost:8080/datafile.txt
        

If the deletion is successful, the server prints the following line to the console:

          Deleting file "datafile.txt" ... Deleted file "datafile.txt"
        

Otherwise, it prints this:

          Deleting file "datafile.txt" ... Failed to delete file "datafile.txt": No such file or directory (os error 2)
        

This is how our web service can be used by curl to delete a remote file.

Examining the code

Let's now examine the differences between this program and the one described in the previous section. The Cargo.toml file contains two new dependencies, as illustrated in the following code snippet:

futures = "0.1"
rand = "0.6"

The futures crate is needed for asynchronous operations, and the rand crate is needed for randomly generating the unique names of the uploaded files.

Many new data types have been imported from the external crates, as can be seen in the following code block:

use actix_web::Error;
use futures::{
future::{ok, Future},
Stream,
};
use rand::prelude::*;
use std::fs::{File, OpenOptions};

The main function has just two changes, as follows:

.route(web::put().to_async(upload_specified_file))
.route(web::post().to_async(upload_new_file)),

Here, two calls to the to function have been replaced by calls to the to_async function. While the to function is synchronous (that is, it keeps the current thread busy until that function is completed), the to_async function is asynchronous (that is, it can be postponed until the expected events have happened).

This change was required by the nature of upload requests. Such requests can send large files (several megabytes), and the TCP/IP protocol sends such files split into small packets. If the server, when it receives the first packet, just waits for the arrival of all the packets, it can waste a lot of time. Even with multithreading, if many users upload files concurrently, the system will dedicate as many threads as possible to handle such uploads, and this is rather inefficient. A more performant solution is asynchronous processing.

Theto_async function, though, cannot receive as an argument a synchronous handler. It must receive a function that returns a value having the impl Future<Item = HttpResponse, Error = Error> type, instead of the impl Responder type, returned by synchronous handlers. This is actually the type returned by the two upload handlers: upload_specified_file and upload_new_file.

The object returned is of an abstract type, but it must implement the Future trait. The concept of a future, used also in C++ since 2011, is similar to JavaScript promises. It represents a value that will be available in the future, and in the meantime, the current thread can handle some other events.

Futures are implemented as asynchronous closures, meaning that these closures are put in a queue in an internal futures list, and not run immediately. When no other task is running in the current thread, the future at the top of the queue is removed from the queue and executed.

If two futures are chained, the failure of the first chain causes the second future to be destroyed. Otherwise, if the first future of the chain succeeds, the second future has the opportunity to run.

Going back to the two upload functions, another change for their signature is the fact that they now get two arguments. In addition to the argument of the Path<(String,)> type, containing the filename, there is an argument of the Payload type. Remember that the contents can arrive piece-wise, and so such a Payload argument does not contain the text of the file, but it is an object to get the contents of the uploaded file asynchronously.

Its use is somewhat complex.

First, for both upload handlers, there is the following code:

payload
.map_err(Error::from)
.fold(web::BytesMut::new(), move |mut body, chunk| {
body.extend_from_slice(&chunk);
Ok::<_, Error>(body)
})
.and_then(move |contents| {

The call to map_err is required to convert the error type.

The call to fold receives from the network one chunk of data at a time and uses it to extend an object of the BytesMut type. Such a type implements a kind of extensible buffer.

The call to and_then chains another future to the current one. It receives a closure that will be called when the processing of fold will be finished. Such a closure receives all the uploaded contents as an argument. This is a way to chain two futures—any closure invoked in this way is executed asynchronously, after the previous one is finished.

The contents of the closure simply write the received contents into a file with the specified name. This operation is synchronous.

The last line of the closure is ok(HttpResponse::Ok().finish()). This is the way to return from a future. Notice the lowercase ok.

The upload_new_file function is similar to the previous one, in terms of the web programming concepts. It is more complex, just because of the following:

  • Instead of having a complete filename, only a prefix is provided, and the rest must be generated as a pseudo-random number.
  • The resulting filename must be sent to the client.

The algorithm to generate a unique filename is the following:

  1. A three-digit pseudo-random number is generated, and it is concatenated to the prefix.
  2. The name obtained is used to create a file; this avoids overwriting an existing file with that name.
  3. If a collision happens, another number is generated until a new file is created, or until 100 failed attempts have been tried.

Of course, this assumes that the number of uploaded files will always be significantly less than 1,000.

Other changes have been made to consider the chance of failure.

The final part of the delete_file function now looks like this:

match std::fs::remove_file(&filename) {
Ok(_) => {
println!("Deleted file "{}"", filename);
HttpResponse::Ok()
}
Err(error) => {
println!("Failed to delete file "{}": {}", filename, error);
HttpResponse::NotFound()
}
}

This code handles the case of a failure in the deletion of the file. Notice that in the case of an error, instead of returning the success status code HttpResponse::Ok() representing the number 200, a HttpResponse::NotFound() failure code is returned, representing the number 404.

The download_file function now contains a local function to read the whole contents of a file into a string, as follows:

fn read_file_contents(filename: &str) -> std::io::Result<String> {
use std::io::Read;
let mut contents = String::new();
File::open(filename)?.read_to_string(&mut contents)?;
Ok(contents)
}

The function ends with some code to handle the possible failure of the function, as follows:

match read_file_contents(&filename) {
Ok(contents) => {
println!("Downloaded file "{}"", filename);
HttpResponse::Ok().content_type("text/plain").body(contents)
}
Err(error) => {
println!("Failed to read file "{}": {}", filename, error);
HttpResponse::NotFound().finish()
}
}

Building a stateful server

The web app of the file_transfer_stubproject was completely stateless, meaning that every operation had the same behavior independently of the previous operations. Other ways to explain this are that no data was kept from one command to the next, or that it computed pure functions only.

The web app of the file_transfer project had a state, but that state was confined to the filesystem. Such a state was the content of the data files. Nevertheless, the application itself was still stateless. No variable survived from one request handling to another request handling.

The REST principles are usually interpreted as prescribing that any API must be stateless. That is a misnomer because REST services can have a state, but they must behave as if they were stateless. To be stateless means that, except for the filesystem and the database, no information survives in the server from one request handling to another request handling. To behave as if stateless means that any sequence of requests should obtain the same results even if the server is terminated and restarted between one request and a successive one.

Clearly, if the server is terminated, its state is lost. So, to behave as stateless means that the behavior should be the same even if the state is reset. So, what is the purpose of the possible server state? It is to store information that can be obtained again with any request, but that would be costly to do so. This is the concept of caching.

Usually, any REST web server has an internal state. The typical information stored in this state is a pool of connections to the database. A pool is initially empty, and when the first handler must connect to the database, it searches the pool for an available connection. If it finds one, it uses it. Otherwise, a new connection is created and added to the pool. A pool is a shared state that must be passed to any request handler.

In the projects of the previous sections, the request handlers were pure functions; they had no possibility of sharing a common state. In the memory_db project, we'll see how we can have a shared state in the Actix web framework that is passed to any request handler.

This web app represents access to a very simple database. Instead of performing actual access to a database, which would require further installations in your computer, it simply invokes some functions exported by the data_access module, defined in the src/data_access.rs file, that keep the database in memory.

A memory database is a state that is shared by all the request handlers. In a more realistic app, a state would contain only one or more connections to an external database.

How to have a stateful server

To have a state in an Actix service, a struct must be declared, and any data that should be part of the state should be a field of that struct.

At the beginning of the main.rs file, there is the following code:

struct AppState {
db: db_access::DbConnection,
}

In the state of our web app, we need only one field, but other fields can be added.

The DbConnectiontype declared in thedb_accessmodule represents the state of our web app. In themainfunction, just before creating the server, there is the following statement that instantiates the AppState, and then properly encapsulates it:

let db_conn = web::Data::new(Mutex::new(AppState {
db: db_access::DbConnection::new(),
}));

The state is shared by all the requests, and the Actix web framework uses several threads to handle the requests, and so the state must be thread-safe. The typical way of declaring a thread-safe object in Rust is to encapsulate it in a Mutex object. This object is then encapsulated in a Data object.

To ensure that such a state is passed to any handler, the following line must be added before calling the service functions:

.register_data(db_conn.clone())

Here, the db_conn object is cloned (cheaply, as it is a smart pointer), and it is registered into the app.

The effect of this registration is that it is now possible to add another type of argument to the request handlers (both synchronous and asynchronous), as follows:

state: web::Data<Mutex<AppState>>

Such an argument can be used in statements like this:

let db_conn = &mut state.lock().unwrap().db

Here, the state is locked to prevent concurrent access by other requests, and its db field is accessed.

The API of this service

The rest of the code in this app is not particularly surprising. The API is clear from the names used in the main function, as illustrated in the following code block:

.service(
web::resource("/persons/ids")
.route(web::get().to(get_all_persons_ids)))
.service(
web::resource("/person/name_by_id/{id}")
.route(web::get().to(get_person_name_by_id)),
)
.service(
web::resource("/persons")
.route(web::get().to(get_persons)))
.service(
web::resource("/person/{name}")
.route(web::post().to(insert_person)))
.default_service(
web::route().to(invalid_resource))

Notice that the first three patterns use the GET method, and so theyquerythe database. The last one uses the POST method, and so it inserts new records into the database.

Notice also the following lexical conventions.

The path of the URI for the first and third patterns begins with the plural word persons, which means that zero, one, or several items will be managed by this request and that any such item represents a person. Instead, the path of the URI for the second and fourth patterns begins with the singular word person, and this means that no more than one item will be managed by this request.

The first pattern ends with the plural word ids, and so several items regarding the id will be handled. It has no condition, and so all the IDs are requested. The second pattern contains the word name_by_id, followed by an id parameter, and so it is a request of the name database column for all the records for which the id column has the value specified.

Even in the case of any doubt, the name of the handling functions or comments should make the behavior of the service clear, without having to read the code of the handlers. When looking at the implementation of the handlers, notice that they either return nothing at all or simple text.

Testing the service

Let's test the service with some curl operations.

First of all, we should populate the database that is initially empty. Remember that, being only in memory, it is empty any time you start the service.

After starting the program, type the following commands:

          curl -X POST http://localhost:8080/person/John
          
curl -X POST http://localhost:8080/person/Jonathan
curl -X POST http://localhost:8080/person/Mary%20Jane

After the first command, a number 1 should be printed to the console. After the second command, 2 should be printed, and after the third command, 3 should be printed. They are the IDs of the inserted names of people.

Now, type the following command:

          curl -X GET http://localhost:8080/persons/ids
        

It should print the following: 1, 2, 3. This is the set of all the IDs in the database.

Now, type the following command:

          curl -X GET http://localhost:8080/person/name_by_id/3
        

It should print the following: Mary Jane. This is the name of the unique person for which the id is equal to 3. Notice that the input sequence %20 has been decoded into a blank.

Now, type the following command:

          curl -X GET http://localhost:8080/persons?partial_name=an
        

It should print the following: 2: Jonathan; 3: Mary Jane. This is the set of all the people for which the name column contains the an substring.

Implementing the database

The whole database implementation is kept in the db_access.rs source file.

The implementation of the database is quite simple. It is a DbConnection type, containing Vec<Person>, wherePersonis a struct of two fields—idandname.

The methods of DbConnection are described as follows:

  • new: This creates a new database.
  • get_all_persons_ids(&self) -> impl Iterator<Item = u32> + '_: This returns an iterator that provides all the IDs contained in the database. The lifetime of such an iterator must be no more than that of the database itself.
  • get_person_name_by_id(&self, id: u32) -> Option<String>: This returns the name of the unique person having the specified ID if there is one, or zero if there isn't one.
  • get_persons_id_and_name_by_partial_name<'a>(&'a self, subname: &'a str) -> impl Iterator<Item = (u32, String)> + 'a: This returns an iterator that provides the ID and the name of all the people whose name contains the specified string. The lifetime of such an iterator must be no more than that of the database itself, and also no more than that of the specified string.
  • insert_person(&mut self, name: &str) -> u32: This adds a record to the database, containing a generated ID and the specified name. This returns the generated ID.

Handling queries

The request handlers, contained in the main.rs file, get arguments of several types, as follows:

  • web::Data<Mutex<AppState>>: As described previously, this is used to access the shared app state.
  • Path<(String,)>: As described in the previous sections, this is used to access the path of the request.
  • HttpRequest: As described in the previous sections, this is used to access general request information.
But also, the request handlers get the web::Query<Filter> argument to access the optional arguments of the request.

The get_persons handler has a query argument—it is a generic argument, whose parameter is the Filter type. Such a type is defined as follows:

#[derive(Deserialize)]
pub struct Filter {
partial_name: Option<String>,
}

This definition allows requests such as http://localhost:8080/persons?partial_name=an. In this request, the path is just /persons, while ?partial_name=anis the so-called query. In this case, it contains just one argument whose key is partial_name, and whose value is an. It is a string and it is optional. This is exactly what is described by the Filter struct.

In addition, such a type is deserializable, as such an object must be read by the request through serialization.

The get_persons function accesses the query through the following expression:

&query.partial_name.clone().unwrap_or_else(|| "".to_string()),

The partial_name field is cloned to get a string. If it is nonexistent, it is taken as an empty string.

Returning JSON data

The previous section returned data in plain text. This is unusual in a web service and rarely satisfactory. Usually, web services return data in JSON, XML, or another structured format. The json_db project is identical to the memory_db project, except for its returning data in the JSON format.

First of all, let's see what happens when the same curl commands from the previous section are executed on it, as follows:

  • The insertions have the same behavior because they just printed a number.
  • The first query should print the following: [1,2,3]. The three numbers are in an array, and so they are enclosed in brackets.
  • The second query should print the following: "Mary Jane". The name is a string, and so it is enclosed in quotation marks.
  • The third query should print the following: [[2,"Jonathan"],[3,"Mary Jane"]]. The sequence of persons is an array of two records, and each of them is an array of two values, which are a number and a string.

Now, let's see the differences in the code of this project with respect to the previous one.

In the Cargo.toml file, one dependency has been added, as follows:

serde_json = "1.0"

This is needed to serialize the data in JSON format.

In the main.rs file, the get_all_persons_ids function (instead of returning simply a string) has the following code:

HttpResponse::Ok()
.content_type("application/json")
.body(
json!(db_conn.get_all_persons_ids().collect::<Vec<_>>())
.to_string())

First, a response with a status code Ok is created; then, its content type is set to application/json, to let the client know how to interpret the data it will receive; and lastly, its body is set, using the json macro taken from the serde_json crate. This macro takes an expression—in this case, with type, Vec<Person>—and returns a serde_json::Value value. Now, we need a string, and so to_string() is called. Notice that the json! macro requires its argument to implement the Serialize trait or to be convertible into a string.

The get_person_name_by_id, get_persons, andinsert_personfunctions have similar changes. Themainfunction has no changes. The db_access.rs files are identical.

Summary

We have learned about a few features of the Actix web framework. It is a really complex framework that covers most needs of the backend web developer, and it is still in active development.

Particularly, in thefile_transfer_stubproject, we learned how to create an API of a RESTful service. In thefile_transferproject, we discussed how to implement the operations of our web service. In thememory_db project, we went through how to manage an inner state, in particular, one containing a database connection. In the json_db project, we have seen how to send a response in JSON format.

In the next chapter, we will be learning how to create a full server-side web application.

Questions

  1. According to the REST principles, what are the meanings of the GET, PUT, POST, and DELETE HTTP methods?
  2. Which command-line tool can be used to test a web service?
  3. How can a request handler retrieve the value of URI parameters?
  4. How can the content type of an HTTP response be specified?
  5. How can a unique file name be generated?
  6. Why do services that have a stateless API need to manage a state?
  7. Why must the state of a service be encapsulated in a Data and a Mutex object?
  8. Why may asynchronous processing be useful in a web service?
  9. What is the purpose of theand_thenfunction of futures?
  10. Which crates are useful to compose an HTTP response in JSON format?

Further reading

To learn more about the Actix framework, view the official documentation at https://actix.rs/docs/, and view official examples at https://github.com/actix/examples/.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset