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.