What is the purpose of nom::combinator::map_opt for parsing with validation and transformation?
map_opt combines parsing, transformation, and validation into a single combinatorâapplying a function to a parsed result and succeeding only if the function returns Some, otherwise converting the None to a parse error. This bridges the gap between parsing (extracting data from input) and validation (ensuring data meets constraints), allowing you to transform parsed values while rejecting invalid ones without separate validation steps. It's particularly useful when parsing values that have semantic constraints beyond syntactic structure, such as numbers within ranges, validated identifiers, or constrained strings.
Basic Usage Pattern
use nom::{IResult, bytes::complete::tag, combinator::map_opt};
fn parse_digit(input: &str) -> IResult<&str, u8> {
map_opt(
// First: parse a single character
nom::character::complete::one_of("0123456789"),
// Second: transform and validate
|c: char| c.to_digit(10).map(|d| d as u8)
)(input)
}
fn main() {
// Valid input: '5' parses and converts to 5
let result = parse_digit("5abc");
assert_eq!(result, Ok(("abc", 5)));
// Invalid: 'x' is not a digit, fails at parsing stage
let result = parse_digit("xyz");
assert!(result.is_err());
}map_opt takes a parser and a function that returns Option<T>, succeeding only when the function returns Some.
Validation Beyond Syntax
use nom::{IResult, character::complete::digit1, combinator::map_opt};
// Parse a port number (must be 1-65535)
fn parse_port(input: &str) -> IResult<&str, u16> {
map_opt(
digit1,
|digits: &str| {
digits.parse::<u16>().ok().filter(|&p| p > 0)
}
)(input)
}
fn main() {
// Valid port
assert_eq!(parse_port("8080/rest"), Ok(("/rest", 8080)));
// Invalid: port 0 is not valid
let result = parse_port("0/path");
assert!(result.is_err());
// Invalid: number too large for u16
let result = parse_port("70000/path");
assert!(result.is_err());
}Parse syntactically valid input but reject semantically invalid values.
Compared to Separate map and filter
use nom::{IResult, character::complete::digit1, combinator::{map, map_opt, filter}};
// Approach 1: Using map_opt (single pass)
fn parse_positive_v1(input: &str) -> IResult<&str, i32> {
map_opt(
digit1,
|s: &str| s.parse::<i32>().ok().filter(|&n| n > 0)
)(input)
}
// Approach 2: Using map then filter (requires intermediate type)
fn parse_positive_v2(input: &str) -> IResult<&str, i32> {
let (remaining, num) = map(digit1, |s: &str| s.parse::<i32>())(input)?;
// filter would need the parsed value to be an Option already
// This approach is more verbose
if num.is_err() || num.as_ref().map(|n| *n <= 0).unwrap_or(false) {
return Err(nom::Err::Error(nom::error::Error::new(input, nom::error::ErrorKind::Verify)));
}
Ok((remaining, num.unwrap()))
}
fn main() {
assert_eq!(parse_positive_v1("42"), Ok(("", 42)));
assert!(parse_positive_v1("0").is_err()); // Rejected by validation
assert!(parse_positive_v1("-5").is_err()); // Fails at digit1 stage
}map_opt combines transformation and validation elegantly.
Range Validation
use nom::{IResult, character::complete::digit1, combinator::map_opt};
fn parse_percentage(input: &str) -> IResult<&str, u8> {
map_opt(
digit1,
|s: &str| {
s.parse::<u8>().ok().filter(|&n| n <= 100)
}
)(input)
}
fn parse_age(input: &str) -> IResult<&str, u8> {
map_opt(
digit1,
|s: &str| {
s.parse::<u8>().ok().filter(|&age| (1..=150).contains(age))
}
)(input)
}
fn parse_byte_value(input: &str) -> IResult<&str, u8> {
map_opt(
digit1,
|s: &str| s.parse::<u8>().ok()
)(input)
}
fn main() {
// Valid percentage
assert_eq!(parse_percentage("75%"), Ok(("%", 75)));
// Invalid: over 100
assert!(parse_percentage("150%").is_err());
// Valid age
assert_eq!(parse_age("25 years"), Ok((" years", 25)));
// Invalid: age 200
assert!(parse_age("200 years").is_err());
// Invalid: age 0
assert!(parse_age("0 years").is_err());
}Validate numeric ranges after parsing.
String Validation
use nom::{IResult, bytes::complete::take_while, combinator::map_opt};
fn is_valid_identifier_char(c: char) -> bool {
c.is_alphanumeric() || c == '_'
}
fn parse_identifier(input: &str) -> IResult<&str, String> {
map_opt(
take_while(is_valid_identifier_char),
|s: &str| {
// Validate: must start with letter or underscore
if s.is_empty() {
return None;
}
let first = s.chars().next()?;
if first.is_alphabetic() || first == '_' {
Some(s.to_string())
} else {
None
}
}
)(input)
}
fn parse_hex_color(input: &str) -> IResult<&str, (u8, u8, u8)> {
map_opt(
nom::bytes::complete::take(6usize),
|hex: &str| {
if hex.len() != 6 {
return None;
}
let r = u8::from_str_radix(&hex[0..2], 16).ok()?;
let g = u8::from_str_radix(&hex[2..4], 16).ok()?;
let b = u8::from_str_radix(&hex[4..6], 16).ok()?;
Some((r, g, b))
}
)(input)
}
fn main() {
// Valid identifier
assert_eq!(parse_identifier("hello_world+rest"), Ok(("+rest", "hello_world".to_string())));
assert_eq!(parse_identifier("_private"), Ok(("", "_private".to_string())));
// Invalid: starts with digit
assert!(parse_identifier("123abc").is_err());
// Empty is invalid
assert!(parse_identifier("123").is_err());
// Valid hex color
assert_eq!(parse_hex_color("ff0000"), Ok(("", (255, 0, 0))));
assert_eq!(parse_hex_color("00ff00rest"), Ok(("rest", (0, 255, 0))));
}Apply custom string validation rules during parsing.
Enum Parsing with Validation
use nom::{IResult, bytes::complete::alpha1, combinator::map_opt};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Status {
Active,
Inactive,
Pending,
}
fn parse_status(input: &str) -> IResult<&str, Status> {
map_opt(
alpha1,
|s: &str| match s.to_lowercase().as_str() {
"active" => Some(Status::Active),
"inactive" => Some(Status::Inactive),
"pending" => Some(Status::Pending),
_ => None,
}
)(input)
}
fn main() {
assert_eq!(parse_status("active"), Ok(("", Status::Active)));
assert_eq!(parse_status("INACTIVE"), Ok(("", Status::Inactive)));
assert!(parse_status("unknown").is_err());
}Parse strings into enum variants, rejecting unknown values.
Chained Validation
use nom::{IResult, character::complete::digit1, combinator::map_opt};
// Parse hours (0-23)
fn parse_hours(input: &str) -> IResult<&str, u8> {
map_opt(
digit1,
|s: &str| s.parse::<u8>().ok().filter(|&h| h < 24)
)(input)
}
// Parse minutes (0-59)
fn parse_minutes(input: &str) -> IResult<&str, u8> {
map_opt(
digit1,
|s: &str| s.parse::<u8>().ok().filter(|&m| m < 60)
)(input)
}
fn parse_time(input: &str) -> IResult<&str, (u8, u8)> {
let (input, hours) = parse_hours(input)?;
let (input, _) = nom::character::complete::char(':')(input)?;
let (input, minutes) = parse_minutes(input)?;
Ok((input, (hours, minutes)))
}
fn main() {
assert_eq!(parse_time("12:30"), Ok(("", (12, 30))));
assert_eq!(parse_time("23:59"), Ok(("", (23, 59))));
// Invalid: hours > 23
assert!(parse_time("24:00").is_err());
// Invalid: minutes > 59
assert!(parse_time("12:60").is_err());
}Multiple validation stages in sequence.
Error Context
use nom::{IResult, character::complete::digit1, combinator::map_opt, error::{Error, ErrorKind}};
fn parse_positive(input: &str) -> IResult<&str, i32> {
map_opt(
digit1,
|s: &str| s.parse::<i32>().ok().filter(|&n| n > 0)
)(input)
}
fn main() {
// When validation fails (returns None), nom creates an error
let result = parse_positive("0");
match result {
Err(nom::Err::Error(e)) => {
// Error contains the input and error kind
println!("Parse failed at: {:?}", e.input);
// ErrorKind::Verify typically used for validation failures
}
_ => {}
}
// When parsing fails (digit1 doesn't match)
let result = parse_positive("abc");
assert!(result.is_err());
}map_opt failures produce ErrorKind::Verify errors by default.
Custom Error Types
use nom::{IResult, character::complete::digit1, combinator::map_opt, error::{Error, ErrorKind}};
#[derive(Debug)]
enum ParseError {
NotANumber,
OutOfRange,
}
fn parse_bounded(input: &str) -> IResult<&str, u8, Error<&str>> {
map_opt(
digit1,
|s: &str| {
s.parse::<u8>().ok().filter(|&n| (1..=100).contains(&n))
}
)(input)
}
fn main() {
// The error doesn't distinguish "not a number" from "out of range"
// For custom errors, you need a custom error type
println!("All bounds validation in single combinator");
}For detailed error information, consider custom error types.
Working with Option Results
use nom::{IResult, bytes::complete::take, combinator::map_opt};
fn parse_base64_chunk(input: &str) -> IResult<&str, Vec<u8>> {
map_opt(
take(4usize), // Base64 encodes in 4-char blocks
|s: &str| {
// Decode and return None on invalid characters
base64_decode(s)
}
)(input)
}
fn base64_decode(input: &str) -> Option<Vec<u8>> {
use base64::{Engine, engine::general_purpose::STANDARD};
STANDARD.decode(input).ok()
}
fn main() {
// Valid base64
assert!(parse_base64_chunk("SGVs").is_ok());
// Invalid base64
assert!(parse_base64_chunk("!!!!").is_err());
}Integrate external fallible operations into parsers.
Combining with Other Combinators
use nom::{IResult, character::complete::digit1, combinator::{map_opt, opt}, sequence::tuple};
fn parse_optional_port(input: &str) -> IResult<&str, Option<u16>> {
// Parse optional ":port" where port must be valid
map_opt(
opt(tuple((
nom::character::complete::char(':'),
digit1
))),
|opt_tuple| {
match opt_tuple {
None => Some(None), // No port is valid
Some((_, digits)) => {
// Port present, must be valid
let port = digits.parse::<u16>().ok()?;
if port > 0 {
Some(Some(port))
} else {
None // Port 0 is invalid
}
}
}
}
)(input)
}
fn main() {
assert_eq!(parse_optional_port(":8080"), Ok(("", Some(8080))));
assert_eq!(parse_optional_port(""), Ok(("", None)));
assert!(parse_optional_port(":0").is_err()); // Port 0 invalid
}map_opt works with complex parsed structures.
Transforming While Validating
use nom::{IResult, bytes::complete::take_while_mn, combinator::map_opt};
#[derive(Debug, PartialEq)]
struct Username(String);
fn parse_username(input: &str) -> IResult<&str, Username> {
map_opt(
take_while_mn(3, 20, |c: char| c.is_alphanumeric() || c == '_'),
|s: &str| {
// Validate length (already done by take_while_mn)
// Validate content
if s.is_empty() {
return None;
}
// Must start with letter
let first = s.chars().next()?;
if !first.is_alphabetic() {
return None;
}
// Transform to Username
Some(Username(s.to_string()))
}
)(input)
}
fn main() {
assert_eq!(parse_username("alice_123 rest"), Ok((" rest", Username("alice_123".to_string()))));
assert!(parse_username("_bob").is_err()); // Starts with underscore
assert!(parse_username("ab").is_err()); // Too short (less than 3)
}Create validated types directly during parsing.
Real-World Example: URL Parsing
use nom::{IResult, character::complete::digit1, bytes::complete::take_while, combinator::map_opt, sequence::tuple, character::complete::char};
fn is_host_char(c: char) -> bool {
c.is_alphanumeric() || c == '.' || c == '-'
}
fn parse_host(input: &str) -> IResult<&str, String> {
map_opt(
take_while(is_host_char),
|s: &str| {
if s.is_empty() {
return None;
}
// Validate: no leading/trailing hyphens
if s.starts_with('-') || s.ends_with('-') {
return None;
}
// Validate: no consecutive dots
if s.contains("..") {
return None;
}
Some(s.to_string())
}
)(input)
}
fn parse_url_port(input: &str) -> IResult<&str, Option<u16>> {
map_opt(
opt(tuple((char(':'), digit1))),
|opt_parsed| {
match opt_parsed {
None => Some(None),
Some((_, port_str)) => {
let port = port_str.parse::<u16>().ok()?;
if port > 0 { Some(Some(port)) } else { None }
}
}
}
)(input)
}
fn main() {
// Valid host
assert_eq!(parse_host("example.com/path"), Ok(("/path", "example.com".to_string())));
assert_eq!(parse_host("my-server.org"), Ok(("", "my-server.org".to_string())));
// Invalid: starts with hyphen
assert!(parse_host("-invalid.com").is_err());
// Invalid: consecutive dots
assert!(parse_host("bad..example.com").is_err());
// Valid port
assert_eq!(parse_url_port(":8080"), Ok(("", Some(8080))));
assert_eq!(parse_url_port(""), Ok(("", None)));
assert!(parse_url_port(":0").is_err());
}Validate complex structured input with semantic constraints.
Comparison with map and verify
use nom::{IResult, character::complete::digit1, combinator::{map_opt, map, verify}};
// map_opt: Parse + transform + validate in one step
fn parse_positive_map_opt(input: &str) -> IResult<&str, i32> {
map_opt(digit1, |s: &str| {
s.parse::<i32>().ok().filter(|&n| n > 0)
})(input)
}
// map + verify: Parse + transform, then validate separately
fn parse_positive_verify(input: &str) -> IResult<&str, i32> {
let (remaining, num) = map(digit1, |s: &str| s.parse::<i32>())(input)?;
let (remaining, num) = verify(
nom::combinator::success(num),
|&n: &i32| n > 0
)(remaining)?;
Ok((remaining, num))
}
fn main() {
// Both approaches work
assert_eq!(parse_positive_map_opt("42"), Ok(("", 42)));
assert_eq!(parse_positive_verify("42"), Ok(("", 42)));
// map_opt is more concise when transformation and validation are related
// verify is useful when you need to validate the parsed value as-is
}map_opt is cleaner when transformation produces the validated type.
Synthesis
Quick reference:
use nom::{IResult, combinator::map_opt};
// map_opt signature:
// map_opt<P, F, I, O1, O2>(parser: P, f: F) -> impl Fn(I) -> IResult<I, O2>
// where P: Fn(I) -> IResult<I, O1>
// F: Fn(O1) -> Option<O2>
fn parse_validated(input: &str) -> IResult<&str, ValidatedType> {
map_opt(
// Parser: extract raw data
some_parser,
// Function: transform and validate
|raw: RawType| {
// Transform raw data
let processed = transform(raw);
// Validate: return Some(valid) or None
if is_valid(&processed) {
Some(processed)
} else {
None
}
}
)(input)
}
// Common patterns:
// 1. Range validation
map_opt(digit1, |s: &str| {
s.parse::<u8>().ok().filter(|&n| (1..=100).contains(&n))
})
// 2. Enum parsing
map_opt(alpha1, |s: &str| {
match s {
"red" => Some(Color::Red),
"green" => Some(Color::Green),
"blue" => Some(Color::Blue),
_ => None,
}
})
// 3. Type conversion with validation
map_opt(take(4), |s: &str| {
s.parse::<i32>().ok().filter(|&n| n > 0)
})
// 4. String validation
map_opt(take_while(|c| c.is_alphanumeric()), |s: &str| {
if s.starts_with(|c: char| c.is_alphabetic()) {
Some(s.to_string())
} else {
None
}
})Key insight: map_opt is the bridge between syntactic parsing and semantic validation in nom. It applies a parser to extract data, then applies a transformation function that returns Option<T>. If the function returns Some(value), the parse succeeds with that value; if None, the parse fails. This is particularly valuable when:
- Parsed values need transformation - Converting strings to numbers, decoding encoded data
- Semantic constraints apply - Numbers must be in ranges, strings must match patterns
- Type conversion can fail - Parsing
&strtou8might overflow, which should be a parse error - Custom validation is needed - Business rules beyond syntax
The combinator produces ErrorKind::Verify errors when validation fails, distinguishing validation failures from parsing failures. Use map_opt when you need to both transform and validate in one logical stepâit's cleaner than chaining map and verify, and more type-safe than handling errors separately. For complex validation with custom error messages, consider implementing your own error types with map_res for detailed failure context.
