Guide contents

Lesson 35 of 94

Conversion Traits: From, Into, TryFrom, FromStr, AsRef

Rust has a small set of standard conversion traits, so every conversion in the ecosystem looks the same, and generic code can accept "anything convertible to X".

Trait Meaning Fails? Example
From<T> / Into<U> Convert, always succeeds No String::from("hi"), let n: i64 = 5_i32.into()
TryFrom<T> / TryInto<U> Convert, may fail Result u8::try_from(300)
FromStr Parse from a string Result "42".parse::<i32>()
Display / ToString Convert to a user-facing string No x.to_string()
AsRef<T> Cheaply borrow as &T No fn open(path: impl AsRef<str>)
FromIterator<T> Build from an iterator No iter.collect::<MyType>()

1. From: a conversion that always works

Implement From<Source> for Target. Call it as Target::from(source).

Rust
#[derive(Debug)]
struct Celsius(f64);

#[derive(Debug)]
struct Fahrenheit(f64);

impl From<Celsius> for Fahrenheit {
    fn from(c: Celsius) -> Self {
        Fahrenheit(c.0 * 9.0 / 5.0 + 32.0)
    }
}

fn main() {
    let body = Celsius(37.0);
    let f = Fahrenheit::from(body);
    println!("{:?}", f);
}
Output
Fahrenheit(98.6)

2. Into comes for free

Every From<A> for B automatically provides Into<B> for A. With .into(), the target type comes from context, such as a type annotation or a parameter type.

Rust
#[derive(Debug)]
struct Celsius(f64);

#[derive(Debug)]
struct Fahrenheit(f64);

impl From<Celsius> for Fahrenheit {
    fn from(c: Celsius) -> Self {
        Fahrenheit(c.0 * 9.0 / 5.0 + 32.0)
    }
}

fn show(temp: Fahrenheit) {
    println!("{:.1}°F", temp.0);
}

fn main() {
    let boiling: Fahrenheit = Celsius(100.0).into();
    println!("{:?}", boiling);
    show(Celsius(-40.0).into());
}
Output
Fahrenheit(212.0)
-40.0°F

Always implement From, never Into directly. You get both.

3. From in the standard library

Many lossless conversions are provided.

Rust
fn main() {
    let a = i64::from(42_i32);
    let b = f64::from(1.5_f32);
    let c = u32::from('A');
    let d = String::from('x');
    let e = String::from("text");
    let f = Vec::from([1, 2, 3]);
    let g: Vec<u8> = Vec::from("bytes");
    let h: Box<str> = Box::from("boxed");
    let i: Option<u8> = Option::from(7);
    println!("{a} {b} {c} {d} {e} {:?} {:?} {h} {:?}", f, g, i);
}
Output
42 1.5 65 x text [1, 2, 3] [98, 121, 116, 101, 115] boxed Some(7)

4. There is no From<i64> for i32

Conversions that could lose information are not From. The compiler tells you so, and points you to TryFrom.

Rust✗ does not compile · E0277
fn main() {
    let big: i64 = 5_000_000_000;
    let small = i32::from(big);
    println!("{small}");
}
Output
error[E0277]: the trait bound `i32: From<i64>` is not satisfied
 --> src/main.rs:3:17
  |
3 |     let small = i32::from(big);
  |                 ^^^ the trait `From<i64>` is not implemented for `i32`
  |
  = help: the following other types implement trait `From<T>`:
            `i32` implements `From<bool>`
            `i32` implements `From<i16>`
            `i32` implements `From<i8>`
            `i32` implements `From<u16>`
            `i32` implements `From<u8>`

5. impl Into<String> parameters

A parameter of type impl Into<String> accepts &str, String, char, and anything else convertible, while the function stores an owned String. Passing a String moves it in without copying.

Rust
#[derive(Debug)]
struct Tag {
    name: String,
}

impl Tag {
    fn new(name: impl Into<String>) -> Self {
        Self { name: name.into() }
    }
}

fn main() {
    let owned = String::from("owned");
    let tags = vec![Tag::new("literal"), Tag::new(owned), Tag::new('x'), Tag::new(format!("v{}", 2))];
    println!("{:?}", tags);
}
Output
[Tag { name: "literal" }, Tag { name: "owned" }, Tag { name: "x" }, Tag { name: "v2" }]

6. Several From implementations for one type

A type can be built from many sources, each with its own From impl.

Rust
#[derive(Debug)]
struct Color {
    r: u8,
    g: u8,
    b: u8,
}

impl From<(u8, u8, u8)> for Color {
    fn from((r, g, b): (u8, u8, u8)) -> Self {
        Color { r, g, b }
    }
}

impl From<u32> for Color {
    fn from(hex: u32) -> Self {
        Color { r: (hex >> 16) as u8, g: (hex >> 8) as u8, b: hex as u8 }
    }
}

impl From<[u8; 3]> for Color {
    fn from([r, g, b]: [u8; 3]) -> Self {
        Color { r, g, b }
    }
}

fn main() {
    let a: Color = (255, 128, 0).into();
    let b = Color::from(0x1E90FF);
    let c: Color = [10, 20, 30].into();
    println!("{:?}\n{:?}\n{:?}", a, b, c);
}
Output
Color { r: 255, g: 128, b: 0 }
Color { r: 30, g: 144, b: 255 }
Color { r: 10, g: 20, b: 30 }

hex as u8 keeps only the lowest 8 bits, which is exactly the byte we want after the shift.

7. From for enum variants

A From impl can wrap a value in the right variant automatically.

Rust
#[derive(Debug)]
enum Value {
    Int(i64),
    Text(String),
    Flag(bool),
}

impl From<i64> for Value {
    fn from(n: i64) -> Self {
        Value::Int(n)
    }
}

impl From<&str> for Value {
    fn from(s: &str) -> Self {
        Value::Text(s.to_string())
    }
}

impl From<bool> for Value {
    fn from(b: bool) -> Self {
        Value::Flag(b)
    }
}

fn main() {
    let row: Vec<Value> = vec![42.into(), "ada".into(), true.into()];
    println!("{:?}", row);
}
Output
[Int(42), Text("ada"), Flag(true)]

8. TryFrom: conversions that can fail

TryFrom returns a Result. Numeric conversions between integer types use it.

Rust
fn main() {
    println!("{:?}", u8::try_from(200_i32));
    println!("{:?}", u8::try_from(300_i32));
    println!("{:?}", u32::try_from(-1_i64));
    println!("{:?}", i32::try_from(5_000_000_000_i64).is_err());

    let n: Result<u16, _> = 70_000_u32.try_into();
    println!("{:?}", n);
}
Output
Ok(200)
Err(TryFromIntError(()))
Err(TryFromIntError(()))
true
Err(TryFromIntError(()))

try_into() is the TryInto counterpart of into(), provided automatically for every TryFrom.

9. Implementing TryFrom for validation

TryFrom is the idiomatic "validating constructor". The associated type Error says what a failure looks like.

Rust
#[derive(Debug, Clone, Copy)]
struct Port(u16);

impl TryFrom<u32> for Port {
    type Error = String;

    fn try_from(value: u32) -> Result<Self, Self::Error> {
        match value {
            0 => Err(String::from("port 0 is reserved")),
            1..=65_535 => Ok(Port(value as u16)),
            _ => Err(format!("{value} is not a valid port")),
        }
    }
}

fn main() {
    for v in [8080, 0, 70_000] {
        println!("{v}: {:?}", Port::try_from(v));
    }
    let p: Result<Port, _> = 443_u32.try_into();
    println!("{:?}", p);
}
Output
8080: Ok(Port(8080))
0: Err("port 0 is reserved")
70000: Err("70000 is not a valid port")
Ok(Port(443))

10. TryFrom<&str> with a custom error enum

Rust
#[derive(Debug, PartialEq)]
enum HttpMethod {
    Get,
    Post,
    Put,
    Delete,
}

#[derive(Debug)]
struct UnknownMethod(String);

impl TryFrom<&str> for HttpMethod {
    type Error = UnknownMethod;

    fn try_from(s: &str) -> Result<Self, Self::Error> {
        match s.to_ascii_uppercase().as_str() {
            "GET" => Ok(HttpMethod::Get),
            "POST" => Ok(HttpMethod::Post),
            "PUT" => Ok(HttpMethod::Put),
            "DELETE" => Ok(HttpMethod::Delete),
            _ => Err(UnknownMethod(s.to_string())),
        }
    }
}

fn main() {
    for m in ["get", "POST", "patch"] {
        println!("{m}: {:?}", HttpMethod::try_from(m));
    }
}
Output
get: Ok(Get)
POST: Ok(Post)
patch: Err(UnknownMethod("patch"))

11. FromStr: making .parse() work for your type

str::parse::<T>() works for any T that implements FromStr. Implementing it lets your type be parsed like a number.

Rust
use std::str::FromStr;

#[derive(Debug, PartialEq)]
struct Rgb(u8, u8, u8);

impl FromStr for Rgb {
    type Err = String;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        let hex = s.strip_prefix('#').ok_or("color must start with '#'")?;
        if hex.len() != 6 {
            return Err(format!("expected 6 hex digits, got {}", hex.len()));
        }
        let channel = |i: usize| u8::from_str_radix(&hex[i..i + 2], 16).map_err(|e| e.to_string());
        Ok(Rgb(channel(0)?, channel(2)?, channel(4)?))
    }
}

fn main() {
    println!("{:?}", "#ff8800".parse::<Rgb>());
    println!("{:?}", "ff8800".parse::<Rgb>());
    println!("{:?}", "#ff88".parse::<Rgb>());
    println!("{:?}", "#gg0000".parse::<Rgb>());
    let c: Rgb = "#1e90ff".parse().unwrap();
    println!("{:?}", c);
}
Output
Ok(Rgb(255, 136, 0))
Err("color must start with '#'")
Err("expected 6 hex digits, got 4")
Err("invalid digit found in string")
Rgb(30, 144, 255)

u8::from_str_radix(text, 16) parses a number in base 16.

12. Round-tripping with Display and FromStr

A type that implements both can be written out and read back, which is the basis for configuration files and command-line arguments.

Rust
use std::fmt;
use std::str::FromStr;

#[derive(Debug, Clone, Copy, PartialEq)]
struct Version {
    major: u32,
    minor: u32,
    patch: u32,
}

impl fmt::Display for Version {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "{}.{}.{}", self.major, self.minor, self.patch)
    }
}

impl FromStr for Version {
    type Err = String;
    fn from_str(s: &str) -> Result<Self, String> {
        let parts: Vec<&str> = s.trim_start_matches('v').split('.').collect();
        let [major, minor, patch] = parts.as_slice() else {
            return Err(format!("'{s}' should look like 1.2.3"));
        };
        let num = |p: &str| p.parse::<u32>().map_err(|_| format!("'{p}' is not a number"));
        Ok(Version { major: num(major)?, minor: num(minor)?, patch: num(patch)? })
    }
}

fn main() {
    let v: Version = "v1.10.3".parse().unwrap();
    let text = v.to_string();
    let back: Version = text.parse().unwrap();
    println!("{v} -> \"{text}\" -> {:?}, equal: {}", back, v == back);
    println!("{:?}", "1.2".parse::<Version>());
    println!("{:?}", "1.x.3".parse::<Version>());
}
Output
1.10.3 -> "1.10.3" -> Version { major: 1, minor: 10, patch: 3 }, equal: true
Err("'1.2' should look like 1.2.3")
Err("'x' is not a number")

13. From for error types makes ? convert automatically

? calls From::from on the error. Implementing From<LowLevelError> for AppError lets ? turn low-level errors into your application's error type. Error Handling builds on this.

Rust
use std::num::ParseIntError;

#[derive(Debug)]
enum ConfigError {
    BadNumber(ParseIntError),
    Missing(&'static str),
}

impl From<ParseIntError> for ConfigError {
    fn from(e: ParseIntError) -> Self {
        ConfigError::BadNumber(e)
    }
}

fn read_workers(value: Option<&str>) -> Result<u32, ConfigError> {
    let text = value.ok_or(ConfigError::Missing("workers"))?;
    let n: u32 = text.parse()?; // ParseIntError converted into ConfigError by `?`
    Ok(n)
}

fn main() {
    println!("{:?}", read_workers(Some("8")));
    println!("{:?}", read_workers(Some("eight")));
    println!("{:?}", read_workers(None));
}
Output
Ok(8)
Err(BadNumber(ParseIntError { kind: InvalidDigit }))
Err(Missing("workers"))

14. AsRef: accept anything that can be borrowed as &T

AsRef<str> is implemented by String, &str, and other string types. A function taking impl AsRef<str> accepts them all, without converting or cloning.

Rust
fn word_count(text: impl AsRef<str>) -> usize {
    text.as_ref().split_whitespace().count()
}

fn total_len<T: AsRef<[u8]>>(chunks: &[T]) -> usize {
    chunks.iter().map(|c| c.as_ref().len()).sum()
}

fn main() {
    let owned = String::from("owned text here");
    println!("{}", word_count("a literal"));
    println!("{}", word_count(&owned));
    println!("{}", word_count(owned));

    let packets = vec![vec![1_u8, 2, 3], vec![4, 5]];
    println!("{}", total_len(&packets));
    println!("{}", total_len(&["abc", "de"]));
}
Output
2
3
3
5
5

The standard library uses AsRef<Path> so that File::open accepts &str, String, Path, and PathBuf.

15. Into vs AsRef for parameters

  • impl Into<String>: the function keeps the text (stores it), so it needs ownership. Callers passing a String avoid a copy.
  • impl AsRef<str>: the function only reads the text. Nothing is moved or copied.
  • &str: the simplest choice when you only read. Most functions just use this.
Rust
struct Registry {
    names: Vec<String>,
}

impl Registry {
    fn add(&mut self, name: impl Into<String>) {
        self.names.push(name.into()); // keeps it: needs ownership
    }

    fn contains(&self, name: impl AsRef<str>) -> bool {
        self.names.iter().any(|n| n == name.as_ref()) // only reads it
    }
}

fn main() {
    let mut r = Registry { names: Vec::new() };
    r.add("auth");
    r.add(String::from("billing"));
    println!("{} {}", r.contains("auth"), r.contains(String::from("search")));
}
Output
true false

16. Borrow: why map.get("key") works with String keys

A HashMap<String, u32> can be searched with a &str, without creating a String. That works because of the Borrow trait. HashMap::get is declared roughly like this:

Rust
impl<K: Hash + Eq, V> HashMap<K, V> {
    pub fn get<Q>(&self, key: &Q) -> Option<&V>
    where
        K: Borrow<Q>,
        Q: Hash + Eq + ?Sized,
    { /* ... */ }
}

String: Borrow<str>, so K = String and Q = str. (?Sized allows Q to be str, whose size isn't known at compile time.) The same pattern works in your own generic code:

Rust
use std::borrow::Borrow;
use std::collections::{HashMap, HashSet};

// Look up an owned key with any borrowed form of it.
fn find<'a, K, Q, V>(entries: &'a [(K, V)], key: &Q) -> Option<&'a V>
where
    K: Borrow<Q>,
    Q: PartialEq + ?Sized,
{
    entries.iter().find(|(k, _)| k.borrow() == key).map(|(_, v)| v)
}

fn main() {
    let mut stock: HashMap<String, u32> = HashMap::new();
    stock.insert(String::from("apple"), 12);
    println!("{:?}", stock.get("apple")); // &str, not &String

    let seen: HashSet<Vec<u8>> = HashSet::from([vec![1, 2], vec![3]]);
    println!("{}", seen.contains(&[1, 2][..])); // &[u8], not &Vec<u8>

    let prices = vec![(String::from("pear"), 3), (String::from("plum"), 7)];
    println!("{:?}", find(&prices, "plum"));
}
Output
Some(12)
true
Some(7)

Borrow looks like AsRef, with one extra rule: the borrowed form must hash, compare, and order exactly like the owned form. String and str do, so HashMap can hash a &str and find a String key. AsRef makes no such promise, which is why collections use Borrow and ordinary "accept anything string-like" parameters use AsRef.

17. char and number conversions

u32::from(char) always works, but not every u32 is a valid character, so the reverse returns an Option.

Rust
fn main() {
    let c = 'é';
    let code = u32::from(c);
    println!("{c} = U+{code:04X}");
    println!("{:?}", char::from_u32(0x1F980));
    println!("{:?}", char::from_u32(0xD800)); // a UTF-16 surrogate: not a valid char
    println!("{:?}", char::from_digit(7, 10));
    println!("{}", char::from(65_u8));
}
Output
é = U+00E9
Some('🦀')
None
Some('7')
A

18. Bytes and text

Rust
fn main() {
    let text = String::from("héllo");
    let bytes: Vec<u8> = text.clone().into_bytes(); // String -> Vec<u8>, no copy
    println!("{:?}", bytes);
    let back = String::from_utf8(bytes).unwrap(); // Vec<u8> -> String, validated
    println!("{back}");
    let view: &[u8] = text.as_bytes(); // borrow as bytes
    println!("{} bytes", view.len());
    let s: &str = std::str::from_utf8(&view[..2]).unwrap_or("<invalid>"); // cuts "é" in half
    println!("{s}");
}
Output
[104, 195, 169, 108, 108, 111]
héllo
6 bytes
<invalid>

19. as vs From vs TryFrom

Conversion Use Why
Widening, lossless (i32 → i64) i64::from(x) or x.into() Breaks the build if the types later change to a lossy pair
Narrowing, must be checked (i64 → i32) i32::try_from(x)? Makes the failure case explicit
Deliberate truncation or wrapping x as u8 Shows you intend to discard bits
Float ↔ integer x as i64, n as f64 From doesn't cover lossy float conversions
Rust
fn main() {
    let count: u16 = 500;
    let widened: u64 = count.into();
    let checked: Result<u8, _> = u8::try_from(count);
    let truncated = count as u8;
    println!("{widened} {:?} {truncated}", checked);
}
Output
500 Err(TryFromIntError(())) 244

20. FromIterator: collect() into your own type

collect() can build any type that implements FromIterator. Implementing it for your type makes pipelines end directly in your type.

Rust
#[derive(Debug, Default)]
struct Stats {
    count: usize,
    min: Option<i64>,
    max: Option<i64>,
    sum: i64,
}

impl FromIterator<i64> for Stats {
    fn from_iter<I: IntoIterator<Item = i64>>(iter: I) -> Self {
        let mut s = Stats::default();
        for x in iter {
            s.count += 1;
            s.sum += x;
            s.min = Some(s.min.map_or(x, |m| m.min(x)));
            s.max = Some(s.max.map_or(x, |m| m.max(x)));
        }
        s
    }
}

fn main() {
    let readings = [12, -4, 30, 7];
    let stats: Stats = readings.iter().copied().collect();
    println!("{:?}", stats);
    let evens: Stats = (1..=10).filter(|n| n % 2 == 0).collect();
    println!("{:?}", evens);
}
Output
Stats { count: 4, min: Some(-4), max: Some(30), sum: 45 }
Stats { count: 5, min: Some(2), max: Some(10), sum: 30 }

21. Extend: adding many items at once

Extend is the "add everything from this iterator" counterpart. Vec, String, and HashMap implement it, and you can implement it for your own collections.

Rust
#[derive(Debug, Default)]
struct Tags(Vec<String>);

impl<'a> Extend<&'a str> for Tags {
    fn extend<I: IntoIterator<Item = &'a str>>(&mut self, iter: I) {
        for t in iter {
            let t = t.trim().to_lowercase();
            if !t.is_empty() && !self.0.contains(&t) {
                self.0.push(t);
            }
        }
    }
}

fn main() {
    let mut tags = Tags::default();
    tags.extend(["Rust", " CLI "]);
    tags.extend("rust,async,,Tokio".split(','));
    println!("{:?}", tags);

    let mut log = String::from("events:");
    log.extend([' ', 'a', 'b']);
    println!("{log}");
}
Output
Tags(["rust", "cli", "async", "tokio"])
events: ab

22. Chaining conversions

Conversions compose. Here raw input is parsed with FromStr, validated with TryFrom, and converted with From.

Rust
#[derive(Debug)]
struct Percent(u8);

impl TryFrom<i64> for Percent {
    type Error = String;
    fn try_from(v: i64) -> Result<Self, String> {
        u8::try_from(v).ok().filter(|p| *p <= 100).map(Percent).ok_or(format!("{v} is not a percentage"))
    }
}

impl From<Percent> for f64 {
    fn from(p: Percent) -> f64 {
        p.0 as f64 / 100.0
    }
}

fn parse_ratio(input: &str) -> Result<f64, String> {
    let raw: i64 = input.trim().trim_end_matches('%').parse().map_err(|_| format!("'{input}' is not a number"))?;
    let percent = Percent::try_from(raw)?;
    Ok(percent.into())
}

fn main() {
    for input in ["75%", " 5 ", "150%", "-3", "half"] {
        println!("{input:>6} -> {:?}", parse_ratio(input));
    }
}
Output
   75% -> Ok(0.75)
    5  -> Ok(0.05)
  150% -> Err("150 is not a percentage")
    -3 -> Err("-3 is not a percentage")
  half -> Err("'half' is not a number")

.map(Percent) works because a tuple struct's name is also a function that builds it.

23. Realistic example: API data to domain models

External data (a JSON payload, a database row) arrives in a loose shape. TryFrom turns it into a validated domain type in one place, and From turns domain types back into the external shape.

Rust
#[derive(Debug)]
struct UserDto {
    email: String,
    age: i64,
    roles: String, // comma-separated
}

#[derive(Debug)]
struct User {
    email: String,
    age: u8,
    roles: Vec<String>,
}

impl TryFrom<UserDto> for User {
    type Error = String;

    fn try_from(dto: UserDto) -> Result<Self, String> {
        if !dto.email.contains('@') {
            return Err(format!("invalid email '{}'", dto.email));
        }
        let age = u8::try_from(dto.age).map_err(|_| format!("invalid age {}", dto.age))?;
        let roles = dto.roles.split(',').map(|r| r.trim().to_string()).filter(|r| !r.is_empty()).collect();
        Ok(User { email: dto.email.to_lowercase(), age, roles })
    }
}

impl From<&User> for UserDto {
    fn from(u: &User) -> Self {
        UserDto { email: u.email.clone(), age: u.age as i64, roles: u.roles.join(",") }
    }
}

fn main() {
    let incoming = vec![
        UserDto { email: String::from("Ana@Example.com"), age: 34, roles: String::from("admin, editor") },
        UserDto { email: String::from("bob"), age: 20, roles: String::new() },
        UserDto { email: String::from("cy@example.com"), age: 400, roles: String::from("viewer") },
    ];
    for dto in incoming {
        match User::try_from(dto) {
            Ok(user) => println!("ok: {:?} -> back to {:?}", user, UserDto::from(&user)),
            Err(e) => println!("rejected: {e}"),
        }
    }
}
Output
ok: User { email: "ana@example.com", age: 34, roles: ["admin", "editor"] } -> back to UserDto { email: "ana@example.com", age: 34, roles: "admin,editor" }
rejected: invalid email 'bob'
rejected: invalid age 400

24. Realistic example: human-friendly durations

Configuration files often say 30s, 5m, or 2h. FromStr makes them parse like numbers, and Display prints them back.

Rust
use std::fmt;
use std::str::FromStr;

#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
struct Duration {
    secs: u64,
}

impl FromStr for Duration {
    type Err = String;
    fn from_str(s: &str) -> Result<Self, String> {
        let s = s.trim();
        let split = s.find(|c: char| !c.is_ascii_digit()).ok_or(format!("'{s}' has no unit"))?;
        let (num, unit) = s.split_at(split);
        let n: u64 = num.parse().map_err(|_| format!("'{s}' has no number"))?;
        let factor = match unit {
            "s" => 1,
            "m" => 60,
            "h" => 3_600,
            "d" => 86_400,
            other => return Err(format!("unknown unit '{other}'")),
        };
        Ok(Duration { secs: n * factor })
    }
}

impl fmt::Display for Duration {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match self.secs {
            s if s % 86_400 == 0 => write!(f, "{}d", s / 86_400),
            s if s % 3_600 == 0 => write!(f, "{}h", s / 3_600),
            s if s % 60 == 0 => write!(f, "{}m", s / 60),
            s => write!(f, "{s}s"),
        }
    }
}

fn main() {
    for input in ["30s", "90m", "48h", "7d", "10x", "m"] {
        match input.parse::<Duration>() {
            Ok(d) => println!("{input:>4} = {} seconds, shown as {d}", d.secs),
            Err(e) => println!("{input:>4} error: {e}"),
        }
    }
    let timeout: Duration = "5m".parse().unwrap();
    println!("5m > 299s: {}", timeout > "299s".parse().unwrap());
}
Output
 30s = 30 seconds, shown as 30s
 90m = 5400 seconds, shown as 90m
 48h = 172800 seconds, shown as 2d
  7d = 604800 seconds, shown as 7d
 10x error: unknown unit 'x'
   m error: 'm' has no number
5m > 299s: true

25. Realistic example: typed IDs from numbers and strings

Rust
use std::fmt;

#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
struct OrderId(u64);

impl From<u64> for OrderId {
    fn from(n: u64) -> Self {
        OrderId(n)
    }
}

impl TryFrom<&str> for OrderId {
    type Error = String;
    fn try_from(s: &str) -> Result<Self, String> {
        let digits = s.strip_prefix("ORD-").ok_or(format!("'{s}' must start with ORD-"))?;
        digits.parse::<u64>().map(OrderId).map_err(|_| format!("'{s}' has a bad number"))
    }
}

impl fmt::Display for OrderId {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "ORD-{:06}", self.0)
    }
}

fn main() {
    let from_db: OrderId = 4_812.into();
    println!("{from_db}");
    for s in ["ORD-004812", "ORD-12a", "4812"] {
        match OrderId::try_from(s) {
            Ok(id) => println!("{s} -> {:?} (same as db id: {})", id, id == from_db),
            Err(e) => println!("{s} -> {e}"),
        }
    }
}
Output
ORD-004812
ORD-004812 -> OrderId(4812) (same as db id: true)
ORD-12a -> 'ORD-12a' has a bad number
4812 -> '4812' must start with ORD-

26. Realistic example: a flexible query builder

Into and AsRef bounds make an API pleasant to call with whatever the caller has at hand.

Rust
#[derive(Debug, Default)]
struct Query {
    table: String,
    filters: Vec<String>,
    limit: Option<u32>,
}

impl Query {
    fn from_table(name: impl Into<String>) -> Self {
        Query { table: name.into(), ..Default::default() }
    }

    fn filter(mut self, column: impl AsRef<str>, value: impl ToString) -> Self {
        self.filters.push(format!("{} = '{}'", column.as_ref(), value.to_string()));
        self
    }

    fn limit(mut self, n: impl TryInto<u32>) -> Self {
        self.limit = n.try_into().ok();
        self
    }

    fn sql(&self) -> String {
        let mut s = format!("SELECT * FROM {}", self.table);
        if !self.filters.is_empty() {
            s += &format!(" WHERE {}", self.filters.join(" AND "));
        }
        if let Some(n) = self.limit {
            s += &format!(" LIMIT {n}");
        }
        s
    }
}

fn main() {
    let column = String::from("status");
    let q = Query::from_table("orders").filter(&column, "paid").filter("customer_id", 42).limit(25_u64);
    println!("{}", q.sql());
    let bad_limit = Query::from_table(String::from("users")).limit(-5_i32);
    println!("{}", bad_limit.sql());
}
Output
SELECT * FROM orders WHERE status = 'paid' AND customer_id = '42' LIMIT 25
SELECT * FROM users

impl ToString accepts every Display type, including numbers. A negative limit fails try_into() and is simply ignored here.

27. Realistic example: parsing a whole config with FromStr

Each field type knows how to parse itself, so the config parser just dispatches to .parse().

Rust
use std::str::FromStr;

#[derive(Debug)]
enum LogLevel {
    Debug,
    Info,
    Warn,
}

impl FromStr for LogLevel {
    type Err = String;
    fn from_str(s: &str) -> Result<Self, String> {
        match s.to_lowercase().as_str() {
            "debug" => Ok(LogLevel::Debug),
            "info" => Ok(LogLevel::Info),
            "warn" => Ok(LogLevel::Warn),
            _ => Err(format!("unknown log level '{s}'")),
        }
    }
}

#[derive(Debug)]
struct Config {
    port: u16,
    level: LogLevel,
    hosts: Vec<String>,
}

impl FromStr for Config {
    type Err = String;
    fn from_str(text: &str) -> Result<Self, String> {
        let mut port = 8080;
        let mut level = LogLevel::Info;
        let mut hosts = Vec::new();
        for line in text.lines().map(str::trim).filter(|l| !l.is_empty()) {
            let (key, value) = line.split_once('=').ok_or(format!("bad line '{line}'"))?;
            match key.trim() {
                "port" => port = value.trim().parse().map_err(|e| format!("port: {e}"))?,
                "level" => level = value.trim().parse()?,
                "hosts" => hosts = value.split(',').map(|h| h.trim().to_string()).collect(),
                other => return Err(format!("unknown key '{other}'")),
            }
        }
        Ok(Config { port, level, hosts })
    }
}

fn main() {
    let good = "port = 9000\nlevel = WARN\nhosts = a.internal, b.internal";
    println!("{:?}", good.parse::<Config>());
    println!("{:?}", "level = loud".parse::<Config>());
    println!("{:?}", "port = 99999".parse::<Config>());
}
Output
Ok(Config { port: 9000, level: Warn, hosts: ["a.internal", "b.internal"] })
Err("unknown log level 'loud'")
Err("port: number too large to fit in target type")

.map(str::trim) passes the method str::trim as a function, which is shorter than the closure |l| l.trim().

Summary

  • Implement From<A> for B for infallible conversions. You get Into for free. Never implement Into directly.
  • Implement TryFrom for conversions that can fail. It's the idiomatic validating constructor.
  • Implement FromStr to make .parse::<YourType>() work, and Display to print it back.
  • From<LowLevelError> for YourError lets ? convert errors automatically.
  • Take impl Into<String> for text you store, impl AsRef<str> or &str for text you only read.
  • Implement FromIterator and Extend so collect() and extend() work with your collections.
  • Prefer From/TryFrom over as for numbers, and keep as for deliberate truncation.

This completes the Traits & Generics section. Next: Closures.