Rust snippets

Contents

1 Introduction

This article is essentially a collection of code snippets written in rust with no clear theme. They include “raw” rust and state-of-the-art crates.

I’ll probably change this article as things evolve. I plan on removing snippets about crates that are no longer a consensus, for intance.

For the examples below, we are using an example crate called snippets, with a binary called tool.

2 Top level

2.1 Tool-specific main function

When creating a tool, we can write a placeholder src/bin/tool.rs file that calls a few default initialization functions that should only be called once, and then calls a tool-specific main function coming from somewhere referenced by lib.rs.

This tool-specific main function does all initialization and looks like the following:

// Copyright information ...
use clap::Parser;
use color_eyre::Result;

#[derive(Parser, Debug)]
#[command(author, version, about, long_about = None)]
pub struct Cli {
    // Add command-line arguments to this struct, with documentation
}

#[tracing::instrument]
pub fn main() -> Result<()> {
    let args = Cli::parse();
    // Program goes here
    // Example trace:
    tracing::info!("args struct: {:?}", args);
    // Return Ok(()) on success
    Ok(())
}

We can also put this code in the top-level main function (example below) when it’s more convenient.

2.2 Top-level main function

The top-level main function that goes in the src/bin/tool.rs is pretty much always like the following:

// Copyright information ...

fn main() -> Result<(), Box<dyn std::error::Error>> {
    color_eyre::install()?;
    tracing_subscriber::fmt()
        .with_span_events(tracing_subscriber::fmt::format::FmtSpan::ACTIVE)
        .with_env_filter(tracing_subscriber::EnvFilter::from_default_env())
        .init();
    snippets::main()?;
    Ok(())
}

2.3 Crates

For these the functions above to compile, we need some crates:

cargo add clap --features derive
cargo add tracing
cargo add tracing-subscriber --features env-filter,tracing-log
cargo add color_eyre

What we get by using the batteries-included main above:

It’s worth noting that there is a whole lot that can be done with tracing, including directing it to OpenTelemetry and/or using it for profiling, maybe with flamegraphs.

Be aware, though, that the setup above directs logs to the tracing infra and not the other way around - meaning that if we increment the setup above to direct traces to the logging infra, we then effectively set up a tracing-logging loop.

2.4 Async tool with tokio

Async versions are not so different. Add the crate:

cargo add tokio --features=macros,rt,rt-multi-thread

Add async to the tool-specific main:

// Copyright information ...
use clap::Parser;
use color_eyre::Result;

#[derive(Parser, Debug)]
#[command(author, version, about, long_about = None)]
pub struct Cli {
    // Add command-line arguments to this struct, with documentation
}

#[tracing::instrument]
pub async fn main() -> Result<()> {
    let args = Cli::parse();
    // Program goes here
    // Example trace:
    tracing::info!("args struct: {:?}", args);
    // Return Ok(()) on success
    Ok(())
}

And add #[tokio::main] and async to the top-level main:

// Copyright information ...

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    color_eyre::install()?;
    tracing_subscriber::fmt()
        .with_span_events(tracing_subscriber::fmt::format::FmtSpan::ACTIVE)
        .with_env_filter(tracing_subscriber::EnvFilter::from_default_env())
        .init();
    snippets::main().await?;
    Ok(())
}

3 Errors

3.1 Error type creation, crateless

use std::error;
use std::fmt;
use std::io;
use std::num;

#[derive(Debug)]
enum MyError {
    Io(io::Error),
    Parse(num::ParseIntError),
}

impl fmt::Display for MyError {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match *self {
            MyError::Io(ref err) => write!(f, "IO error: {}", err),
            MyError::Parse(ref err) => write!(f, "Parse error: {}", err),
        }
    }
}

impl error::Error for MyError {
    fn source(&self) -> Option<&(dyn error::Error + 'static)> {
        match *self {
            MyError::Io(ref err) => Some(err),
            MyError::Parse(ref err) => Some(err),
        }
    }
}

impl From<io::Error> for MyError {
    fn from(err: io::Error) -> MyError {
        MyError::Io(err)
    }
}

impl From<num::ParseIntError> for MyError {
    fn from(err: num::ParseIntError) -> MyError {
        MyError::Parse(err)
    }
}

3.2 Error type creation with thiserror

https://docs.rs/thiserror/latest/thiserror/

use thiserror::Error;

#[derive(Error, Debug)]
pub enum DataStoreError {
    #[error("data store disconnected")]
    Disconnect(#[from] io::Error),
    #[error("the data for key `{0}` is not available")]
    Redaction(String),
    #[error("invalid header (expected {expected:?}, found {found:?})")]
    InvalidHeader {
        expected: String,
        found: String,
    },
    #[error("unknown data store error")]
    Unknown,
}

3.3 Error handling with eyre

use color_eyre::{eyre::eyre, Result};

fn get_cluster_info() -> Result<ClusterMap> {
    let config = std::fs::read_to_string("cluster.json")?;
    let map: ClusterMap = serde_json::from_str(&config)?;
    let opt = None;
    let fromopt = opt.ok_or_else(|| eyre!("error message"));
    Ok(map)
}

4 Traits for instantiation

These traits create instances of the type they implement. Consider the following example type for the sections below:

#[derive(Debug)]
struct MyType {
    value: usize,
    // ...
}

4.1 Default

This can be auto-derived if we want to use the default for all the members. Otherwise, we have to define it manually.

https://doc.rust-lang.org/std/default/trait.Default.html

impl Default for MyType {
    fn default() -> Self {
        Self { value: 10 }
    }
}

4.2 From

https://doc.rust-lang.org/std/convert/trait.From.html

impl From<usize> for MyType {
    fn from(value: usize) -> Self {
        MyType { value }
    }
}

4.3 TryFrom

https://doc.rust-lang.org/stable/std/convert/trait.TryFrom.html

impl TryFrom<u32> for MyType {
    type Error = &'static str;
    fn try_from(value: u32) -> Result<Self, Self::Error> {
        Ok(MyType {
            value: value as usize,
        })
    }
}

5 Traits for string conversion

5.1 Debug

https://doc.rust-lang.org/std/fmt/trait.Debug.html

use std::fmt;

impl fmt::Debug for MyType {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.debug_struct("MyType")
            .field("value", &self.value)
            .finish()
    }
}

5.2 Display

https://doc.rust-lang.org/std/fmt/trait.Display.html

use std::fmt;

impl fmt::Display for MyType {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{}", self.value)
    }
}

5.3 FromStr

https://doc.rust-lang.org/std/str/trait.FromStr.html

use std::{num, str::FromStr};

impl FromStr for MyType {
    type Err = num::ParseIntError;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        let value = s.parse::<usize>()?;
        Ok(MyType { value })
    }
}

6 Sync I/O

6.1 Running external commands

https://doc.rust-lang.org/std/process/struct.Command.html

https://doc.rust-lang.org/std/process/struct.Child.html

Run the command and get the whole output directly:

Command::new("ls")
    .arg("-l")
    .arg("/")
    .output()?;

Spawn a child, read output line-by-line:

use std::io::{BufRead, BufReader, Error, ErrorKind};
use std::process::{Command, Stdio};

pub fn run_commands() -> std::io::Result<()> {
    let mut child = Command::new("ls")
        .args(["-l", "/"])
        .stdout(Stdio::piped())
        .spawn()?;
    let stdout = child
        .stdout
        .as_mut()
        .ok_or(Error::from(ErrorKind::BrokenPipe))?;
    for line in BufReader::new(stdout).lines() {
        println!("{:?}", line);
    }
    let result = child.wait()?;
    println!("{:?}", result);
    Ok(())
}

7 Async I/O with tokio

7.1 Running external commands

https://docs.rs/tokio/latest/tokio/process/

cargo add tokio --features=macros,rt,rt-multi-thread,process
use tokio::process::Command;

pub async fn run_commands() -> std::io::Result<()> {
    let output = Command::new("ls").args(["-l", "/"]).output().await?;
    println!("{:?}", output.status);
    println!("{:?}", output.stdout);
    Ok(())
}

8 Misc snippets (i.e. unclassified)

8.1 Entry

https://doc.rust-lang.org/std/collections/hash_map/enum.Entry.html

Or “how to efficiently update a collection entry that may not be there”.

use std::collections::HashMap;
let mut hashmap: HashMap<i32, Vec<i32>> = HashMap::new();
let key = 5;
// The or_* methods return &mut v, which means we can also::
let entry = hashmap.entry(key).or_default();
entry.push(9);
// These 3 options also do the same:
hashmap.entry(key).or_default().push(9);
hashmap.entry(key).or_insert(vec![]).push(9);
hashmap.entry(key).or_insert_with(|| vec![]).push(9);