RustArena

Step 1 of 7

Constructors, Default and builders

Rust has no constructors in the language, no default arguments and no function overloading. Instead there are a few conventions every Rust developer recognizes, and following them makes your types feel familiar to your teammates.

Default and struct update

struct Settings {
    volume: u8,
    fullscreen: bool,
    name: String,
    fps_limit: Option<u32>,
}

impl Default for Settings {
    fn default() -> Self {
        Settings { volume: 80, fullscreen: true, name: String::from("player"), fps_limit: None }
    }
}

fn main() {
    let s = Settings { volume: 30, fps_limit: Some(60), ..Default::default() };
    println!("{} {} {} {:?}", s.volume, s.fullscreen, s.name, s.fps_limit);
    let zeroes: (u32, String, Vec<i32>, bool, Option<u8>) = Default::default();
    println!("{zeroes:?}");
}
30 true player Some(60)
(0, "", [], false, None)

..Default::default() fills every field you didn't write from Settings::default(). Most std types implement Default (0, empty string, empty collections, false, None), so #[derive(Default)] works whenever those zero values are right; write the impl by hand when they aren't. Clippy's new_without_default lint asks for a Default impl whenever you write a public new() without arguments, because generic code (and ..Default::default()) can use it.

Builders

An HTTP request has a URL and maybe a method, a timeout, headers, a body... A new with seven parameters is unreadable at the call site (new(url, "GET", 30, true, None, ...): which true is which?). A builder gives every option a named method:

struct Request {
    url: String,
    method: String,
    timeout_secs: u32,
    headers: Vec<(String, String)>,
}

struct RequestBuilder {
    url: String,
    method: String,
    timeout_secs: u32,
    headers: Vec<(String, String)>,
}

impl RequestBuilder {
    fn new(url: &str) -> Self {
        RequestBuilder { url: url.to_string(), method: String::from("GET"), timeout_secs: 30, headers: Vec::new() }
    }

    fn method(mut self, method: &str) -> Self {
        self.method = method.to_string();
        self
    }

    fn timeout(mut self, secs: u32) -> Self {
        self.timeout_secs = secs;
        self
    }

    fn header(mut self, name: &str, value: &str) -> Self {
        self.headers.push((name.to_string(), value.to_string()));
        self
    }

    fn build(self) -> Result<Request, String> {
        if !self.url.starts_with("https://") {
            return Err(format!("url must use https: {}", self.url));
        }
        if self.timeout_secs == 0 {
            return Err(String::from("timeout must be positive"));
        }
        Ok(Request { url: self.url, method: self.method, timeout_secs: self.timeout_secs, headers: self.headers })
    }
}

fn main() {
    let req = RequestBuilder::new("https://api.arena.dev/scores")
        .method("POST")
        .timeout(5)
        .header("Accept", "application/json")
        .build()
        .expect("valid request");
    println!("{} {} ({}s, {} header)", req.method, req.url, req.timeout_secs, req.headers.len());

    match RequestBuilder::new("http://insecure.example").build() {
        Ok(_) => println!("built"),
        Err(e) => println!("error: {e}"),
    }
}
POST https://api.arena.dev/scores (5s, 1 header)
error: url must use https: http://insecure.example

Piece by piece:

Common mistakes

Forgetting .build(): you're holding a builder, not the thing you wanted.

struct Request {
    url: String,
}

struct RequestBuilder {
    url: String,
}

impl RequestBuilder {
    fn new(url: &str) -> Self {
        RequestBuilder { url: url.to_string() }
    }

    fn build(self) -> Request {
        Request { url: self.url }
    }
}

fn send(req: Request) {
    println!("sending to {}", req.url);
}

fn main() {
    send(RequestBuilder::new("https://arena.dev"));
}
error[E0308]: mismatched types

Your task

Write a builder for Server. Server::builder() returns a ServerBuilder with defaults host 127.0.0.1, port 8080, workers 4, tls false. Add consuming setters host, port, workers and tls, and build() returning Result<Server, String> with the errors port must not be 0 and workers must be 1 to 64 (check the port first).

Challenge 2

Implement Default for LogConfig (level "info", color true, max_files 5, path "app.log"), then write fn debug_config() -> LogConfig that returns the defaults with only level set to "debug" and max_files to 20, using struct update syntax.

Challenge 3

Write a consuming builder that produces SQL text. QueryBuilder::new("users") starts a query; .filter(cond) can be called several times (conditions are joined with AND), .order_by(column) and .limit(n) are optional. .build() returns the query string, for example SELECT * FROM users WHERE age > 18 AND active ORDER BY name LIMIT 10. Leave out the parts that weren't set.

Next: Newtypes: give plain values a meaning