Integrating Naurt’s Geocoder with Rust

A full guide on creating a Rust-based web server to plot Naurt geocodes on a map

Indigo Curnick
August 1, 2024
Resources

Rust is a powerful modern language with strong typing and memory safety which makes it a perfect backend choice. In this blog post, we’ll be integrating Naurt’s geocoder into a simple Rust server, with the end goal of displaying a map of the results. We’ll use Rocket for the server, with Tera templates and Plotly for the map.

Setting Up the Environment with Rust

Make sure you have Rust installed correctly - cargo version should output something on your command line. Run

$ cargo init naurt_rust

Then add the following into the Cargo.toml file

1[dependencies]
2plotly = "0.8.4"
3rocket = "0.5.1"
4rocket_codegen = "0.5.1"
5reqwest = { version = "0.12.5", features = ["json"] }
6tokio = { version = "1.38", features = ["full"] }
7rocket_dyn_templates = { version = "0.1.0", features = ["tera"] }
8serde = { version = "1.0", features = ["derive"] }
9serde_json = "1.0.118"

You’ll also need an API key to use Naurt. You can get one for free from our dashboard, it comes loaded with thousands of API calls and we don’t require a credit card for sign up. Place your API key in a file called api.key next to the Cargo.toml.

One final thing we need to do is add

#[macro_use]
extern crate rocket;

To the top of main.rs - this will allow us to use the macros from the Rocket crate.

A Simple Web Server

Let’s start by just getting a hello world from a server. Rocket is an extremely easy to use and powerful web server.

1#[macro_use]
2extern crate rocket;
3
4#[rocket::main]
5async fn main() {
6    let figment = rocket::Config::figment()
7        .merge(("port", 8080))
8        .merge(("address", "0.0.0.0"));
9
10    if let Err(e) = rocket::custom(figment)
11        .mount("/", routes![handler])
12        .launch()
13        .await
14    {
15        println!("Did not run. Error: {:?}", e)
16    }
17}
18
19#[get("/")]
20async fn handler() -> String {
21    return "Hello From Rocket!".to_string();
22}
23

This is the bare bones Rocket server. We create the server itself in main and then give it a list of routes - in this case just handler. Rust’s powerful macro system is already on full display - #[get(/)] means all GET requests to /  are sent to handler . Any other HTTP method will respond with the built in 404.

Also note that Rocket can have async or non-async functions as handlers, but we’re going to be making web requests in this function later on, so we might as well mark it appropriately now.

If you go to http://localhost:8080/ you should see our hello message!

Managing State

Since we will want to make requests to Naurt which requires an API key, then we’ll need that in our program. It’s already in the project in a file called api.key but reading from disc every time would be inefficient. Thankfully, Rocket provides first class support for managing state safely between threads. Edit the main function to contain this:

1let api_key = fs::read_to_string("api.key").unwrap();
2
3let figment = rocket::Config::figment()
4    .merge(("port", 8080))
5    .merge(("address", "0.0.0.0"));
6
7if let Err(e) = rocket::custom(figment)
8    .mount("/", routes![handler])
9    .manage(api_key)
10    .launch()
11    .await
12{
13    println!("Did not run. Error: {:?}", e)
14}

(make sure you import std::fs)

It’s as simple as the call to .manage(api_key)  - now every handler we like has access to this across threads. To bring it into the handler, all we have to do is edit the function signature

async fn handler(api_key: &State<String>) -> String { ...

Make sure you import use rocket::State; . And it’s done! While we’re here, Rocket does state on a per type basis - so it can only manage one String at a time. If you needed to manage multiple strings you could either compose them into a single struct or use multiple tuple structs. By doing it on a per type basis, Rocket knows at compile time exactly which state member you want accessed in that function.

Making a Request to Naurt

Now that we have the API key in the handler we’re ready to start making web requests - we’ll use reqwest for that. Start by making a constant

const NAURT_URL: &'static str = "https://api.naurt.net/final-destination/v1";

And import some new things

use reqwest::Client;
use rocket::response::content::RawJson;

Now we can update the handler to this

1#[get("/")]
2async fn handler(api_key: &State<String>) -> RawJson<String> {
3    let client = Client::new();
4
5    let body = "{\"address_string\":\"The Grand Hotel, Brighton\"}";
6
7    let response = client
8        .post(NAURT_URL)
9        .body(body)
10        .header("Authorization", api_key.as_str())
11        .header("content-type", "application/json")
12        .send()
13        .await;
14
15    let reply = match response {
16        Ok(x) => x,
17        Err(y) => return RawJson(format!("\"Request Error\":\"{}\"", y)),
18    };
19
20    let body = reply.text().await;
21
22    return match body {
23        Ok(x) => RawJson(x),
24        Err(y) => RawJson(format!("\"Body Error\":\"{}\"", y)),
25    };
26}

We’re making a static request to Naurt, and we’re simply sending the JSON that Naurt sends forward. That’s what the RawJson<> type does - it sorts out the response and headers for you.

Strongly Typing the Request

Rust has a very powerful type system and first class serlisation and deserialisation support. We can use this to create strongly typed requests so we don’t accidentally make mistakes. The Naurt request can be made like so

1use serde::Serialize;
2
3#[derive(Serialize)]
4struct NaurtRequest {
5    #[serde(skip_serializing_if = "Option::is_none")]
6    pub address_string: Option<String>,
7    #[serde(skip_serializing_if = "Option::is_none")]
8    pub latitude: Option<f64>,
9    #[serde(skip_serializing_if = "Option::is_none")]
10    pub longitude: Option<f64>,
11    #[serde(skip_serializing_if = "Option::is_none")]
12    pub additional_matches: Option<bool>,
13}

We only need to derive Serialize since we’ll be converting this struct into a JSON. Notice how we made every field an Option<> - this is because we only need to specify either an address or location when using Naurt (or both). We also tagged each field with #[serde(skip_serializing_if = "Option::is_none")] which will mean that field won’t appear in the JSON if it is None.

To use it, we can remake the request like so

1let body = NaurtRequest {
2    address_string: Some("The Grand Hotel, Brighton".to_string()),
3    latitude: None,
4    longitude: None,
5    additional_matches: None,
6};
7
8let response = client
9    .post(NAURT_URL)
10    .json(&body)
11    .header("Authorization", api_key.as_str())
12    .send()
13    .await;

Notice how we no longer specify the header content-type and have changed from using .body() to .json() . Since the Rust ecosystem is so cohesive, reqwest comes with first class serde support out of the box! Any struct that derives Serialize can be used there.

Making the System Interactive

Right now, you need to restart the server for every different request, which is obviously less than helpful. Instead, we want to use URL arguments to be able to generate different requests. Thankfully, Rocket has one of the most robust and intuitive argument systems ever made. All we need to do is modify the handler like so

1#[get("/?<address>&<latitude>&<longitude>&<additional_matches>")]
2async fn handler(
3    api_key: &State<String>,
4    address: Option<String>,
5    latitude: Option<f64>,
6    longitude: Option<f64>,
7    additional_matches: Option<bool>,
8) -> RawJson<String> {
9    let client = Client::new();
10
11    let body = NaurtRequest {
12        address_string: address,
13        latitude: latitude,
14        longitude: longitude,
15        additional_matches: additional_matches,
16    };
17...

The beauty of this system compared with others is we don’t need to worry about whether the arguments are present or their types - Rocket will only pass a request to this function if the guards can be satisfied. Therefore, we already have and can use these as native Rust types. For example, in the Go blog we had to check if the argument was present, represent it with a pointer and handle parsing the string as a number. This is just so much better.

Strongly Typing the Response

Just passing the JSON back to the user isn’t the most useful thing ever, so we’d like to do something with it. In this case our end goal is plotting it on a map, but whatever we want to do strongly typing the response is really helpful. Thankfully, since Rust has first-class serialisation support this is super simple.

1use serde::Deserialize;
2use serde_json::Value;
3
4#[derive(Deserialize)]
5struct NaurtResponse {
6    pub best_match: DestinationResponse,
7    pub additional_matches: Option<Vec<DestinationResponse>>,
8    pub version: String,
9}
10
11#[derive(Deserialize)]
12struct DestinationResponse {
13    pub id: String,
14    pub address: String,
15    pub geojson: NaurtGeojson,
16    pub distance: Option<f64>,
17}
18
19#[derive(Deserialize)]
20struct NaurtGeojson {
21    pub features: Vec<Feature>,
22    #[serde(rename(deserialize = "type"))]
23    pub type_val: String
24}
25
26#[derive(Deserialize)]
27struct Feature {
28    pub geometry: Option<Coordinates>,
29    #[serde(rename(deserialize = "type"))]
30    pub type_val: String,
31    pub properties: Properties,
32}
33
34#[derive(Deserialize)]
35struct Coordinates {
36    pub coordinates: CoordinatesWrapper,
37    #[serde(rename(deserialize = "type"))]
38    pub type_val: String,
39}
40
41#[derive(Deserialize)]
42#[serde(untagged)]
43enum CoordinatesWrapper {
44    Number(Vec<Vec<f64>>),
45    NestedNumber(Vec<Vec<Vec<f64>>>),
46}
47
48#[derive(Deserialize)]
49struct Properties {
50    pub naurt_type: String,
51    pub contributors: Option<Vec<String>>,
52}

Notice how for all of these we only derive Deserialize since we only ever convert from a JSON to these structs. A few notes to point out here. First, the word type appears in the Naurt response, to conform to the geojson standard. However, type is a reserved keyword in Rust, so we tell serde to rename it with #[serde(rename(deserialize = "type"))]. In the CoordinatesWrapper struct we need to use #[serde(untagged)] - this tells serde that CoordinatesWrapper will not actually appear in the JSON. We’re doing this because the type of coordinates in the JSON is either a double nested array or a triple nested array. You can read more about working with enums in Rust here.

Plotting the Response on a Map

Now that we have the NaurtResponse as a struct, we can think about plotting Naurt on a map. For this, we’ll use Plotly. Rust has great Plotly support, so it’s a pretty simple task.

We want to do the following things

  • Plot the parking spots and building outlines as shapes
  • Plot the building entrances as points
  • Centre the map on the building entrance of the best match

We can do that with the following code

1use plotly::{
2    common::{Marker, Mode},
3    layout::{Center, Mapbox, MapboxStyle},
4    scatter_mapbox::Fill,
5    Layout, Plot, ScatterMapbox, Trace,
6};
7
8fn extract_naurt(response: &NaurtResponse) -> String {
9    let mut plot = Plot::new();
10    let mut layout = None;
11    let mut traces: Vec<Box<dyn Trace>> = vec![];
12
13    extract_naurt_inner(&response.best_match, &mut traces, &mut layout, true);
14
15    if let Some(additional_matches) = &response.additional_matches {
16        for dest in additional_matches {
17            extract_naurt_inner(dest, &mut traces, &mut layout, false);
18        }
19    }
20
21    plot.add_traces(traces);
22    plot.set_layout(layout.unwrap());
23
24    return plot.to_inline_html(Some("mapDiv"));
25}
26
27fn extract_naurt_inner(
28    data: &DestinationResponse,
29    traces: &mut Vec<Box<dyn Trace>>,
30    layout: &mut Option<Layout>,
31    best_match: bool,
32) {
33    for feat in &data.geojson.features {
34        let geometry = match &feat.geometry {
35            Some(x) => x,
36            None => continue,
37        };
38
39        match &geometry.coordinates {
40            CoordinatesWrapper::Number(coords) => {
41                if best_match {
42                    *layout = Some(
43                        Layout::new()
44                            .mapbox(
45                                Mapbox::new()
46                                    .style(MapboxStyle::CartoPositron)
47                                    .center(Center::new(coords[0][1], coords[0][0]))
48                                    .zoom(15),
49                            )
50                            .show_legend(false)
51                            .auto_size(true),
52                    );
53                }
54
55                let trace = ScatterMapbox::new(
56                    coords.iter().map(|z| z[1]).collect(),
57                    coords.iter().map(|z| z[0]).collect(),
58                )
59                .text(format!("{} - {}", data.address, feat.properties.naurt_type))
60                .mode(Mode::Markers)
61                .marker(Marker::new().size(13));
62
63                traces.push(trace);
64            }
65            CoordinatesWrapper::NestedNumber(coords) => {
66                for shape in coords {
67                    let trace = ScatterMapbox::new(
68                        shape.iter().map(|z| z[1]).collect(),
69                        shape.iter().map(|z| z[0]).collect(),
70                    )
71                    .text(format!("{} - {}", data.address, feat.properties.naurt_type))
72                    .mode(Mode::Lines)
73                    .fill(Fill::ToSelf);
74
75                    traces.push(trace);
76                }
77            }
78        }
79    }
80}

The strengths of the Rust typing system are on full display now. By matching on the features we can automatically know that we’re working with the right datatype for the plot (i.e. points or shapes). This works better than in the TypeScript blog where we had to string match on the naurt_type and then coerce the datatype of the array, which could easily crash in runtime.

We use the to_inline_html function from Plotly to turn this into an already rendered Plotly plot. All that we have to do now is simply serve up some HTML to contain this.

Responding with the Map

We need a template to work with for this. Start by making a folder and file

$ mkdir templates
$ touch templates/index.html.tera

The template HTML is the following

1<!DOCTYPE html>
2<html>
3
4<head>
5    <meta charset="UTF-8">
6    <title>Plotly Map</title>
7    <script src="https://cdn.plot.ly/plotly-latest.min.js"></script>
8</head>
9<style>
10    .container {
11        width: 100%;
12        height: 100vh;
13    }
14</style>
15
16<body>
17    <div class="container" id="mapDiv">{{data | safe}}</div>
18</body>
19
20</html>

Since we are going to embed HTML into the template, we need to use the safe keyword or the browser will render it as plaintext.

We need to tell Rocket about the templates. We need to import Templates

use rocket_dyn_templates::Template;

And then we can attach it in the main function

1if let Err(e) = rocket::custom(figment)
2    .attach(Template::fairing())
3    .mount("/", routes![handler])
4    .manage(api_key)
5    .launch()
6    .await
7{
8    println!("Did not run. Error: {:?}", e)
9}

Now the Tera templates are available for us to render.

Finally, to actually use all of this we need to return a map to the user!

1use rocket_dyn_templates::tera::Context;
2
3#[get("/?<address>&<latitude>&<longitude>&<additional_matches>")]
4async fn handler(
5    api_key: &State<String>,
6    address: Option<String>,
7    latitude: Option<f64>,
8    longitude: Option<f64>,
9    additional_matches: Option<bool>,
10) -> Result<Template, RawJson<String>> {
11    let client = Client::new();
12
13    let request = NaurtRequest {
14        address_string: address,
15        latitude: latitude,
16        longitude: longitude,
17        additional_matches: additional_matches,
18    };
19
20    let response = client
21        .post(NAURT_URL)
22        .json(&request)
23        .header("Authorization", api_key.as_str())
24        .send()
25        .await;
26
27    let reply = match response {
28        Ok(x) => x,
29        Err(y) => return Err(RawJson(format!("\"Request Error\":\"{}\"", y))),
30    };
31
32    let body = reply.text().await;
33
34    let json_text = match body {
35        Ok(x) => x,
36        Err(y) => return Err(RawJson(format!("\"Body Error\":\"{}\"", y))),
37    };
38
39    let naurt_response = match serde_json::from_str::<NaurtResponse>(&json_text) {
40        Ok(x) => x,
41        Err(y) => return Err(RawJson(format!("\"Json Error\":\"{}\"", y))),
42    };
43
44    let plotly = extract_naurt(&naurt_response);
45
46    let mut context = Context::new();
47    context.insert("data", &plotly);
48
49    return Ok(Template::render("index", context.into_json()));
50}

Notice how we change the response type to Result<Template, RawJson<String>> - this way we can send back a rendered Template if everything goes well or send back an Error JSON if it doesn’t. Rust has really great error handling, and usually handling errors properly is easier in Rust than not handling them. As here, great error handling more or less falls out of the code naturally.

Try going to http://localhost:8080/?additional_matches=true&address=The Grand Hotel, Brighton and you should see the following map!

And we’re done! If you want to view the full code you can go to our GitHub

Subscribe To Our Newsletter - Sleek X Webflow Template

Subscribe to our newsletter

Sign up at Naurt for product updates, and stay in the loop!

Thanks for subscribing to our newsletter
Oops! Something went wrong while submitting the form.