Loading page…
Rust walkthroughs
Loading page…
nom::combinator::map_res enable error type transformation in parser combinators?The nom::combinator::map_res combinator bridges the gap between raw parsing results and application-specific error types. It applies a transformation function to a successful parse result, but crucially, allows that function to return a Result—if the function returns Err, the entire parser fails with that error. This enables enriching parse errors with context, validating parsed values during parsing, converting between error types, and implementing domain-specific validation that integrates cleanly with nom's error handling infrastructure. Unlike simple map, which only transforms successful values, map_res can fail the parser based on the transformation's outcome.
use nom::{IResult, combinator::map_res, bytes::complete::tag};
fn parse_digit(input: &str) -> IResult<&str, u8> {
// map_res applies a function that returns Result
map_res(
tag("42"),
|s: &str| s.parse::<u8>()
)(input)
}
#[test]
fn test_digit() {
assert_eq!(parse_digit("42abc"), Ok(("abc", 42)));
// If parse fails, the parser fails
assert!(parse_digit("43abc").is_err());
}map_res takes a parser and a fallible function, applying the function to successful parse results.
use nom::{IResult, error::{Error, ErrorKind}, combinator::map_res, bytes::complete::take_while};
#[derive(Debug)]
pub enum AppError {
ParseError(String),
InvalidValue(String),
}
fn parse_number(input: &str) -> IResult<&str, i32, AppError> {
map_res(
take_while(|c: char| c.is_ascii_digit()),
|digits: &str| {
digits.parse::<i32>().map_err(|e| {
AppError::InvalidValue(format!("Failed to parse '{}': {}", digits, e))
})
}
)(input)
}
// The error type transforms from nom's default to AppErrorWhen the function returns Err, that error becomes the parser's error.
use nom::{IResult, combinator::map_res, bytes::complete::take_while1};
fn parse_age(input: &str) -> IResult<&str, u8, String> {
map_res(
take_while1(|c: char| c.is_ascii_digit()),
|digits: &str| {
let age: u8 = digits.parse().map_err(|_| "Invalid number".to_string())?;
if age > 150 {
Err(format!("Age {} is unreasonably high", age))
} else if age == 0 {
Err("Age cannot be zero".to_string())
} else {
Ok(age)
}
}
)(input)
}
#[test]
fn test_age_validation() {
assert_eq!(parse_age("25"), Ok(("", 25)));
assert!(parse_age("200").is_err()); // Too high
assert!(parse_age("0").is_err()); // Zero not allowed
}Validation logic can fail the parser with meaningful error messages.
use nom::{IResult, combinator::map_res, character::complete::digit1};
#[derive(Debug)]
pub struct ContextualError {
pub message: String,
pub input: String,
pub context: String,
}
fn parse_port(input: &str) -> IResult<&str, u16, ContextualError> {
map_res(
digit1,
|digits: &str| {
let port: u16 = digits.parse().map_err(|_| ContextualError {
message: "Not a valid port number".to_string(),
input: digits.to_string(),
context: "parsing port".to_string(),
})?;
if port == 0 {
Err(ContextualError {
message: "Port cannot be zero".to_string(),
input: digits.to_string(),
context: "validating port range".to_string(),
})
} else {
Ok(port)
}
}
)(input)
}map_res enables rich error types with context beyond what nom provides by default.
use nom::{IResult, combinator::map_res, bytes::complete::take_while, sequence::tuple};
fn parse_username(input: &str) -> IResult<&str, String, String> {
map_res(
take_while(|c: char| c.is_alphanumeric() || c == '_'),
|s: &str| {
if s.len() < 3 {
Err(format!("Username '{}' too short (minimum 3 characters)", s))
} else if s.len() > 20 {
Err(format!("Username '{}' too long (maximum 20 characters)", s))
} else if s.starts_with('_') {
Err(format!("Username '{}' cannot start with underscore", s))
} else {
Ok(s.to_string())
}
}
)(input)
}
fn parse_user(input: &str) -> IResult<&str, (String, u8), String> {
let (remaining, (name, _, age)) = tuple((
parse_username,
nom::character::complete::space1,
map_res(
nom::character::complete::digit1,
|digits: &str| {
let age: u8 = digits.parse().map_err(|_| "Invalid age".to_string())?;
if age < 13 {
Err(format!("User must be at least 13 years old, got {}", age))
} else if age > 120 {
Err(format!("Age {} is unrealistic", age))
} else {
Ok(age)
}
}
)
))(input)?;
Ok((remaining, (name, age)))
}Multiple validations compose naturally through map_res.
use nom::{IResult, error::Error, combinator::map_res, bytes::complete::tag};
// Define application error types
#[derive(Debug)]
pub enum ParseError {
NomError(nom::error::ErrorKind),
InvalidSyntax(String),
OutOfRange { value: i64, min: i64, max: i64 },
}
fn parse_bounded(input: &str) -> IResult<&str, i32, ParseError> {
map_res(
nom::character::complete::i64,
|value: i64| {
const MIN: i64 = -100;
const MAX: i64 = 100;
if value < MIN {
Err(ParseError::OutOfRange { value, min: MIN, max: MAX })
} else if value > MAX {
Err(ParseError::OutOfRange { value, min: MIN, max: MAX })
} else {
Ok(value as i32)
}
}
)(input)
}
// Converts i64 parse errors to application error typemap_res translates between nom's internal errors and application-specific types.
use nom::{IResult, combinator::map_res, character::complete::{digit1, space0}};
#[derive(Debug, Clone)]
pub struct JsonPathError {
pub position: usize,
pub expected: String,
pub found: String,
}
fn parse_index(input: &str) -> IResult<&str, usize, JsonPathError> {
let position = input.len();
map_res(
digit1,
|digits: &str| {
digits.parse::<usize>().map_err(|_| JsonPathError {
position,
expected: "array index".to_string(),
found: digits.to_string(),
})
}
)(input)
}
fn parse_array_access(input: &str) -> IResult<&str, Vec<usize>, JsonPathError> {
let mut indices = Vec::new();
let mut remaining = input;
loop {
let (rem, _) = space0(remaining)?;
let (rem, _) = nom::bytes::complete::tag("[")(rem)?;
let (rem, idx) = parse_index(rem)?;
let (rem, _) = nom::bytes::complete::tag("]")(rem)?;
indices.push(idx);
remaining = rem;
if !remaining.starts_with("[") {
break;
}
}
Ok((remaining, indices))
}Custom error types enable domain-specific error handling throughout the parser.
use nom::{IResult, combinator::{map, map_res}};
// map: Always succeeds, transforms value
fn parse_with_map(input: &str) -> IResult<&str, String> {
map(
nom::character::complete::alpha1,
|s: &str| s.to_uppercase()
)(input)
}
// map_res: Can fail, transforms value with validation
fn parse_with_map_res(input: &str) -> IResult<&str, String, String> {
map_res(
nom::character::complete::alpha1,
|s: &str| {
if s.len() > 10 {
Err("String too long".to_string())
} else {
Ok(s.to_uppercase())
}
}
)(input)
}
#[test]
fn test_comparison() {
// map always succeeds with transformation
assert!(parse_with_map("averylongstring").is_ok());
// map_res can fail
assert!(parse_with_map_res("averylongstring").is_err());
assert!(parse_with_map_res("short").is_ok());
}Use map when transformation always succeeds; use map_res when it might fail.
use nom::{IResult, combinator::{map_res, alt}, character::complete::digit1};
#[derive(Debug)]
pub enum FieldError {
InvalidFormat(String),
ValueTooLarge { max: usize },
}
fn parse_optional_size(input: &str) -> IResult<&str, Option<usize>, FieldError> {
alt((
// "unlimited" maps to None
map_res(
nom::bytes::complete::tag("unlimited"),
|_| Ok::<_, FieldError>(None)
),
// Number maps to Some(size)
map_res(
digit1,
|digits: &str| {
let size: usize = digits.parse().map_err(|_| {
FieldError::InvalidFormat(digits.to_string())
})?;
if size > 1_000_000 {
Err(FieldError::ValueTooLarge { max: 1_000_000 })
} else {
Ok(Some(size))
}
}
)
))(input)
}map_res works within alt to try multiple parsing strategies with validation.
use nom::{IResult, combinator::map_res, bytes::complete::take_while1, character::complete::char};
#[derive(Debug, Clone)]
pub enum ConfigValue {
Integer(i64),
Float(f64),
Boolean(bool),
String(String),
}
#[derive(Debug)]
pub struct ConfigParseError {
pub field: String,
pub reason: String,
}
fn parse_config_value(input: &str) -> IResult<&str, ConfigValue, ConfigParseError> {
// Try boolean first
let bool_result = map_res(
nom::bytes::complete::alt((
nom::bytes::complete::tag("true"),
nom::bytes::complete::tag("false"),
)),
|s: &str| {
match s {
"true" => Ok(ConfigValue::Boolean(true)),
"false" => Ok(ConfigValue::Boolean(false)),
_ => Err(ConfigParseError {
field: s.to_string(),
reason: "Invalid boolean".to_string(),
}),
}
}
)(input);
if bool_result.is_ok() {
return bool_result;
}
// Try integer
let int_result = map_res(
nom::character::complete::i64,
|n| Ok::<_, ConfigParseError>(ConfigValue::Integer(n))
)(input);
if int_result.is_ok() {
return int_result;
}
// Fall back to string
map_res(
take_while1(|_| true),
|s: &str| Ok::<_, ConfigParseError>(ConfigValue::String(s.to_string()))
)(input)
}
fn parse_config_line(input: &str) -> IResult<&str, (String, ConfigValue), ConfigParseError> {
let (remaining, key) = take_while1(|c: char| c.is_alphanumeric() || c == '_')(input)
.map_err(|_| ConfigParseError {
field: "".to_string(),
reason: "Expected key".to_string(),
})?;
let (remaining, _) = char('=').parse(remaining)
.map_err(|_| ConfigParseError {
field: key.to_string(),
reason: "Expected '=' after key".to_string(),
})?;
let (remaining, value) = parse_config_value(remaining)?;
Ok((remaining, (key.to_string(), value)))
}Configuration parsing often needs validation that integrates with nom's error flow.
use nom::{IResult, combinator::map_res, sequence::separated_pair, character::complete::digit1};
#[derive(Debug)]
pub struct AddrError {
pub message: String,
}
fn parse_u8(input: &str) -> IResult<&str, u8, AddrError> {
map_res(
digit1,
|digits: &str| {
digits.parse::<u8>().map_err(|_| AddrError {
message: format!("'{}' is not a valid u8", digits),
})
}
)(input)
}
fn parse_ipv4_octet(input: &str) -> IResult<&str, u8, AddrError> {
map_res(
parse_u8,
|value| {
if value > 255 {
Err(AddrError {
message: format!("{} exceeds IPv4 octet maximum (255)", value),
})
} else {
Ok(value)
}
}
)(input)
}
fn parse_ipv4(input: &str) -> IResult<&str, [u8; 4], AddrError> {
let (remaining, (a, _, b, _, c, _, d)) = nom::sequence::tuple((
parse_ipv4_octet,
nom::character::complete::char('.'),
parse_ipv4_octet,
nom::character::complete::char('.'),
parse_ipv4_octet,
nom::character::complete::char('.'),
parse_ipv4_octet,
))(input)?;
Ok((remaining, [a, b, c, d]))
}
fn parse_port(input: &str) -> IResult<&str, u16, AddrError> {
map_res(
digit1,
|digits: &str| {
let port: u16 = digits.parse().map_err(|_| AddrError {
message: format!("'{}' is not a valid port", digits),
})?;
if port == 0 {
Err(AddrError {
message: "Port 0 is not allowed".to_string(),
})
} else {
Ok(port)
}
}
)(input)
}
fn parse_socket_addr(input: &str) -> IResult<&str, ([u8; 4], u16), AddrError> {
let (remaining, (ip, _, port)) = nom::sequence::tuple((
parse_ipv4,
nom::character::complete::char(':'),
parse_port,
))(input)?;
Ok((remaining, (ip, port)))
}Network address parsing requires validation at multiple levels.
use nom::{IResult, combinator::map_res, character::complete::{space0, space1}};
#[derive(Debug)]
pub struct FormattedError {
pub message: String,
pub location: Option<String>,
}
impl std::fmt::Display for FormattedError {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match &self.location {
Some(loc) => write!(f, "{} at {}", self.message, loc),
None => write!(f, "{}", self.message),
}
}
}
fn parse_token(name: &'static str) -> impl Fn(&str) -> IResult<&str, &str, FormattedError> {
move |input: &str| {
let (remaining, token) = nom::bytes::complete::is_a("abcdefghijklmnopqrstuvwxyz")(input)
.map_err(|_| FormattedError {
message: format!("Expected {}", name),
location: None,
})?;
map_res(
nom::combinator::success(token),
|t: &str| {
if t.len() < 2 {
Err(FormattedError {
message: format!("{} too short", name),
location: Some(format!("position {}", input.len() - remaining.len())),
})
} else {
Ok(t)
}
}
)(remaining)
}
}map_res enables precise error locations and formatted messages.
use nom::{IResult, combinator::{map_res, map_parser}, character::complete::digit1};
#[derive(Debug)]
pub enum ParserError {
InvalidDigit(String),
OutOfRange(i64),
Custom(String),
}
fn parse_hex_digit(input: &str) -> IResult<&str, u8, ParserError> {
map_res(
nom::bytes::complete::take_while_m_n(1, 2, |c: char| c.is_ascii_hexdigit()),
|hex: &str| {
u8::from_str_radix(hex, 16).map_err(|_| {
ParserError::InvalidDigit(hex.to_string())
})
}
)(input)
}
fn parse_validated_number(input: &str) -> IResult<&str, i64, ParserError> {
map_res(
digit1,
|digits: &str| {
let value: i64 = digits.parse().map_err(|_| {
ParserError::Custom(format!("'{}' is not a valid number", digits))
})?;
if value < 0 {
Err(ParserError::OutOfRange(value))
} else if value > 1000 {
Err(ParserError::OutOfRange(value))
} else {
Ok(value)
}
}
)(input)
}map_res integrates with other combinators for complex parsing flows.
Key behaviors:
| Behavior | Description |
|----------|-------------|
| Applies function | Transforms successful parse results |
| Can fail | Returns Err if function returns Err |
| Error transformation | Converts function errors to parser errors |
| Validation | Enables in-parser value validation |
When to use:
| Use case | Example |
|----------|---------|
| String to number | map_res(digit1, \|s\| s.parse::<i32>()) |
| Range validation | Check bounds after parsing |
| Custom errors | Convert to domain error types |
| Multi-step validation | Parse then validate |
Comparison with related combinators:
| Combinator | Can fail? | Transforms error? |
|------------|-----------|-------------------|
| map | No | N/A |
| map_res | Yes | Yes (from function) |
| map_parser | Yes | No |
| verify | Yes | No |
Key insight: nom::combinator::map_res is the primary bridge between nom's parsing infrastructure and application-specific validation and error handling. It takes a parser and a fallible transformation function, applying the function to successful parse results. If the function returns Err, the parser fails with that error, enabling validation failures to propagate through nom's error handling system. This is essential for domain-specific validation (like range checks on parsed numbers), converting between error types (from nom's generic errors to application-specific types), adding context to errors (field names, positions, expectations), and implementing multi-stage parsing where later stages validate earlier results. Unlike map, which only transforms successful values, map_res can fail the parser based on validation logic, making it the key combinator for integrating business rules into the parsing process itself.