REST API Wrapper with Rust
Tags: rust, reqwest, api, beginners
Published: 2021-07-05 16:38:25

> TL;DR: This is a post for beginners where I show:
> - How I structured the files in the project for better documentation: **every file in the same folder**; 
> - How I coded a "query builder" to handle different calls without repeating code: **using a generic parameter**;
> - How I coded test cases within the documentation: **nothing special, just remarks on _async_ tests**;
> - How I created High Order Functions (HOF) to emulate that behaviour we have in Rust's Option and Iterator, for example: **created a struct with functions that return the struct itself**;
> - How I used _serde_ to deserialize the Json: **nothing special, just some extra remarks**;
> - How I tested it as if it were a crate without exporting it to crates.io: **just add the lib.rs in the dependencies of the new project**.

I build a wrapper for the [Magic: The Gathering API](https://docs.magicthegathering.io/) (an SDK, by their own terms). And I did so because I wanted a personal project that offered the following possibilities:
- Use the _**reqwest**_ crate;
- Code something for other coders (e.g.: allowing them to use [HOFs](https://doc.rust-lang.org/rust-by-example/fn/hof.html), such as `cards.filter().types("creature").colors("red").name("dragon")`...);
- To document it as a crate, including tests in the documentation;
- Implement Github Actions (_Edit_: I did this later, you can find it [here](https://dev.to/rogertorres/getting-started-with-github-actions-for-rust-1o6g)).

The reason why I chose the Magic: The Gathering (MTG) API (besides being a MTG nerd) is because it is a very simple API (it has only `GET` methods). 

This is not a tutorial, I will just highlight the interesting choices this endeavour led me to. Also, this is for beginners; I highly doubt I will say anything new to someone who had carefully read [The Book](https://doc.rust-lang.org/book/title-page.html) and [Rust by example](https://doc.rust-lang.org/rust-by-example/) and toyed with _async_ a little bit (although we never know).
 
> The result can be found [here](https://github.com/rogertorres/mtgsdk).

---

## The project structure

I had two choices: 
![Project structure](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/6ws392kayt5mk60xuisp.png)

I chose the left one for two reasons:
- It allows the person using the crate to type `use mtgsdk` instead of `use mtgsdk::mtgsdk`
- This way the documentation shows everything on the first page. Had I went for the option on the right, the docs would only show the module `mtgsdk`, which I found is not how the cool kids do it.

**How it is (left option):**
![Option on the left](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/mdgirxnt48mmfd9fmrpc.png)
> If you want to see for yourself, fork/download the repository and type `cargo doc --open`


**How it would be (right option):**
![Option on the right](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/c5r7jb2wrr653tm50a4z.png)

Maybe the first image and [Rust by example](https://doc.rust-lang.org/rust-by-example/mod/split.html?highlight=mod.rs#file-hierarchy) is enough to show how each way of doing this is carried out; however, for the sake of clarity, I will say this: If you want the left one, all you have to do is to declare the _mods_ in your `lib.rs`. Otherwise, you have to create a folder with your single module name, create a `mod.rs` file in it and use the `mod` and `pub mod` inside it, declaring only the folder name within `lib.rs` (in this case, `lib.rs` would only have `pub mod mtgsdk;`.

---

## Query builder
As I said, this API only has `GET` methods, and there's not much to talk about how _**[reqwest](https://docs.rs/reqwest/0.11.4/reqwest/fn.get.html)**_ handles it, for it is pretty much just passing a URL as you would do in a `curl`.

> I am not saying that this is all that _**reqwest**_ does; it is not. I am saying that for this API we don't actually need anything else that accessing the URL and parsing the Json (more on this later).

However, instead of repeating the `reqwest::get(url)` inside every module, I created a _query builder_ that receives an `url` and returns a `Result<T, StatusCode>` where T is a struct containing the data for the various calls (cards, formats, etc.). 

Besides allowing me to maintain the usage of _**reqwest**_ in a single spot, it also allowed me to handle the errors and just send `StatusCode`, so the developer using this crate would easily handle the errors. Here is the code with some additional comments.

```rust
async fn build<T>(url: String) -> Result<T, StatusCode>
where
    //This is a requirement for Serde; I will talk about it below.
    T: DeserializeOwned,
{
    let response = reqwest::get(url).await;

    // I am using match instead of "if let" or "?" 
    // to make what's happening here crystal clear
    match &response {
        Ok(r) => {
            if r.status() != StatusCode::OK {
                return Err(r.status());
            }
        }
        Err(e) => {
            if e.is_status() {
                return Err(e.status().unwrap());
            } else {
                return Err(StatusCode::BAD_REQUEST);
            }
        }
    }

    // This is where de magic (and most problems) occur.
    // Again, more on this later.
    let content = response.unwrap().json::<T>().await;

    match content {
        Ok(s) => Ok(s),
        Err(e) => {
            println!("{:?}", e);
            Err(StatusCode::BAD_REQUEST)
        }
    }
}
```
Pretty straightforward: the functions calling the `build()` function will tell which type `T` corresponds to, a type that will be a struct with the `Deserialize` trait so that _**reqwest's**_ `json()` can do the heavy lifting for us.

---

## Documentation tests
The [documentation section](https://doc.rust-lang.org/book/ch14-02-publishing-to-crates-io.html?highlight=docu#making-useful-documentation-comments) in the Rust Book is pretty good. Besides reading that, I only checked some examples of how documentation is managed in the crates I use. 

What I want to highlight is the insertion of tests within the docs:
![Documentation test](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/5rmp0ezau9lipcr6k7jt.png)

That this test will be executed is something that The Book talks about, so I will not stress about it. What was specific for me is that I was testing `async` calls, which required two minor tweaks:
- It has to be in an async block;
- I could not return anything (so no `await?`, [because it returns the error](https://doc.rust-lang.org/edition-guide/rust-2018/error-handling-and-panics/the-question-mark-operator-for-easier-error-handling.html)).

## High Order Functions
I will not lecture about [HOF](https://doc.rust-lang.org/rust-by-example/fn/hof.html), let alone explain anything about functional programming. The reason I ended up with this is because, instead of something like this Builder Pattern (this is from another wrapper for the same API)...
```rust
let mut get_cards_request = api.cards().all_filtered(
    CardFilter::builder()
        .game_format(GameFormat::Standard)
        .cardtypes_or(&[CardType::Instant, CardType::Sorcery])
        .converted_mana_cost(2)
        .rarities(&[CardRarity::Rare, CardRarity::MythicRare])
        .build(),
    );
    
let mut cards: Vec<CardDetail> = Vec::new();
loop {
    let response = get_cards_request.next_page().await?
    let cards = response.content;
    if cards.is_empty() {
        break;
    }
    filtered_cards.extend(cards);
}
println!("Filtered Cards: {:?}", filtered_cards);
```
...I wanted something like this:
```rust
let response = cards::filter()
    .game_format("standard")
    .type_field("instant|sorcery")
    .cmc(2)
    .rarity("rare|mythic")
    .all()
    .await;

println!("Filtered cards: {:?}", response.unwrap());
```

Why? Because as a developer I love how `Option` and `Iterator`, as well as crates such as `warp`, implement this, giving Rust its "[functional flavour](https://doc.rust-lang.org/rust-by-example/fn/hof.html)".

### How to do it
The function `filter()` returns a struct called `Where` that has a vector where I keep all the filters that are going to be added.
```rust
pub struct Where<'a> {
    query: Vec<(&'a str, String)>,
}

pub fn filter<'a>() -> Where<'a> {
    Where { query: Vec::new() }
}
```

So, when I do something like `response = mtgsdk::card::filter()`, the variable `response` is a Where `struct`, and that allows me to call any function implemented inside `Where`, e.g.:
```rust
impl<'a> Where<'a> {
    pub fn game_format(mut self, input: &'a str) -> Self {
        self.query.push(("gameFormat", String::from(input)));
        self
    }
}
```

So basically, when I called `filter()` and then added the functions `game_format()`, `type_field()`, `cmc()` and `rarity()` I was doing this:
- Created a `Where` struct with `filter()`
- Called `game_format()` implemented inside `Where`, which returned the same `Where`
- Called `type_field()` from the `Where` returned by `game_format()`
- Called `cmc()` from the `Where` returned by `type_field()`
- Called `rarity()` from the `Where` returned by `cmc()`
- Called `all()` from the `Where` returned by `rarity()` which **finally** returned the vector of cards:

```rust
pub async fn all(mut self) -> Result<Vec<Card>, StatusCode> {
    let val = self.query.remove(0);
    let mut filter = format!("?{}={}", val.0, val.1);

    for (k, v) in self.query.into_iter() {
        filter = format!("{}&{}={}", filter, k, v);
    }

    let cards: Result<RootAll, StatusCode> = query_builder::filter("cards", &filter).await;

    match cards {
        Ok(t) => Ok(t.cards),
        Err(e) => Err(e),
    }
}
```
And that's it.

---

## Deserialize Json
As promised. 
```rust
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Set {
    pub code: String,
    pub name: String,
    #[serde(rename = "type")]
    pub type_field: String,
    #[serde(default)]
    pub booster: Vec<Booster>,
    pub release_date: String,
    pub block: Option<String>,
    pub online_only: Option<bool>,
    pub gatherer_code: Option<String>,
    pub old_code: Option<String>,
    pub magic_cards_info_code: Option<String>,
    pub border: Option<String>,
    pub expansion: Option<String>,
    pub mkm_name: Option<String>,
    pub mkm_id: Option<u32>,
}
```

Few things I want to mention:
1. The [transform](https://transform.tools/json-to-rust-serde) tool helps quite a bit;
2. In case `#[serde(rename_all = "camelCase")]` is not sufficiently self-explanatory, it will allow a struct field like `release_date` to receive data from a field that the API calls `releaseDate`;
3. There are two ways to handle optional fields:
   - Using [Option](https://doc.rust-lang.org/core/option/enum.Option.html), which I preferred because in this case the developer using the crate will have absolute certainty if the field wasn't (the Option will be `None`) sent or if it was just empty (it will me `Some`)
   - Using `#[serde(default)]`, which I used for the mandatory fields because in these cases there's no doubt that the API sent them.

---

## Testing "as if" it was a crate
I wanted to import it in a new project, but I didn't wanted to send it to crates.io. How to do it? Like this:

In the new project's `Cargo.toml` I added this:
```rust
[dependencies]
mtgsdk = { path = "../mtgsdk" }
tokio = { version = "1", features = ["full"] }
```

And that's all. In my `main.rs` I just used it as if it was a crate.
```rust
use mtgsdk::cards;

#[tokio::main]
async fn main() {
    let result = cards::find(46012).await;

    if let Ok(card) = result{
        println!("{}", card.name)
    };
}
```

This might be helpful if you want to be truthful to what The Book calls [integration testing](https://doc.rust-lang.org/rust-by-example/testing/integration_testing.html).

---

See ya 🙃

_Cover image by [Wayne Low](https://unsplash.com/photos/OvN4OkhkTLo)_