DEV Community

Martin Becker
Martin Becker

Posted on

Building My first Rust Project: Wercker Build Status

Introduction

warning This is my first ever techy blog post so I hope you'll forgive any errors or badly written sections.

My main language of choice is ruby, I like it's ease of use, it's simplicity and expressiveness and it's malability makes it a dream to use, It is however Slow, lacks clear typing and even subtle bugs can crop up if you aren't aware of the fact that ruby is both pass by value and reference as you can see here:

a = 'hello '
b = a
b << 'world'
puts a
=> 'hello world'

Enter fullscreen mode Exit fullscreen mode

Whilst all of it's pro's make rapidly building something easy it can make it difficult to maintain over long term.

Enter Rust!

As far as I'm concerned Rust is the perfect partner to ruby, it's fast where ruby is slow, it's typed where ruby isn't and you never need to worry about undefined behavior or subtle bugs due to its nature.

To that end I decided on learning rust to act as a foil to ruby. Learning rust has been hard but worth it I feel.

The tool - Werker Build Status

This tool exists because at my job we use Oracle's Wercker (I keep calling it wrecker) to handle Continues Intergration but I have a tendency not to pay attention to when a build is failing so I thought I'd build a tool to display the status of my last build in the Tmux status line.

Before I started working on this tool I'd always felt lost when working with the language, I can safely say that this is the first time I'd worked with rust and felt like I knew what I was doing (more or less).

Originally I was going to build a bash script around the Wercker CLI tool, but then it occurred to me that it's not a large project and its scope is small, wouldn't that be perfect for my first project with Rust? So I did, and I started with planning.

Planning

I realised that my tool could be broken down into steps that I could then use to work out what crates I might use to make to make it easier (something I admit I don't normally do).

1) Loading user config - config
2) Getting the list of runs - curl
3) Deserializing the json - serde_json

I admit that normally I'd just jump into ruby and start hammering code out however due to the nature of rust and the fact I don't know it that well I thought it would be a good idea to plan it out and it ended up working in my favour.

Loading the User Config

I honestly found this to be the easiest of all the steps as the documentation was easy to follow, for me I simply added a single function to take care of loading the config:

use config_rs;

use std::collections::HashMap;
type Config = HashMap<String, String>;

fn load(config_file: String) -> Config {
    // create config object
    let mut settings = config_rs::Config::default();
    // check if config_file was supplied, if it was then load it    
    if config_file != "" {
        settings.merge(config_rs::File::with_name(config_file.as_str())).unwrap();
    }
    //check for and add env variables
    settings.merge(config_rs::Environment::with_prefix(::CONFIG_PREFIX)).unwrap();
    settings.try_into::<Config>().unwrap()
}

Enter fullscreen mode Exit fullscreen mode

Initially I was checking for each key I needed with an if statement to check if a key existed but in the end I found that this to be the dry-ist way to do it but still remaining flexible:


// check if config options exists
let keys: [&str;3] = ["token", "author", "pipeline_id"];

match keys.iter().position( |key| {
    !settings.contains_key(&key.to_string())
}) {
    Some(i) => println!( "No `{}` detected in config or env ({}_{}) variables",
            keys[i], CONFIG_PREFIX, keys[i].to_uppercase()),
    None    => {
}
Enter fullscreen mode Exit fullscreen mode

Getting the list of runs

This was were things started getting tricky, Setting up the Curl client was fairly straightforward:

use curl::easy::Easy;

mod urls;

pub fn set_up(token: &String) -> Easy {

    use curl::easy::List;

    let mut easy = Easy::new();
    let mut list = List::new();

    // add authorisation header to curl client
    list.append(&format!("Authorization: Bearer {}",token)).unwrap();
    easy.http_headers(list).unwrap();
    easy
}

Enter fullscreen mode Exit fullscreen mode

I created a function to handle doing get requests:

fn get(curl: &mut Easy, url: String) -> String {
    use std::str;

    let mut data = Vec::new();

    curl.url(url.as_str()).unwrap();
    // add mechanism to transfer data recived data into a vec
    {
        let mut transfer = curl.transfer();
        transfer.write_function(|new_data| {
            data.extend_from_slice(new_data);
            Ok(new_data.len())
        }).unwrap();
        transfer.perform().unwrap();
    }
    // Return Data!
    data
}

Enter fullscreen mode Exit fullscreen mode

However this is where I encountered my first hiccup, where I was expecting a JSON string I was getting a Vec[u8] it puzzled me for a bit but then I realised that curl was outputting raw bytes, a bit of digging and the answer was to change the last line to this:

// convert byte result into utf_8 string
str::from_utf8(&data).unwrap().to_string()
Enter fullscreen mode Exit fullscreen mode

Now I was getting a JSON string!

Deserializing the json

At first I was content to just filter the build runs by author and then grab the first deserialized object:

let mut client = set_up_client(&settings["token"]);
let runs: Value = serde_json::from_str(get_runs(&mut client,&settings["author"],
       &settings["pipeline_id"]).as_str()).unwrap();
println!("{}",runs[FIRST]);
Enter fullscreen mode Exit fullscreen mode

However due to what appears to be a bug on the Wercker API this wasn't possible. the bug was that no matter what I set as the author (a username, random letters) it would always send back the last builds of the same incorrect user.

This means means I have to instead grab the last 20 runs and manually find the first with a matching username, to do that I had to create structs to represent the data I wanted:

pub type Runs = Vec<Run>;

// only define the structs for the data I want to pull out
#[derive(Debug,Serialize, Deserialize)]
pub struct Run {
    pub status: String,
    pub result: String,
    pub user: User,
}

#[derive(Debug,Serialize, Deserialize)]
pub struct User {
    pub meta: Meta,
}

#[derive(Debug,Serialize, Deserialize)]
pub struct Meta {
    pub username: String
}
Enter fullscreen mode Exit fullscreen mode

and then deserialize to my struct:

let runs: Runs = serde_json::from_str(client::get_runs(&mut client,&settings["pipeline_id"]).as_str()).unwrap();
Enter fullscreen mode Exit fullscreen mode

I honestly didn't want to have to do this as the basic deserialisation was fine for my needs as I wasn't intending to store the results however Serde wouldn't co-operate quite so nicely, I tried doing this:

runs.as_array().iter().find()
Enter fullscreen mode Exit fullscreen mode

However I found that serde only returns a Vec[String] with a single element containing the entire JSON string, so manually creating the deserialized data was the only way.

Another issue I found was that following the serde documentation gave me a puzzling result:

let runs: Value = serde_json::from_str(client::get_runs(&mut client,&settings["pipeline_id"]).as_str())?;
Enter fullscreen mode Exit fullscreen mode

It didn't like me using ? operator, it turns out I'd completely missed the fact you can't use it nor try! in main because they both return and you can't return in main, changing ? to unwrap() solved the issue.

Displaying the result

Lastly I iterate through the Vec[Runs] and display the first to match the username:

match runs.iter().find( |run| {
    &run.user.meta.username.to_lowercase() == &settings["author"].to_lowercase()
}) {
    // print out status and result
    Some(run) => {
        if settings.contains_key("tmux") && &settings["tmux"] == &"true".to_string() {

            match run.result.as_ref() {
                "failed"    => println!("##[fg=red,bold]{}",&run.result),
                "passed"    => println!("#[fg=green,bold,bright]{}",&run.result),
                "aborted"    => println!("#[fg=yellow,bold,bright]{}",&run.result),
                _           => println!("#[fg=blue,bright]{}",&run.status)
            }
        } else {
            match run.result.as_ref() {
                "failed"    => println!("{}",&run.result.red()),
                "passed"    =>  println!("{}",&run.result.green()),
                "aborted"    =>  println!("{}",&run.result.yellow()),
                _           =>  println!("{}",&run.status.blue(),)
            }
        }
    },
    None => print!("{}","None Found")

}
Enter fullscreen mode Exit fullscreen mode

conclusion

Learning Rust was worth it and this tool I built though basic shows me that I can build things with this language. A someone that has a tendency to forget things and not notice when things are slightly off, rust helps a lot in this regard as it just won't compile when things aren't right so it's nice that if it compile it will pretty much work (barring any bugs of course).

There are a few things I did not cover in this as the point isn't to show the program in its entirety, if you want to see it all you can look at the repo the tool is stored at which can be found here.

I'm honestly glad I took the time to learn rust and I feel it will be of help in my future endeavours.

Thank you for taking the time to read this retrospective I hope it was in any way informative to you.

Top comments (0)