Rocket and Rust Part III (and a canned cocktail star)

Alright, so far I have talked a lot about wanting to learn Rust and wanting to implement a web application using the Rocket framework. Alright... but what am I actually building? Well... In case my blog, my domain, my profile photo, or any of the other many clues was not enough, I will state unequivocally I am a lover of beer, wine, and other alcoholic beverages. I consider myself "an equal opportunity alcohol consumer". Recently, I started taking stock (literally) of all the bottles around my home, and recording what they were and where they were. To start, I was creating my records in a spreadsheet. But why not build myself an application to do it instead! Thus, the idea for "Ethel" was born.

<Small Side Tangent about Alcohol>

Ethanol, or ethyl alcohol, is the "drinking alcohol" we all know and love. When you see an alcohol by volume (ABV) on an adult beverage, ethanol is what is being measured. You would NOT want to drink straight ethanol, however, as this can cause coma or death.

The chemical formula for ethanol is C2H6O. It can also be abbreviated as EtOH and is also referred to as ethyl alcohol, grain alcohol, drinking alcohol, or simply alcohol. When humans (and other mammals) drink alcohol, our bodies are changing the ethanol into acetaldehyde (ethanal). This chemical is actually what causes the effect of intoxication, not the ethanol itself, and is responsible for the long-term health impacts seen in regular alcohol consumption. Eventually, acetaldehyde is further converted to acetate and then broken down to water and carbon dioxide (which we then excrete in one way or another).

Alright alright, enough alcohol nerdiness, although someday I am going to talk in detail about the different kinds of alcohol you get when distilling and the process of breaking up the heads, hearts, and tails...

TLDR: the name Ethel comes from Ethanol. It's common German name meaning "noble" and I think I am clever. Lol.

</Small Side Tangent about Alcohol>

Alright, so. I decided the first step I wanted to take was representing (if somewhat naively to start) the data I already had recorded in my spreadsheet. I created a smaller public snapshot of some of the data to share with you all in Google Sheets, but you can also just take a look at the screenshots if you prefer:

Some sample data from liquor tracker sheet.

Alright, so, let's start with a Bottle struct. My initial naive interpretation of my struct looked like this:

struct Bottle {
    id: u16,
    name: String,
    category: String,
    sub_category: [SubCategory] 
}
Initial Bottle struct.

I included an id because I really want to store this in a database at some point. I decided on u16 (a 16-bit unsigned integer), as I plan on do a super simple auto incrementing id. If I were building a "real" production system, I would not choose a) such a small field for the id (max 65,535) or b) an auto incrementing id approach. For our simple app, though, I find it more than sufficient. And let's be real... if I get to the max value of u16 for bottles I probably have other issues... My point is, make sure to choose your id's sensibly for your application! There are tons of articles out there about different id generating styles and functions. Some of them are really really neat when factoring in distributed systems... I'm looking at you, snowflake ids.

Now, once I wrote my Bottle struct, I realized I had some problems:

  1. Location is missing
  2. Category is a string
  3. Sub Category is also a string

In all three cases, I decided to take the unique data from my dataset and create enums representing each of these sets of data. Why enums? Well, I could have done individual structs and stored these items as their own data. I may even do this in the future, but since I am the only consumer of this project (and I don't expect to be building any new rooms or buying new furniture regularly) I decided the enum representations would suffice for my simple application for now. Onward to the enums!

enum Room {
    DiningRoom,
    Entry,
    LivingRoom,
    Kitchen,
    Garage,
}

enum Storage {
    BeerFridge,
    Buffet,
    BuiltIn,
    Cabinet,
    Counter,
    Fridge,
    IkeaShelf,
    LeftIkea,
    RightIkea,
    WineFridge,
}

enum Shelf {
    LeftTop,
    LeftBottom,
    CenterTop,
    CenterBottom,
    RightTop,
    RightBottom,
    Top,
    LeftMiddle,
    RightMiddle,
    RightCenter,
    Shelf1,
    Shelf2,
    Shelf3,
    Shelf4,
    Shelf5,
    CenterMiddle,
    BarArea,
    Fridge,
    Shelf,
    LeftKeg,
    RightKeg,
    Small,
    Large,
}

enum Category {
    Whiskey,
    Brandy,
    Vodka,
    Liqueurs,
    Gin,
    Rum,
    Agave,
    Other,
    Beer,
    Wine,
    Sochu,
}

enum SubCategory {
    American,
    SingleMalt,
    Scotch,
    Blend,
    Bitter,
    Sweet,
    Fruit,
    Dairy,
    Cognac,
    Armagnac,
    Calvados,
    CaskStrength,
    NavyStrength,
    Rye,
    Bourbon,
    Aged,
    Silver,
    Dark,
    Flavored,
    Gold,
    Spiced,
    Mezcal,
    Tequila,
    Other,
    Peated,
    Palinka,
    Añejo,
    Reposado,
    Blanco,
    Blackstrap,
    Irish,
    Poitín,
    Japanese,
    NewZealand,
    Australia,
    Wheat,
    Korean,
}
Enums for liquor dataset.

Okay, so yeah... I have a lot of sub categories and shelves... Heh. I could see down the road refining the sub categories to be a function of the categories: as in represent the relationship between Whiskey -> Scotch. The way I am using sub category, however, is complicated due to having generic data such as "Sweet" or "Japanese" which can apply to multiple categories. So, for now, I am going to ignore it and just allow serialization of the current unique values from my data.

Alright, now, how do I represent the location data, which is currently missing from my Bottle struct? With another struct, of course:

struct Location {
    room: Room,
    storage: Storage,
    shelf: Shelf
}
Location struct.

The Location struct is "smarter" compared to our initial Bottle struct, as it uses the enum types to represent the data we expect in the location. Now the room must match out Room enum and so on. After resolving some of the original concerns about our Bottle struct, we can update our code to use the new representations we have created:

struct Bottle {
    id: u16,
    name: String,
    category: Category,
    sub_category: [SubCategory],
    location: Location
}
Updated Bottle struct using new enums and location struct.

Awesome! Now we have a complete representation of our current data structure in our sheet. I took some recommendations from an article I referenced previously about building a web app with Rocket and decidied to use the serde crate for serialization. Check out their documentation for more details on the powerful framework they offer for serializing and deserializing data structures in Rust!

Like the "Creating a Rust Web App" article, I wanted to implement a JSON api returning a random bottle, similar to the author's example of a random blog post. Once I installed the serde crate, I was able to use the derive keyword to decorate my struct for serialization and deserialization. I should also point out, while Rocket does support JSON "out of the box" it is a feature we need to turn on. To do so, I updated my Cargo.toml to include the feature I wanted. The serde crate is also referenced in my dependencies now:

[dependencies]
rocket = { version = "=0.5.0-rc.3", features = ["json"] }
serde = "1.0.167"
Excerpt from Cargo.toml showing dependencies on rocket and serde.

The other option to enable the feature is to install the rocket crate from the beginning with the feature by running cargo add rocket@0.5.0-rc.3 --features=json. Either way yields the same result.

Alright, so now we can use JSON in our Rocket routes! Let's go ahead and write a get random route and just return the JSON of an inline created bottle for now. This will validate we can serialize our data correctly and return a JSON object representing a bottle from the endpoint.

#[get("/random")]
fn get_random_bottle() -> Json<Bottle>  {
    Json(
        Bottle {
            id: 1,
            name: "Faretti Biscotti Famosi".to_string(),
            category: Category::Liqueurs,
            sub_category: [SubCategory::Sweet],
            location: Location {
                room: Room::LivingRoom,
                storage: Storage::LeftIkea,
                shelf: Shelf::Shelf5
            }
        }
    )
}
Excerpt from Cargo.toml showing dependencies on rocket and serde.

Alright, let's run it... actually, I am getting really sick of running things manually. There is a lovely little crate we can install called cargo-watch. We can add it by running cargo install cargo-watch. Then, we can run cargo watch -x run. Now, whenever we change our files in our cargo project, cargo watch will run the application again! No more having to exit and rerun our application, it will happen automatically now. Magic!

Okay, back to what we were doing: when we try and run things we run into a few issues. First, we need to make sure we imported our Json function from the rocket::serde module. Second, we forgot to tell our application we wanted to derive Serialization and Deserialization on our struct! We will need to import those from the module as well. Adding the following line to your source file will import all the necessary functions for our example:

use rocket::serde::{Serialize, Deserialize, json::Json};

Alright, great. Now we need to decorate our Bottle struct in order to apply the traits from serde's Serialize and Deserialize to our data structure. We do this by adding #[derive(Serialize, Deserialize)] above our struct definition. Okay, let's save and run again...

We immediately run into several errors. First, some of you rustier folks may have noticed my mistake in my Bottle struct when I declared the desire to have multiple sub categories. I told my application I wanted [String]. This would only work if I knew the size of the array everytime, because arrays go on the stack and must be of a known size at compile time. I am used to run time, being a frequent user of JavaScript and other languages where this is not an issue (and sometimes a source of serious other problems). So, instead, let's use a vector, or Vec, and put our item on the heap instead. Ideally, we should be telling Rust exactly what the max capacity of our items are, but we will gloss over this for now. So, to recap, our Bottle struct now looks like this:

#[derive(Serialize, Deserialize)]
struct Bottle {
    id: u16,
    name: String,
    category: Category,
    sub_category: Vec<SubCategory>,
    location: Location,
}
Final Bottle struct.

We also need to update our route to make our data a Vec for the object we are returning. Our updated route looks like this:

#[get("/random")]
fn get_random_bottle() -> Json<Bottle>  {
    Json(
        Bottle {
            id: 1,
            name: "Faretti Biscotti Famosi".to_string(),
            category: Category::Liqueurs,
            sub_category: [SubCategory::Sweet].to_vec(),
            location: Location {
                room: Room::LivingRoom,
                storage: Storage::LeftIkea,
                shelf: Shelf::Shelf5
            }
        }
    )
}

Notice the to_vec() on line 8. Alright, let's save and see what the compiler says... Oh!

the trait `Deserialize<'_>` is not implemented for `Location`

Duh. We need to make sure Deserialize and Serialize exist for all of our data structures! Let's make sure to decorate each of our enums and structs with #[derive(Serialize, Deserialize)]. Okay, let's save again... Oh interesting! Another issue related to our Vec array issues. We get an error related to the line we just updated:

the trait `Clone` is not implemented for `SubCategory`

So. The issue is, we started with an array which we knew the exact size of. But by default, our new enum does not have the Clone trait, which is what we need to copy it in to our Vec using the to_vec() function. Now we could solve this a couple of ways... We could just add Clone to our #derive above the enum definition for SubCategory. But do we really need Clone? We are only returning the object this way for testing our data structures and route. Instead, I changed my route function again to get rid of the to_vec() call and used the vec! macro for a more convenient initialization of my test data.

Alright let's save and see what we get... Success! Our new data structures work, and if we go to our localhost, we get a JSON response showing the data we serialized! Whoo!

We accomplished our goal of getting an initial "random bottle" working. It took a bit and we had some fun playing around with data structures in Rust, but mission accomplished. Next time, I plan on implementing the other basic routes I will need (still with dummy data for the time being): create, list, and delete. I will also implement a search route... but we may leave that for Part V!!! For now, I think I deserve a break...

Canned Cocktail Goodness

Okay, yeah yeah... Its not a beer. This is not the first time I have deviated from beer here and it won't be the last (equal opportunity alcohol consumer, remember). Anyway, its summer. I love Mai Tais. I don't always love making Mai Tais. Believe it or not (hey, don't roll your eyes) I am sometimes very lazy. Luckily, Cutwater makes pretty damn good canned Mai Tai!

Canned cocktails have gotten big the last few years. Unfortunately a lot of them suck. As in suck really, really, bad. Cutwater's offerings have been pretty consistently good. The Mai Tai, clocking in at 12.5% ABV, is one of their best in my opinion. Even my father approved when he tried them over the 4th of July holiday! They are not too sweet, which is frequently an issue with these canned concoctions. The beverage actually tastes like rum: not as good as the funk you are going to get from a good quality rum in a homemade one, but hey, its there. The Mai Tai also has the hint of almond I expect from a proper version (orgeat, yum) and the citrus tastes more or less like good quality juice (not a sugary substitute). Overall, 4/5, I buy them regularly and seek them out. Perfect lazy summer cocktail in a can! In an aside... I realized when I uploaded my photo of the can... It perfectly matches my current nail color which I also appreciate. Ha!

Picture of Cutwater Mai Tai held in hand.