June 1, 2019

My first Rust project

At work, I keep a todo list vaguely resembling a bullet-journal.

+--------------+
| * Task       |
| x Completed  |
| > Migrated   |
| - Cancelled  |
+--------------+


## 2019-05-27
x Version-pin deploy tooling
x Build auth package
* Replace Marco's deploy keys in the CI

## 2019-05-28
x Replace Marco's deploy keys in the CI
* Write new ticket: failed logins on STG
* Write new ticket: Create users for Kubectl
* Investigate bug #123

## 2019-05-29
x Write new ticket: Create users for Kubectl
> Write new ticket: failed logins on STG

Every working day, I open the file with my favourite editor, I add the date, and I report the unfinished items that I intend to work on.

## 2019-05-31
> Investigate bug #123
> Write new ticket: failed logins on STG

So I wrote a Rust program that does that for me.

use std::collections::HashSet;
use std::io::{self, BufRead};
use chrono::{NaiveDate, Local};


fn main() {

    // The last inserted date will be stored here in its string form.
    let mut last_date = String::from("");

    // This set will contain the todo items.
    let mut items: HashSet<String> = HashSet::new();

    // For every line passed in stdin...
    for line in io::stdin().lock().lines() {
        // ...assume that parsing to string is successful...
        let l = line.unwrap();
        // ...print it to stdout...
        println!("{}", l);

        // ...then create a char iterator over it.
        let mut chars = l.chars();

        // Depending on the first char of the line:
        match chars.next() {

            // collect the date and save it for later
            // (this string will be overwritten if there is another one next)
            Some('#') => {
                last_date = chars.skip(2).take(10).collect();
            },

            // or add a new entry to the set
            Some('*') | Some('>') => {
                items.insert(chars.skip(1).collect());
            },

            // or remove the entry if it is complete
            Some('x') | Some('-') => {
                let item: String = chars.skip(1).collect();
                items.remove(&item);
            },

            // In the case of an empty line, or if the line starts with an unknown char, nothing is
            // done.
            _ => (),
        }
    }

    // Compare the last date in the file...
    let d = NaiveDate::parse_from_str(&last_date, "%Y-%m-%d").expect("Not a date");

    // ...with the date of today.
    let today = Local::today().naive_local();

    // If they don't match:
    if d != today {

        // write today's date
        println!("\n## {}", today.format("%Y-%m-%d").to_string());

        // and report all unfinished items.
        for item in items {
            println!("> {}", item);
        }
    }
}

While parsing the file, Todo items must be stored in some collection for later use. What data type does it make sense to use? Since items do not have a unique ID here, the text itself must be used to retrieve the items. Rust’s standard library has this elegant HashSet, which is technically a HashMap with empty values. Exactly what is needed for our little content-addressable storage.

This little exercise let me try out Rust’s powerful match. I used it to define the behaviour of the parser, based on the first char of the current line.

For every line of text, an iterator is created. It is used in the match value for comparing the first char, and in the match arms for extracting the rest of the string. No waste!

I have used the beautiful Chrono library (ehm, crate) for dealing with dates.

Next steps

Currently, the program only uses stdin and stdout. It would be interesting to let the caller pass different streams at runtime:

$ newday -f ~/todo.txt # Appends to the file passed with the `-f` argument

Generalising the operations on the streams (and getting rid of println) will probably teach me something about Rust’s Traits.

Next, I’d like to understand how to run tests and do proper TDD with Rust.