Writing a Rust CLI to complete the DigitalOcean Functions Challenge
Tags: rust, digitalocean, serverless, api
Published: 2022-06-15 04:24:44

---
title: Writing a Rust CLI to complete the DigitalOcean Functions Challenge
published: true
description: A quick write-up on writing a Rust CLI to complete the DigitalOcean Functions Challenge
tags: rust, digitalocean, serverless, api
series: DigitalOcean Functions
# cover_image: https://direct_url_to_image.jpg
# Use a ratio of 100:42 for best results.
---

# Prelude

To continue from my [Go CLI write-up](https://dev.to/j4ng5y/writing-a-go-cli-to-complete-the-digital-ocean-functions-challenge-3372) and my [Python CLI Write-up](https://dev.to/j4ng5y/writing-a-python-cli-to-complete-the-digitalocean-functions-challenge-1e29), I will go in to detail about writing a Rust CLI to perform the same [DigitalOcean Functions Challenge](https://functionschallenge.digitalocean.com)

## The Code

I'm sensing a theme here, but much like my last two write-ups, I simply threw everything into the `main.rs` file that was created with the `cargo new sharks` command.

First things first, if you have done any development with Rust that accesses the web via JSON, there are a few essential crates to grab:

1. [Reqwest](https://docs.rs/reqwest/0.9.18/reqwest/) is a crate that makes sending web requests simple, and even though I used the blocking client, you can use [Tokio](https://docs.rs/tokio/1.19.2/tokio/) to [make Reqwest asynchronous](https://docs.rs/reqwest/0.9.18/reqwest/async/index.html) just as simply.

2. [Serde](https://docs.rs/serde/1.0.137/serde/) / [serde_json](https://docs.rs/serde_json/1.0.81/serde_json/)  / [serde_derive](https://docs.rs/serde_derive/1.0.137/serde_derive/) are obvious choices for serialization/deserialization.

For the CLI, I personally like to use [Structopt](https://docs.rs/structopt/0.3.26/structopt/), but feel free to use whatever you are comfortable with.

My `Cargo.toml` dependencies look like so:

```toml
[dependencies]
structopt = { version = "0.3" }
reqwest = { version = "0.11", features = ["blocking", "json"]}
serde = { version = "1.0" }
serde_json = { version = "1.0" }
serde_derive = { version = "1.0" }
```

Now, knowing that I want something similar to my previous two write-ups, I started with the API URL constant and some Request and Response structs:

```rust
// src/main.rs

use std::collections::HashMap;
use serde_derive::{Serialize,Deserialize};

const API_URL: &str = "https://functionschallenge.digitalocean.com/api/sammy";

// Obviously, the serialize trait needs to be here so that
// we can turn this into JSON later. I usually use debug here
// as well just in case I need to do a little println!
// debugging.
#[derive(Debug, Serialize)]
struct Request {
    name: String,

    // Because of the type keyword, we needed to name this
    // field something other than type, so _type suits me
    // just fine, but we have to tell serde that we want to
    // rename this field when we go to actually serialize
    // the JSON.
    #[serde(rename(serialize = "type"))]
    _type: String,
}

// Again, and obvious deserialize trait needed with the
// response. And again, the almost obligatory Debug trait.
#[derive(Debug, Deserialize)]
struct Response {
    message: String,
    
    // In this case, we need to tell serde to give us the
    // "zero" value of a HashMap<String,Vec<String>> because
    // a successful response will not have this field, and
    // will cause the app to panic because this field doesn't 
    // get used.
    #[serde(default)]
    errors: HashMap<String, Vec<String>>
}
```

And for the request/response, this is really all we need to do.

Now, I'll set up the CLI struct really fast:

```rust
// src/main.rs
...

use structopt::StructOpt;

...

// I went ahead and created a "possible values" constant here
// so that we have the CLI filter our sammy type input for us.

const TYPE_VALUES: &[&str] = &[
    "sammy",
    "punk",
    "dinosaur",
    "retro",
    "pizza",
    "robot",
    "pony",
    "bootcamp",
    "xray"
];

...

// In classic fashion, we have to derive the structopt trait
// as well as the obligatory debug trait. The additional
// structopt config follows it.
#[derive(Debug, StructOpt)]
#[structopt(name = "sharks", version = "0.1.0")]
struct Opt {
    #[structopt(long = "name")]
    sammy_name: String,

    #[structopt(long = "type", possible_values(TYPE_VAULES))]
    sammy_type: String,
}
```

Now all we have to do is pull it all together in the `main()` function:

```rust
// src/main.rs

...

fn main() {
    // Build the opt struct from the arguments provided
    let opt: Opt = Opt::from_args();

    // Build a new request from those arguments
    let r: Request = Request {
        // notice the clones here, I needed that values later
        // in the output, so this was just the most painless
        // way to not fight the borrow checker :D
        name: opt.sammy_name.clone(),
        _type: opt.sammy_type.clone(),
    };

    // Grab a new blocking client. I don't care if this
    // blocks because of what this app is and does, but
    // you may care more. Feel free to try this async.
    let c = reqwest::blocking::Client::new();

    // Build the JSON from our Request struct
    let resp_body = serde_json::to_string(&r).unwrap();

    // Send the request
    let resp: Response = c.post(API_URL)
        .header("ACCEPT", "application/json") // Set a header
        .header("CONTENT-TYPE", "application/json") // Set a header
        .body(resp_body) // Set the body
        .send() // Send it
        .expect("failed to get response") // Don't fail
        .json() // Convert response to JSON
        .expect("failed to get payload"); // Don't fail

    // Here, I'm just doing a little error checking to see if
    // something exists. There are obviously far nicer ways
    // to do this, but again, this was fast and accomplished
    // the goal.
    if resp.errors.len() > 0 {
        println!("ERROR: {:#?}", resp.errors);
        return
    }
  
    // Give us a success message if it worked!
    println!("Successfully created Sammy: {} of type {}", opt.sammy_name, opt.sammy_type)
}
```

With all this written down, now we can just do a `cargo run -- --name <your sammy name> --type <your sammy type>` and it works as expected.

## Conclusion

Like usual, if you want to see the full file, feel free to check it out on the repository here:
https://github.com/j4ng5y/digitalocean-functions-challenge/tree/main/rust