Commit message generator with serde and reqwest

Let's start a new tool that outputs helpful commit messages. We can use it in case we can't come up with one ourselves, but it's Friday afternoon and we want to get hell out of the office.

Create a Cargo project

Let's start a new project called "commit-msg-gen": cargo new --bin commit-msg-gen.

You'll find a Cargo.toml file that contains:

[package]
name = "commit-msg-gen"
version = "0.1.0"
authors = ["Your Name <your@email.address>"]
edition = "2018"

[dependencies]

Add quicli to your dependencies, as well as structopt:

quicli = "0.4"
structopt = "0.2"

We'll get out commit messages from the glorious website called whatthecommit.com, so we'll need a crate that let's us do HTTP ws. Let's use reqwest:

reqwest = "0.9"

The response will be in JSON, so let's add serde that allows us to easily deal with serialized data:

serde = "1"

Import quicli and reqwest

To get this party started, import our new friends:

use quicli::prelude::*;
use structopt::StructOpt;

Write a CLI struct

So, what should our command line interface look like? Above, we talked about generating a commit message. How about we go one step further and offer to generate multiple commit messages?

Sounds great? Let's do this!

/// Get some cool commit messages!
#[derive(Debug, StructOpt)]
struct Cli {
    /// How many?
    #[structopt(long = "amount", default_value = "3")]
    amount: i32,
    #[structopt(flatten)]
    verbosity: Verbosity,
}

Implement all the features

Alright, let's get down to business. If we open https://whatthecommit.com/index.json in a browser, we see it is structured like this:

{
  "permalink": "http://whatthecommit.com/f71fdcde399d6e26db9d66c6166c9b99",
  "commit_message": "Is there an achievement for this?",
  "hash": "f71fdcde399d6e26db9d66c6166c9b99"
}

So we need to do two things:

  1. Load that page and get its content somehow.
  2. Parse the JSON data and get the commit message out of it.

For the first, we've already imported reqwest, so it's only a function call away (nice!):

reqwest::get("https://whatthecommit.com/index.json")?

To parse data in a format like JSON, there is serde. Remeber that we added that as dependency earlier, too? As it turns out, reqwest has a handy .json() method available on its HTTP response type, that uses serde under the hood. So, we can do:

reqwest::get("https://whatthecommit.com/index.json")?.json()?;

What does this return? Anything you like! Well, anything you like and that serde can produce from JSON. With a small type annotation you can tell the compiler what type you expect, and serde will try to generate it from the JSON input.

If you've used dynamically typed languages like JavaScript before, you might expect to get a general object that contains JSON-like data. This is also possible in Rust: The serde_json crate has a type called Value, that maps directly to what JSON looks like. (So, a number, a string, a boolean, an array, a map, or nested versions of these.) But there is another option, and it is suprisingly easy to use but gives us a lot of benefits. We can define our own type that describes the JSON structure:

#[derive(Deserialize)]
struct Commit {
    commit_message: String,
}

The interesting thing here is the #[derive(Deserialize)] annotation. It generates an implementation of a special trait so serde can parse data and turn it into Commits. In the above definition, we only wrote one of the three fields. That's okay, the other fields will be ignored. We don't have to care about those right now.

Nice! Now, with that out of the way, let's write the typical main function, but this time with a loop in which we request some wonderful commits, and print them. Oh, and because it might take a while for the commits to arrive, we also write some log output. (Use -vv to see it!)

fn main() -> CliResult {
    let args = Cli::from_args();
    args.verbosity.setup_env_logger("commit-msg-gen")?;

    for i in 0..args.amount {
        info!("try {}", i);
        let c: Commit = reqwest::get("https://whatthecommit.com/index.json")?.json()?;
        println!("{}) {}", i + 1, c.commit_message);
    }

    Ok(())
}

Give it a spin!

All set? cargo run it!