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).
#[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);
}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.
#[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());
}Fahrenheit(212.0)
-40.0°FAlways implement From, never Into directly. You get both.
3. From in the standard library
Many lossless conversions are provided.
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);
}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.
fn main() {
let big: i64 = 5_000_000_000;
let small = i32::from(big);
println!("{small}");
}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.
#[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);
}[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.
#[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);
}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.
#[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);
}[Int(42), Text("ada"), Flag(true)]8. TryFrom: conversions that can fail
TryFrom returns a Result. Numeric conversions between integer types use it.
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);
}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.
#[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);
}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
#[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));
}
}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.
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);
}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.
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>());
}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.
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));
}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.
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"]));
}2
3
3
5
5The 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 aStringavoid 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.
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")));
}true false16. 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:
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:
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"));
}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.
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));
}é = U+00E9
Some('🦀')
None
Some('7')
A18. Bytes and text
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}");
}[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 |
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);
}500 Err(TryFromIntError(())) 24420. 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.
#[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);
}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.
#[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}");
}Tags(["rust", "cli", "async", "tokio"])
events: ab22. Chaining conversions
Conversions compose. Here raw input is parsed with FromStr, validated with TryFrom, and converted with From.
#[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));
}
} 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.
#[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}"),
}
}
}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 40024. Realistic example: human-friendly durations
Configuration files often say 30s, 5m, or 2h. FromStr makes them parse like numbers, and Display prints them back.
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());
} 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: true25. Realistic example: typed IDs from numbers and strings
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}"),
}
}
}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.
#[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());
}SELECT * FROM orders WHERE status = 'paid' AND customer_id = '42' LIMIT 25
SELECT * FROM usersimpl 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().
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>());
}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 Bfor infallible conversions. You getIntofor free. Never implementIntodirectly. - Implement
TryFromfor conversions that can fail. It's the idiomatic validating constructor. - Implement
FromStrto make.parse::<YourType>()work, andDisplayto print it back. From<LowLevelError> for YourErrorlets?convert errors automatically.- Take
impl Into<String>for text you store,impl AsRef<str>or&strfor text you only read. - Implement
FromIteratorandExtendsocollect()andextend()work with your collections. - Prefer
From/TryFromoverasfor numbers, and keepasfor deliberate truncation.
This completes the Traits & Generics section. Next: Closures.
