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:

  1. Parsed values need transformation - Converting strings to numbers, decoding encoded data
  2. Semantic constraints apply - Numbers must be in ranges, strings must match patterns
  3. Type conversion can fail - Parsing &str to u8 might overflow, which should be a parse error
  4. 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.