Capstone 1: a JSON value

The parser of the previous lesson builds a tree of Value, and the rest of the program should never have to match on that tree by hand. This lesson gives the type its accessors and its first writer: a lookup that returns Option, a length, and a function that turns the tree back into compact JSON.

The shape of the accessors is the important decision. Each one returns a reference or a copy wrapped in an Option, so a missing key and a value of the wrong kind are the same None, and a reader can chain three lookups without a single match. The alternative, a panic on the wrong type, is the habit this lesson replaces.

The six alternatives

A JSON value is one of six things, and the enum says so in one place: null, a boolean, a number, a string, an array of values and an object of pairs. The compiler checks every match that reads it, which is the property the C++ version of this capstone gets from a tagged union by hand.

The four accessors below answer the question "is this a number, a text or a boolean, and what is it". An object or an array answers None to all four, and len answers zero for a scalar.

#[derive(Debug, Clone, PartialEq)] enum Token { OpenBrace, CloseBrace, OpenBracket, CloseBracket, Colon, Comma, Text(String), Number(f64), Word(String), End, } fn text_token(bytes: &[u8], position: &mut usize) -> Result<Token, String> { let start = *position; *position += 1; let mut value = String::new(); while *position < bytes.len() && bytes[*position] != b'"\' { let byte = bytes[*position]; if byte == b\'\\\' { *position += 1; match bytes.get(*position) { Some(b\'"') => value.push('"\'), Some(b\'\\\') => value.push(\'\\\'), Some(b\'n\') => value.push(\'\n\'), Some(b\'t\') => value.push(\'\t\'), Some(b\'u\') => { let digits = bytes.get(*position + 1..*position + 5) .ok_or_else(|| format!("short unicode escape at {}", *position))?; let hex = std::str::from_utf8(digits) .map_err(|_| format!("bad unicode escape at {}", *position))?; let code = u32::from_str_radix(hex, 16) .map_err(|_| format!("bad unicode escape at {}", *position))?; value.push(char::from_u32(code) .ok_or_else(|| format!("bad code point at {}", *position))?); *position += 4; } _ => return Err(format!("bad escape at {}", *position)), } } else { value.push(byte as char); } *position += 1; } if *position == bytes.len() { return Err(format!("unterminated text at {start}")); } *position += 1; Ok(Token::Text(value)) } fn number_token(bytes: &[u8], position: &mut usize) -> Result<Token, String> { let start = *position; while *position < bytes.len() && matches!(bytes[*position], b\'0\'..=b\'9\' | b\'-\' | b\'+\' | b\'.\' | b\'e\' | b\'E\') { *position += 1; } let slice = std::str::from_utf8(&bytes[start..*position]).unwrap(); match slice.parse() { Ok(value) => Ok(Token::Number(value)), Err(_) => Err(format!("bad number at {start}")), } } fn word_token(bytes: &[u8], position: &mut usize) -> Token { let start = *position; while *position < bytes.len() && bytes[*position].is_ascii_alphabetic() { *position += 1; } let slice = std::str::from_utf8(&bytes[start..*position]).unwrap(); Token::Word(slice.to_string()) } fn tokenize(text: &str) -> Result<Vec<Token>, String> { let bytes = text.as_bytes(); let mut position = 0; let mut tokens = Vec::new(); loop { while position < bytes.len() && bytes[position].is_ascii_whitespace() { position += 1; } if position == bytes.len() { tokens.push(Token::End); return Ok(tokens); } let start = position; let token = match bytes[position] { b\'{\' => { position += 1; Token::OpenBrace } b\'}\' => { position += 1; Token::CloseBrace } b\'[\' => { position += 1; Token::OpenBracket } b\']\' => { position += 1; Token::CloseBracket } b\':\' => { position += 1; Token::Colon } b\',\' => { position += 1; Token::Comma } b\'"' => text_token(bytes, &mut position)?, b'-' | b'0'..=b'9' => number_token(bytes, &mut position)?, b'a'..=b'z' => word_token(bytes, &mut position), other => return Err(format!("unexpected character \'{}\' at {start}", other as char)), }; tokens.push(token); } } #[derive(Debug, Clone, PartialEq)] enum Value { Null, Bool(bool), Number(f64), Text(String), Array(Vec<Value>), Object(Vec<(String, Value)>), } struct Parser { tokens: Vec<Token>, position: usize, } impl Parser { fn new(tokens: Vec<Token>) -> Parser { Parser { tokens, position: 0 } } fn peek(&self) -> &Token { &self.tokens[self.position] } fn next(&mut self) -> Token { let token = self.tokens[self.position].clone(); self.position += 1; token } fn value(&mut self) -> Result<Value, String> { match self.peek().clone() { Token::OpenBrace => self.object(), Token::OpenBracket => self.array(), Token::Text(text) => { self.next(); Ok(Value::Text(text)) } Token::Number(number) => { self.next(); Ok(Value::Number(number)) } Token::Word(word) => { self.next(); match word.as_str() { "true" => Ok(Value::Bool(true)), "false" => Ok(Value::Bool(false)), "null" => Ok(Value::Null), other => Err(format!("unknown word {other}")), } } other => Err(format!("expected a value, found {other:?}")), } } fn array(&mut self) -> Result<Value, String> { self.next(); let mut items = Vec::new(); if matches!(self.peek(), Token::CloseBracket) { self.next(); return Ok(Value::Array(items)); } loop { let value = self.value()?; items.push(value); match self.next() { Token::Comma => continue, Token::CloseBracket => return Ok(Value::Array(items)), Token::End => return Err("document ended inside an array".to_string()), other => return Err(format!("expected a comma, found {other:?}")), } } } fn object(&mut self) -> Result<Value, String> { self.next(); let mut fields = Vec::new(); if matches!(self.peek(), Token::CloseBrace) { self.next(); return Ok(Value::Object(fields)); } loop { let key = match self.next() { Token::Text(text) => text, other => return Err(format!("expected a key, found {other:?}")), }; match self.next() { Token::Colon => {} other => return Err(format!("expected a colon, found {other:?}")), } let value = self.value()?; fields.push((key, value)); match self.next() { Token::Comma => continue, Token::CloseBrace => return Ok(Value::Object(fields)), Token::End => return Err("document ended inside an object".to_string()), other => return Err(format!("expected a comma, found {other:?}")), } } } } fn parse(text: &str) -> Result<Value, String> { let tokens = tokenize(text)?; let mut parser = Parser::new(tokens); let value = parser.value()?; match parser.peek() { Token::End => Ok(value), other => Err(format!("trailing token {other:?}")), } } impl Value { fn as_number(&self) -> Option<f64> { match self { Value::Number(number) => Some(*number), _ => None, } } fn as_text(&self) -> Option<&str> { match self { Value::Text(text) => Some(text), _ => None, } } fn as_bool(&self) -> Option<bool> { match self { Value::Bool(value) => Some(*value), _ => None, } } fn len(&self) -> usize { match self { Value::Array(items) => items.len(), Value::Object(fields) => fields.len(), _ => 0, } } } fn main() { let document = r#"["rust", 1.5, true]"#; let value = parse(document).unwrap(); if let Value::Array(items) = &value { println!("{:?}", items[0].as_text()); println!("{:?}", items[1].as_number()); println!("{:?}", items[2].as_bool()); } println!("{}", value.len()); }

rustc --edition 2024 kinds.rs
./kinds

Some("rust")
Some(1.5)
Some(true)
3

The enum also decides what equality means. Deriving PartialEq compares the variant and its data, so two documents written differently but parsed to the same tree are equal, and a number written as 1.0 equals one written as 1. That is the property the round trip of the third section uses, and it is structural rather than textual.

The array has three elements, and each accessor finds its own kind in its own position. The Option in the output is the guarantee: a caller that prints Some(1.5) knows the value was a number and a caller that gets None knows it was not, without a second check.

The accessors borrow rather than copy, so as_text returns &str and a string of a megabyte is not cloned to be read. The lesson on Option covers the combinators that the next section chains, and borrowing covers the reference that the accessor hands out.

Accessors that return an Option

Two more accessors make a whole document readable: get for a key of an object and index for a position of an array. Both return Option<&Value>, which is what makes them chain with and_then.

The program follows a path through a nested document and ends with a key that is not there. The last line prints the missing None and the number of fields, which is the same program that a configuration reader writes.

#[derive(Debug, Clone, PartialEq)] enum Token { OpenBrace, CloseBrace, OpenBracket, CloseBracket, Colon, Comma, Text(String), Number(f64), Word(String), End, } fn text_token(bytes: &[u8], position: &mut usize) -> Result<Token, String> { let start = *position; *position += 1; let mut value = String::new(); while *position < bytes.len() && bytes[*position] != b'"\' { let byte = bytes[*position]; if byte == b\'\\\' { *position += 1; match bytes.get(*position) { Some(b\'"') => value.push('"\'), Some(b\'\\\') => value.push(\'\\\'), Some(b\'n\') => value.push(\'\n\'), Some(b\'t\') => value.push(\'\t\'), Some(b\'u\') => { let digits = bytes.get(*position + 1..*position + 5) .ok_or_else(|| format!("short unicode escape at {}", *position))?; let hex = std::str::from_utf8(digits) .map_err(|_| format!("bad unicode escape at {}", *position))?; let code = u32::from_str_radix(hex, 16) .map_err(|_| format!("bad unicode escape at {}", *position))?; value.push(char::from_u32(code) .ok_or_else(|| format!("bad code point at {}", *position))?); *position += 4; } _ => return Err(format!("bad escape at {}", *position)), } } else { value.push(byte as char); } *position += 1; } if *position == bytes.len() { return Err(format!("unterminated text at {start}")); } *position += 1; Ok(Token::Text(value)) } fn number_token(bytes: &[u8], position: &mut usize) -> Result<Token, String> { let start = *position; while *position < bytes.len() && matches!(bytes[*position], b\'0\'..=b\'9\' | b\'-\' | b\'+\' | b\'.\' | b\'e\' | b\'E\') { *position += 1; } let slice = std::str::from_utf8(&bytes[start..*position]).unwrap(); match slice.parse() { Ok(value) => Ok(Token::Number(value)), Err(_) => Err(format!("bad number at {start}")), } } fn word_token(bytes: &[u8], position: &mut usize) -> Token { let start = *position; while *position < bytes.len() && bytes[*position].is_ascii_alphabetic() { *position += 1; } let slice = std::str::from_utf8(&bytes[start..*position]).unwrap(); Token::Word(slice.to_string()) } fn tokenize(text: &str) -> Result<Vec<Token>, String> { let bytes = text.as_bytes(); let mut position = 0; let mut tokens = Vec::new(); loop { while position < bytes.len() && bytes[position].is_ascii_whitespace() { position += 1; } if position == bytes.len() { tokens.push(Token::End); return Ok(tokens); } let start = position; let token = match bytes[position] { b\'{\' => { position += 1; Token::OpenBrace } b\'}\' => { position += 1; Token::CloseBrace } b\'[\' => { position += 1; Token::OpenBracket } b\']\' => { position += 1; Token::CloseBracket } b\':\' => { position += 1; Token::Colon } b\',\' => { position += 1; Token::Comma } b\'"' => text_token(bytes, &mut position)?, b'-' | b'0'..=b'9' => number_token(bytes, &mut position)?, b'a'..=b'z' => word_token(bytes, &mut position), other => return Err(format!("unexpected character \'{}\' at {start}", other as char)), }; tokens.push(token); } } #[derive(Debug, Clone, PartialEq)] enum Value { Null, Bool(bool), Number(f64), Text(String), Array(Vec<Value>), Object(Vec<(String, Value)>), } struct Parser { tokens: Vec<Token>, position: usize, } impl Parser { fn new(tokens: Vec<Token>) -> Parser { Parser { tokens, position: 0 } } fn peek(&self) -> &Token { &self.tokens[self.position] } fn next(&mut self) -> Token { let token = self.tokens[self.position].clone(); self.position += 1; token } fn value(&mut self) -> Result<Value, String> { match self.peek().clone() { Token::OpenBrace => self.object(), Token::OpenBracket => self.array(), Token::Text(text) => { self.next(); Ok(Value::Text(text)) } Token::Number(number) => { self.next(); Ok(Value::Number(number)) } Token::Word(word) => { self.next(); match word.as_str() { "true" => Ok(Value::Bool(true)), "false" => Ok(Value::Bool(false)), "null" => Ok(Value::Null), other => Err(format!("unknown word {other}")), } } other => Err(format!("expected a value, found {other:?}")), } } fn array(&mut self) -> Result<Value, String> { self.next(); let mut items = Vec::new(); if matches!(self.peek(), Token::CloseBracket) { self.next(); return Ok(Value::Array(items)); } loop { let value = self.value()?; items.push(value); match self.next() { Token::Comma => continue, Token::CloseBracket => return Ok(Value::Array(items)), Token::End => return Err("document ended inside an array".to_string()), other => return Err(format!("expected a comma, found {other:?}")), } } } fn object(&mut self) -> Result<Value, String> { self.next(); let mut fields = Vec::new(); if matches!(self.peek(), Token::CloseBrace) { self.next(); return Ok(Value::Object(fields)); } loop { let key = match self.next() { Token::Text(text) => text, other => return Err(format!("expected a key, found {other:?}")), }; match self.next() { Token::Colon => {} other => return Err(format!("expected a colon, found {other:?}")), } let value = self.value()?; fields.push((key, value)); match self.next() { Token::Comma => continue, Token::CloseBrace => return Ok(Value::Object(fields)), Token::End => return Err("document ended inside an object".to_string()), other => return Err(format!("expected a comma, found {other:?}")), } } } } fn parse(text: &str) -> Result<Value, String> { let tokens = tokenize(text)?; let mut parser = Parser::new(tokens); let value = parser.value()?; match parser.peek() { Token::End => Ok(value), other => Err(format!("trailing token {other:?}")), } } impl Value { fn as_number(&self) -> Option<f64> { match self { Value::Number(number) => Some(*number), _ => None, } } fn as_text(&self) -> Option<&str> { match self { Value::Text(text) => Some(text), _ => None, } } fn as_bool(&self) -> Option<bool> { match self { Value::Bool(value) => Some(*value), _ => None, } } fn get(&self, key: &str) -> Option<&Value> { match self { Value::Object(fields) => fields.iter().find(|(name, _)| name == key).map(|(_, value)| value), _ => None, } } fn index(&self, index: usize) -> Option<&Value> { match self { Value::Array(items) => items.get(index), _ => None, } } fn len(&self) -> usize { match self { Value::Array(items) => items.len(), Value::Object(fields) => fields.len(), _ => 0, } } } fn main() { let document = r#"{"name": "rust", "version": 1.5, "tags": ["fast", "safe"], "nested": {"ok": true}}"#; let value = parse(document).unwrap(); println!("{:?}", value.get("name").and_then(Value::as_text)); println!("{:?}", value.get("version").and_then(Value::as_number)); println!("{:?}", value.get("tags").and_then(|tags| tags.index(1)).and_then(Value::as_text)); println!("{:?}", value.get("nested").and_then(|nested| nested.get("ok")).and_then(Value::as_bool)); println!("{:?} {}", value.get("missing").and_then(Value::as_text), value.len()); }

rustc --edition 2024 access.rs
./access

Some("rust")
Some(1.5)
Some("safe")
Some(true)
None 4

Each line is one chain: the key, then the kind, and no branch in the caller. The chain in the third line goes through an array and an index, and the fourth through two objects. A missing key anywhere in the chain gives None at the end, which is why the style is called a safe navigation and why it replaces a pile of null checks in a C++ program.

The find behind get is linear in the number of fields, which is right for a configuration object with ten keys and wrong for a table with ten thousand rows. A reader that needs the fast lookup builds a HashMap from the pairs once, as the lesson on maps shows.

A writer in one function

A tree is only useful if it can be written back. The writer is the parser in reverse and fits in one recursive function. It appends to a String the caller owns, which keeps the function free of allocation policy: a file writer and a test both pass a buffer.

A text value has to escape the quote, the backslash, the newline and the tab, and the program's document contains all four so that the output shows them. The last line parses the output again and compares it with the original tree, which is the round trip.

#[derive(Debug, Clone, PartialEq)] enum Token { OpenBrace, CloseBrace, OpenBracket, CloseBracket, Colon, Comma, Text(String), Number(f64), Word(String), End, } fn text_token(bytes: &[u8], position: &mut usize) -> Result<Token, String> { let start = *position; *position += 1; let mut value = String::new(); while *position < bytes.len() && bytes[*position] != b'"\' { let byte = bytes[*position]; if byte == b\'\\\' { *position += 1; match bytes.get(*position) { Some(b\'"') => value.push('"\'), Some(b\'\\\') => value.push(\'\\\'), Some(b\'n\') => value.push(\'\n\'), Some(b\'t\') => value.push(\'\t\'), Some(b\'u\') => { let digits = bytes.get(*position + 1..*position + 5) .ok_or_else(|| format!("short unicode escape at {}", *position))?; let hex = std::str::from_utf8(digits) .map_err(|_| format!("bad unicode escape at {}", *position))?; let code = u32::from_str_radix(hex, 16) .map_err(|_| format!("bad unicode escape at {}", *position))?; value.push(char::from_u32(code) .ok_or_else(|| format!("bad code point at {}", *position))?); *position += 4; } _ => return Err(format!("bad escape at {}", *position)), } } else { value.push(byte as char); } *position += 1; } if *position == bytes.len() { return Err(format!("unterminated text at {start}")); } *position += 1; Ok(Token::Text(value)) } fn number_token(bytes: &[u8], position: &mut usize) -> Result<Token, String> { let start = *position; while *position < bytes.len() && matches!(bytes[*position], b\'0\'..=b\'9\' | b\'-\' | b\'+\' | b\'.\' | b\'e\' | b\'E\') { *position += 1; } let slice = std::str::from_utf8(&bytes[start..*position]).unwrap(); match slice.parse() { Ok(value) => Ok(Token::Number(value)), Err(_) => Err(format!("bad number at {start}")), } } fn word_token(bytes: &[u8], position: &mut usize) -> Token { let start = *position; while *position < bytes.len() && bytes[*position].is_ascii_alphabetic() { *position += 1; } let slice = std::str::from_utf8(&bytes[start..*position]).unwrap(); Token::Word(slice.to_string()) } fn tokenize(text: &str) -> Result<Vec<Token>, String> { let bytes = text.as_bytes(); let mut position = 0; let mut tokens = Vec::new(); loop { while position < bytes.len() && bytes[position].is_ascii_whitespace() { position += 1; } if position == bytes.len() { tokens.push(Token::End); return Ok(tokens); } let start = position; let token = match bytes[position] { b\'{\' => { position += 1; Token::OpenBrace } b\'}\' => { position += 1; Token::CloseBrace } b\'[\' => { position += 1; Token::OpenBracket } b\']\' => { position += 1; Token::CloseBracket } b\':\' => { position += 1; Token::Colon } b\',\' => { position += 1; Token::Comma } b\'"' => text_token(bytes, &mut position)?, b'-' | b'0'..=b'9' => number_token(bytes, &mut position)?, b'a'..=b'z' => word_token(bytes, &mut position), other => return Err(format!("unexpected character \'{}\' at {start}", other as char)), }; tokens.push(token); } } #[derive(Debug, Clone, PartialEq)] enum Value { Null, Bool(bool), Number(f64), Text(String), Array(Vec<Value>), Object(Vec<(String, Value)>), } struct Parser { tokens: Vec<Token>, position: usize, } impl Parser { fn new(tokens: Vec<Token>) -> Parser { Parser { tokens, position: 0 } } fn peek(&self) -> &Token { &self.tokens[self.position] } fn next(&mut self) -> Token { let token = self.tokens[self.position].clone(); self.position += 1; token } fn value(&mut self) -> Result<Value, String> { match self.peek().clone() { Token::OpenBrace => self.object(), Token::OpenBracket => self.array(), Token::Text(text) => { self.next(); Ok(Value::Text(text)) } Token::Number(number) => { self.next(); Ok(Value::Number(number)) } Token::Word(word) => { self.next(); match word.as_str() { "true" => Ok(Value::Bool(true)), "false" => Ok(Value::Bool(false)), "null" => Ok(Value::Null), other => Err(format!("unknown word {other}")), } } other => Err(format!("expected a value, found {other:?}")), } } fn array(&mut self) -> Result<Value, String> { self.next(); let mut items = Vec::new(); if matches!(self.peek(), Token::CloseBracket) { self.next(); return Ok(Value::Array(items)); } loop { let value = self.value()?; items.push(value); match self.next() { Token::Comma => continue, Token::CloseBracket => return Ok(Value::Array(items)), Token::End => return Err("document ended inside an array".to_string()), other => return Err(format!("expected a comma, found {other:?}")), } } } fn object(&mut self) -> Result<Value, String> { self.next(); let mut fields = Vec::new(); if matches!(self.peek(), Token::CloseBrace) { self.next(); return Ok(Value::Object(fields)); } loop { let key = match self.next() { Token::Text(text) => text, other => return Err(format!("expected a key, found {other:?}")), }; match self.next() { Token::Colon => {} other => return Err(format!("expected a colon, found {other:?}")), } let value = self.value()?; fields.push((key, value)); match self.next() { Token::Comma => continue, Token::CloseBrace => return Ok(Value::Object(fields)), Token::End => return Err("document ended inside an object".to_string()), other => return Err(format!("expected a comma, found {other:?}")), } } } } fn parse(text: &str) -> Result<Value, String> { let tokens = tokenize(text)?; let mut parser = Parser::new(tokens); let value = parser.value()?; match parser.peek() { Token::End => Ok(value), other => Err(format!("trailing token {other:?}")), } } impl Value { fn as_number(&self) -> Option<f64> { match self { Value::Number(number) => Some(*number), _ => None, } } fn as_text(&self) -> Option<&str> { match self { Value::Text(text) => Some(text), _ => None, } } fn as_bool(&self) -> Option<bool> { match self { Value::Bool(value) => Some(*value), _ => None, } } fn get(&self, key: &str) -> Option<&Value> { match self { Value::Object(fields) => fields.iter().find(|(name, _)| name == key).map(|(_, value)| value), _ => None, } } fn index(&self, index: usize) -> Option<&Value> { match self { Value::Array(items) => items.get(index), _ => None, } } fn len(&self) -> usize { match self { Value::Array(items) => items.len(), Value::Object(fields) => fields.len(), _ => 0, } } } fn write(value: &Value, out: &mut String) { match value { Value::Null => out.push_str("null"), Value::Bool(value) => out.push_str(if *value { "true" } else { "false" }), Value::Number(number) => out.push_str(&format!("{number}")), Value::Text(text) => { out.push('"\'); for c in text.chars() { match c { \'"' => out.push_str("\\\""), \'\\\' => out.push_str("\\\\"), \'\n\' => out.push_str("\\n"), \'\t\' => out.push_str("\\t"), other => out.push(other), } } out.push(\'"'); } Value::Array(items) => { out.push('['); for (index, item) in items.iter().enumerate() { if index > 0 { out.push(','); } write(item, out); } out.push(']'); } Value::Object(fields) => { out.push('{'); for (index, (key, item)) in fields.iter().enumerate() { if index > 0 { out.push(','); } out.push('"\'); out.push_str(key); out.push_str("\":"); write(item, out); } out.push('}'); } } } fn main() { let document = r#"{"name": "rust", "version": 1.5, "stable": true, "tags": ["fast", "safe"], "note": "a \"b\"\\nc"}"#; let value = parse(document).unwrap(); println!("{:?}", value.get("version").and_then(Value::as_number)); println!("{:?}", value.get("tags").and_then(|tags| tags.index(0)).and_then(Value::as_text)); println!("{:?} {}", value.get("stable").and_then(Value::as_bool), value.len()); let mut out = String::new(); write(&value, &mut out); println!("{out}"); println!("{}", parse(&out).unwrap() == value); }

rustc --edition 2024 writer.rs
./writer

Some(1.5)
Some("fast")
Some(true) 5
{"name":"rust","version":1.5,"stable":true,"tags":["fast","safe"],"note":"a \"b\"\\nc"}
true

An output buffer that is reused across many documents is the reason the function appends rather than returns, and it is the same reason a serializer in a server holds one buffer per thread. The compact form has no spaces, which is what a program sends over a socket and what a large file stores. The escaped quote and backslash survive the trip out and back, which is what the last line proves: the parsed copy is equal to the original, and equality is derived on the enum. The lesson on derive covers the PartialEq that makes the comparison possible.

A number is written with the plain formatting of f64, so 1.5 stays and 1.0 becomes 1, which JSON accepts. The writer of the next lesson adds indentation and the \u escape for control characters.

The alternative that is not there

The habit from a union is to read the field that belongs to the variant the program expects. Rust refuses the local binding that would do it, because the value might be another variant, and the message lists the variants that are not covered.

#[derive(Debug)] enum Value { Null, Bool(bool), Number(f64), } fn main() { let value = Value::Bool(true); let Value::Number(number) = value; println!("{number}"); }

The program below is the same mistake in three lines of main, and the message lists the variants that a let cannot rule out.

rustc --edition 2024 mistake.rs

error[E0005]: refutable pattern in local binding
--> mistake.rs:10:9
|
10 | let Value::Number(number) = value;
| ^^^^^^^^^^^^^^^^^^^^^ patterns `Value::Null` and `Value::Bool(_)` not covered
|
= note: `let` bindings require an "irrefutable pattern", like a `struct` or an `enum` with only one variant
= note: for more information, visit https://doc.rust-lang.org/book/ch19-02-refutability.html
note: `Value` defined here
--> mistake.rs:2:6
|
2 | enum Value {
| ^^^^^
3 | Null,
| ---- not covered
4 | Bool(bool),
| ---- not covered
= note: the matched value is of type `Value`
help: you might want to use `let...else` to handle the variants that aren't matched
|
10 | let Value::Number(number) = value else { todo!() };
| ++++++++++++++++

error: aborting due to 1 previous error

For more information about this error, try `rustc --explain E0005`.

The fix is the accessor: value.as_number() returns Option<f64> and the caller decides what a missing number means. A match or let ... else is the right tool when the program really needs every variant, and let ... else is how the compiler suggests the refutable binding with a branch for the rest.

The next lesson makes the output readable, and the tests of lesson 127 compare the writer with the parser over a table of documents.

Exercises

  1. Add as_array and as_object and use them to print the number of fields of the nested object.
  2. Add a path helper that takes a slice of keys and follows them with one for loop.
  3. Write a total function that sums every number in a document, whatever the nesting.

Questions

A new set of questions in a new order every time. Choose one answer for each, then check.

  1. Why does an accessor like as_text return Option<&str>?

    Answer

    Because the value may be another variant, and a borrowed string avoids a clone

    The Option says whether the kind matches, and the reference says the tree keeps the text.

  2. What does a chain of get and and_then do when a key is missing?

    Answer

    It returns None at the end of the chain

    Each step returns Option, and and_then short-circuits on None.

  3. Why does the writer take a &mut String instead of returning one?

    Answer

    The caller chooses the buffer, so the function makes no allocation policy of its own

    A file writer and a test can pass their own buffer, and the recursion appends to it.

  4. What proves a round trip in this lesson?

    Answer

    The written text, parsed again, equals the original tree

    Equality of the two trees is the strongest check the type allows.

  5. Why does the writer escape a backslash and a quote?

    Answer

    Without the escapes the text would end early or mean something else

    A quote ends a string and a backslash starts an escape, so both must be written twice.

  6. What does this program print? #[derive(Debug)] enum Value { Number(f64), Text(String), Null, } fn as_number(value: &Value) -> Option<f64> { match value { Value::Number(number) => Some(*number), _ => None, } } fn main() { let values = [Value::Number(2.5), Value::Text("x".to_string()), Value::Null]; let total: f64 = values.iter().filter_map(as_number).sum(); println!("{}", total); }

    Answer

    2.5

    Only the number contributes, and the other two values are filtered out.

  7. What does this program print? #[derive(Debug)] enum Value { Number(f64), Text(String), Object(Vec<(String, Value)>), } fn get<'a>(value: &'a Value, key: &str) -> Option<&'a Value> { match value { Value::Object(fields) => fields.iter().find(|(name, _)| name == key).map(|(_, value)| value), _ => None, } } fn main() { let value = Value::Object(vec![ ("a".to_string(), Value::Number(1.0)), ("b".to_string(), Value::Number(2.0)), ]); let first = get(&value, "a"); let missing = get(&value, "c"); println!("{:?} {}", first.is_some(), missing.is_none()); }

    Answer

    true true

    The first key exists and the second does not, so both checks are true.

  8. What does this program print? ``` fn main() { let mut out = String::new(); for (index, item) in ["a", "b", "c"].iter().enumerate() { if index > 0 { out.push(

    Answer

    a,b,c

    The comma goes before every item except the first, which is the separator pattern.

  9. What does this program print? #[derive(Debug, PartialEq)] enum Value { Null, Bool(bool), Number(f64), Text(String), } fn main() { let original = Value::Text("a \"b\"".to_string()); let mut out = String::new(); match &original { Value::Text(text) => { out.push('"'); for c in text.chars() { if c == '"' || c == '\\' { out.push('\\'); } out.push(c); } out.push('"'); } _ => out.push_str("null"), } println!("{}", out); }

    Answer

    "a \"b\""

    The escaped quote inside the text is written with a backslash in front of it.

  10. What is the first line of the compiler output for this program? enum Value { Null, Bool(bool), Number(f64), } fn main() { let value = Value::Null; let Value::Number(number) = value; println!("{number}"); }

    Answer

    error[E0005]: refutable pattern in local binding

    A let binding needs a pattern that always matches, and a variant pattern does not.

The code on this page was built with rustc 1.96.1 and run on Alpine Linux 3.24.3 (x86_64) in a Docker container.

Article author: Arthur Isaev