How does nom::combinator::map_opt combine parsing with validation logic?
map_opt applies a parsing function and then validates the result with an Option-returning closure, succeeding only when both parsing and validation succeed. It combines parsing and validation into a single combinator, returning None from the validation closure causes the parser to fail.
Parsing and Validation in nom
use nom::{IResult, bytes::complete::tag, combinator::map};
fn basic_parsing() {
// Standard parsing: parse bytes, return result
let input = "hello world";
let result: IResult<&str, &str> = tag("hello")(input);
// Result: Ok((" world", "hello"))
// Sometimes we need to validate the parsed result
// - Is the number in range?
// - Does the string match a pattern?
// - Is the value semantically valid?
}Parsing extracts structure; validation checks whether the result is acceptable.
The map_opt Combinator
use nom::{IResult, combinator::map_opt, bytes::complete::tag};
fn map_opt_basic() {
// map_opt signature:
// fn map_opt<I, O, E, F, G>(f: F, g: G) -> impl Fn(I) -> IResult<I, O, E>
// where
// F: Fn(I) -> IResult<I, O, E>,
// G: Fn(O) -> Option<O2>,
// Parse "true" or "false" and validate the result
let parser = map_opt(
tag("true"), // Parser
|result: &str| -> Option<bool> {
if result == "true" {
Some(true)
} else {
None // Validation fails
}
}
);
let result = parser("true");
// Ok(("", true))
}map_opt chains a parser and a validation function, succeeding only when both succeed.
Validation Through Option
use nom::{IResult, combinator::map_opt, bytes::complete::take_while};
use std::str::FromStr;
fn validation_via_option() {
// Parse a number and ensure it's positive
let positive_number = map_opt(
take_while(|c: char| c.is_ascii_digit()),
|digits: &str| -> Option<i32> {
let n = digits.parse::<i32>().ok()?;
if n > 0 { Some(n) } else { None }
}
);
// Success case: positive number
let result = positive_number("123abc");
// Ok(("abc", 123))
// Failure case: zero or empty
let result = positive_number("0abc");
// Err(Err::Error(...)) - validation failed
let result = positive_number("abc");
// Err(Err::Error(...)) - parsing failed (no digits)
}Returning None from the validation function causes the parser to fail.
vs map: Adding Validation
use nom::{IResult, combinator::{map, map_opt}};
fn map_vs_map_opt() {
// map: transforms the result unconditionally
let parser_map = map(
|i: &str| Ok(("", i.parse::<i32>().unwrap())),
|n: i32| n * 2,
);
// Always succeeds if parser succeeds
// map_opt: transforms but can fail
let parser_map_opt = map_opt(
|i: &str| Ok(("", i.parse::<i32>().unwrap())),
|n: i32| {
if n > 0 { Some(n * 2) } else { None }
}
);
// Fails if validation returns None
// Key difference:
// - map: Result always transformed
// - map_opt: Result can be rejected
}map always transforms; map_opt can reject via None.
Practical Example: Bounded Integer
use nom::{IResult, bytes::complete::digit1, combinator::map_opt, character::complete::char};
fn bounded_integer() {
// Parse integer between 1 and 100
let port_parser = map_opt(
digit1,
|digits: &str| -> Option<u16> {
let n = digits.parse().ok()?;
if n >= 1 && n <= 100 {
Some(n)
} else {
None
}
}
);
let result = port_parser("80");
// Ok(("", 80))
let result = port_parser("150");
// Err(...) - out of range
let result = port_parser("0");
// Err(...) - out of range
let result = port_parser("abc");
// Err(...) - parsing failed
}Validation ensures the parsed value meets constraints.
Parsing Enums with Validation
use nom::{IResult, bytes::complete::alpha1, combinator::map_opt};
#[derive(Debug, PartialEq)]
enum Color {
Red,
Green,
Blue,
}
fn parse_color() -> impl Fn(&str) -> IResult<&str, Color> {
map_opt(
alpha1,
|name: &str| -> Option<Color> {
match name.to_lowercase().as_str() {
"red" => Some(Color::Red),
"green" => Some(Color::Green),
"blue" => Some(Color::Blue),
_ => None, // Unknown color
}
}
)
}
fn color_example() {
let parser = parse_color();
let result = parser("red");
// Ok(("", Color::Red))
let result = parser("RED");
// Ok(("", Color::Red)) - case insensitive
let result = parser("yellow");
// Err(...) - not a valid color
}map_opt validates that parsed strings match known enum variants.
Combining Multiple Constraints
use nom::{IResult, bytes::complete::take_while1, combinator::map_opt};
fn username_parser() {
// Parse username: 3-20 chars, alphanumeric and underscore only
let username = map_opt(
take_while1(|c: char| c.is_alphanumeric() || c == '_'),
|s: &str| -> Option<String> {
let len = s.len();
if len >= 3 && len <= 20 {
Some(s.to_string())
} else {
None
}
}
);
let result = username("valid_user");
// Ok(("", "valid_user"))
let result = username("ab"); // Too short
// Err(...)
let result = username("this_username_is_way_too_long");
// Err(...) - but first 20 chars parsed, then validation fails
}Multiple validation constraints combine in the Option closure.
Error Handling
use nom::{IResult, error::{Error, ErrorKind}, combinator::map_opt, bytes::complete::take_while};
fn error_handling() {
// When validation fails, nom returns an error
let parser = map_opt(
take_while(|c: char| c.is_ascii_digit()),
|digits: &str| -> Option<i32> {
digits.parse().ok().filter(|&n| n > 0)
}
);
match parser("0abc") {
Ok((remaining, value)) => {
println!("Parsed: {}, remaining: {}", value, remaining);
}
Err(nom::Err::Error(e)) => {
// Validation failed
println!("Error: {:?}", e);
}
Err(nom::Err::Failure(e)) => {
// Fatal error (if using cut)
println!("Failure: {:?}", e);
}
Err(nom::Err::Incomplete(_)) => {
// Need more input
println!("Incomplete");
}
}
}Validation failure produces a nom error, allowing backtracking or error messages.
Combining with Other Parsers
use nom::{
IResult,
bytes::complete::tag,
character::complete::digit1,
sequence::preceded,
combinator::map_opt,
};
fn combined_parser() {
// Parse "port:N" where N is 1-65535
let port = map_opt(
preceded(tag("port:"), digit1),
|digits: &str| -> Option<u16> {
let n = digits.parse().ok()?;
if n > 0 && n <= 65535 {
Some(n)
} else {
None
}
}
);
let result = port("port:8080");
// Ok(("", 8080))
let result = port("port:0");
// Err(...) - validation failed (port must be > 0)
let result = port("port:70000");
// Err(...) - validation failed (port must be <= 65535)
}map_opt integrates with other combinators seamlessly.
Transforming Types with Validation
use nom::{IResult, bytes::complete::take_while1, combinator::map_opt};
use std::net::{Ipv4Addr, Ipv6Addr, IpAddr};
fn ip_address_parser() {
// Parse IPv4 address and validate
let ipv4 = map_opt(
take_while1(|c: char| c.is_ascii_digit() || c == '.'),
|s: &str| -> Option<IpAddr> {
s.parse::<Ipv4Addr>()
.ok()
.map(IpAddr::V4)
}
);
let result = ipv4("192.168.1.1");
// Ok(("", 192.168.1.1))
let result = ipv4("256.1.1.1");
// Err(...) - invalid octet
let result = ipv4("192.168.1");
// Err(...) - incomplete address
}map_opt parses strings and validates them as structured types.
vs filter for Validation
use nom::{IResult, bytes::complete::digit1, combinator::{map_opt, map, filter}};
fn map_opt_vs_filter() {
// filter: keeps result if predicate is true
let filter_parser = filter(
digit1,
|digits: &str| digits.len() <= 3,
);
// Returns the original parsed value if predicate passes
// map_opt: transforms and validates
let map_opt_parser = map_opt(
digit1,
|digits: &str| -> Option<i32> {
if digits.len() <= 3 {
digits.parse().ok()
} else {
None
}
}
);
// Returns transformed value if validation passes
// Key differences:
// - filter: predicate returns bool, output type unchanged
// - map_opt: function returns Option<T>, allows type transformation
}Use filter when you only need to accept/reject; use map_opt when transforming types.
Nested Validation
use nom::{IResult, bytes::complete::digit1, combinator::map_opt};
fn nested_validation() {
// Parse coordinate, validate range, then validate it's in allowed set
let coordinate = map_opt(
digit1,
|digits: &str| -> Option<i32> {
let n = digits.parse().ok()?;
// First validation: range
if n < -90 || n > 90 {
return None;
}
// Second validation: allowed values
let allowed = [0, 30, 45, 60, 90];
if allowed.contains(&n.abs()) {
Some(n)
} else {
None
}
}
);
}Multiple validation steps compose naturally within the closure.
Chaining Validations
use nom::{IResult, combinator::map_opt, bytes::complete::take_while1};
fn chained_validation() {
// Parse and validate in stages
let parse_and_validate = |input: &str| -> IResult<&str, String> {
// First: parse identifier
let (remaining, ident) = take_while1(|c: char| c.is_alphanumeric() || c == '_')(input)?;
// Second: validate and transform
let (remaining, validated) = map_opt(
|i: &str| Ok((i, i)),
|s: &str| -> Option<String> {
// Must start with letter
if !s.chars().next()?.is_alphabetic() {
return None;
}
// Must not be reserved keyword
let keywords = ["if", "else", "for", "while"];
if keywords.contains(&s) {
return None;
}
Some(s.to_string())
}
)(ident)?;
Ok((remaining, validated))
};
}Complex validation logic lives in the closure, keeping the parser structure clean.
Real-World Example: HTTP Status Code
use nom::{IResult, bytes::complete::digit1, combinator::map_opt};
#[derive(Debug, Clone, Copy)]
enum HttpStatus {
Informational(u16),
Success(u16),
Redirection(u16),
ClientError(u16),
ServerError(u16),
}
fn http_status() -> impl Fn(&str) -> IResult<&str, HttpStatus> {
map_opt(
digit1,
|digits: &str| -> Option<HttpStatus> {
let code: u16 = digits.parse().ok()?;
// Validate and categorize
match code {
100..=199 => Some(HttpStatus::Informational(code)),
200..=299 => Some(HttpStatus::Success(code)),
300..=399 => Some(HttpStatus::Redirection(code)),
400..=499 => Some(HttpStatus::ClientError(code)),
500..=599 => Some(HttpStatus::ServerError(code)),
_ => None, // Invalid HTTP status
}
}
)
}
fn status_example() {
let parser = http_status();
let result = parser("200");
// Ok(("", HttpStatus::Success(200)))
let result = parser("404");
// Ok(("", HttpStatus::ClientError(404)))
let result = parser("600");
// Err(...) - not a valid HTTP status
}map_opt parses and validates semantic correctness in one step.
Performance Considerations
use nom::{IResult, combinator::map_opt, bytes::complete::take_while};
fn performance() {
// map_opt is efficient: it backtracks on None
// Alternative approaches:
// 1. Parse then check separately (more code, same effect)
let parse_then_check = |input: &str| -> IResult<&str, i32> {
let (remaining, digits) = take_while(|c: char| c.is_ascii_digit())(input)?;
let n: i32 = digits.parse().map_err(|_| nom::Err::Error(nom::error::Error::new(input, nom::error::ErrorKind::Verify)))?;
if n > 0 {
Ok((remaining, n))
} else {
Err(nom::Err::Error(nom::error::Error::new(input, nom::error::ErrorKind::Verify)))
}
};
// 2. map_opt is cleaner and equally efficient
let with_map_opt = map_opt(
take_while(|c: char| c.is_ascii_digit()),
|digits: &str| -> Option<i32> {
digits.parse().ok().filter(|&n| n > 0)
}
);
}map_opt provides a clean interface for combined parsing and validation without performance penalty.
Complete Example
use nom::{
IResult,
bytes::complete::{tag, take_while1},
character::complete::digit1,
sequence::tuple,
combinator::map_opt,
};
#[derive(Debug, PartialEq)]
struct Port(u16);
#[derive(Debug, PartialEq)]
struct Host(String);
#[derive(Debug, PartialEq)]
struct Address {
host: Host,
port: Port,
}
fn parse_port() -> impl Fn(&str) -> IResult<&str, Port> {
map_opt(
digit1,
|digits: &str| -> Option<Port> {
let n: u16 = digits.parse().ok()?;
if n > 0 { Some(Port(n)) } else { None }
}
)
}
fn parse_host() -> impl Fn(&str) -> IResult<&str, Host> {
map_opt(
take_while1(|c: char| c.is_alphanumeric() || c == '.' || c == '-'),
|s: &str| -> Option<Host> {
// Validate hostname: not empty, reasonable length
if !s.is_empty() && s.len() <= 253 {
Some(Host(s.to_string()))
} else {
None
}
}
)
}
fn parse_address() -> impl Fn(&str) -> IResult<&str, Address> {
|input: &str| {
let (remaining, (host, _, port)) = tuple((
parse_host(),
tag(":"),
parse_port(),
))(input)?;
Ok((remaining, Address { host, port }))
}
}
fn main() {
let parser = parse_address();
// Valid address
let result = parser("localhost:8080");
match result {
Ok((remaining, addr)) => println!("Parsed: {:?}, remaining: {}", addr, remaining),
Err(e) => println!("Error: {:?}", e),
}
// Invalid port
let result = parser("localhost:0");
match result {
Ok((remaining, addr)) => println!("Parsed: {:?}, remaining: {}", addr, remaining),
Err(_) => println!("Failed to parse - port must be > 0"),
}
// Port out of range
let result = parser("localhost:70000");
match result {
Ok((remaining, addr)) => println!("Parsed: {:?}, remaining: {}", addr, remaining),
Err(_) => println!("Failed to parse - port out of range"),
}
}Summary
use nom::combinator::map_opt;
fn summary() {
// ┌─────────────────────────────────────────────────────────────────────────┐
// │ Aspect │ map_opt │
// ├─────────────────────────────────────────────────────────────────────────┤
// │ Input │ Parser function, validation function │
// │ Parser result │ Passed to validation function │
// │ Validation │ Returns Option<T> │
// │ Success │ Validation returns Some(value) │
// │ Failure │ Validation returns None │
// │ Error type │ nom error on None │
// │ Use case │ Parse + validate + transform │
// └─────────────────────────────────────────────────────────────────────────┘
// Key points:
// 1. map_opt combines parsing and validation in one combinator
// 2. Return Some(value) to accept and transform the result
// 3. Return None to reject the parsed value
// 4. Allows type transformation during validation
// 5. Integrates seamlessly with other nom combinators
// 6. Cleaner than parsing then checking separately
}Key insight: map_opt provides a clean pattern for parsers that need semantic validation beyond syntax. The validation closure returns Some(transformed_value) to accept and potentially transform the result, or None to reject it. This is especially valuable for parsing bounded integers, enums, structured types (IPs, URLs), or any value with semantic constraints beyond raw syntax. The combinator handles backtracking automatically when validation fails, integrating cleanly with nom's error handling model.
