Skip to content
← Back to Examples

Custom Atom Patterns

Rustconceptualadvanced

Learn patterns for extending Parsanol with custom validation and semantic checks.

Key Concepts

  • post-parse validation
  • semantic validation
  • grammar builder extensions
  • trait-based extensibility
  • separation of concerns

Why Parse This Format?

While Parsanol's built-in atoms handle most parsing needs, some domains require custom validation logic. This example shows the preferred patterns for extending Parsanol.

Use Cases

  • Validating reserved words
  • Range checking for numbers
  • Email and URL format validation
  • Custom identifier rules
  • Cross-field validation

Input Format

Configuration files with key-value pairs

key = value (various types)

Examples

server_port = 8080 admin_email = admin@example.comVALID

Valid configuration

return = value port = 80INVALID

Configuration with semantic issues

Output Format

Validated configuration with semantic checks

Structure

key

Configuration key (validated as identifier)

value

Configuration value (type-validated)

valid

Whether the pair passed semantic validation

How the Code Works

This section explains how the parsing rules map to the format semantics.

1. Post-Parse Semantic Validation

pub struct SemanticValidator {
    reserved_words: Vec<String>,
    port_range: (u16, u16),
}

impl SemanticValidator {
    pub fn validate_identifier(&self, name: &str) -> Result<(), String> {
        if self.reserved_words.contains(&name.to_string()) {
            return Err(format!("'{}' is a reserved word", name));
        }
        Ok(())
    }
}

Instead of creating custom atoms, validate after parsing: **Why this pattern:** - Keeps grammar simple - Separates syntax from semantics - Easier to test validation logic independently

2. Grammar Builder Extensions

pub trait GrammarBuilderExt {
    fn quoted_string(&mut self, name: &str) -> usize;
    fn bounded_integer(&mut self, name: &str, min: i64, max: i64) -> usize;
    fn safe_identifier(&mut self, name: &str) -> usize;
}

Create convenience methods for common patterns:

3. Validation Patterns

#### Identifier Validation - Check against reserved words - Enforce naming conventions - Validate length constraints #### Numeric Range Validation - Check bounds - Validate special ranges (e.g., privileged ports) #### Email Validation - Format validation (beyond regex) - Semantic checks (domain exists, etc.)

3. Validation Patterns

#### Identifier Validation - Check against reserved words - Enforce naming conventions - Validate length constraints #### Numeric Range Validation - Check bounds - Validate special ranges (e.g., privileged ports) #### Email Validation - Format validation (beyond regex) - Semantic checks (domain exists, etc.)

Source Code

Rust
//! Custom Atoms Example
//!
//! Demonstrates patterns for extending Parsanol with custom validation logic.
//! Shows how to create domain-specific parsers with semantic checks.
//!
//! Run with: cargo run --example custom-atoms --no-default-features

#![allow(clippy::print_literal)]

use parsanol::portable::{
    parser_dsl::{dynamic, re, seq, GrammarBuilder},
    AstArena, Grammar, PortableParser,
};

// ============================================================================
// Pattern 1: Semantic Validation (Post-Parse)
// ============================================================================

/// Reserved words that cannot be used as identifiers
const RESERVED_WORDS: &[&str] = &[
    "if", "else", "while", "for", "return", "fn", "let", "const", "true", "false", "null", "break",
    "continue", "import", "export",
];

/// Validate that an identifier is not a reserved word
pub fn validate_identifier(name: &str) -> Result<(), String> {
    if RESERVED_WORDS.contains(&name) {
        return Err(format!("'{}' is a reserved word", name));
    }
    if name.starts_with('_') && name.len() == 1 {
        return Err("Underscore alone is not a valid identifier".to_string());
    }
    if name.len() < 2 {
        return Err(format!("Identifier '{}' is too short", name));
    }
    Ok(())
}

/// Validate a port number
pub fn validate_port(port: u16) -> Result<(), String> {
    if port == 0 {
        return Err("Port 0 is not valid".to_string());
    }
    if port < 1024 {
        return Err(format!("Port {} is a privileged port (< 1024)", port));
    }
    Ok(())
}

/// Validate an email format (beyond regex)
pub fn validate_email(email: &str) -> Result<(), String> {
    let parts: Vec<&str> = email.split('@').collect();
    if parts.len() != 2 {
        return Err("Email must contain exactly one @".to_string());
    }

    let local = parts[0];
    let domain = parts[1];

    if local.is_empty() {
        return Err("Email local part cannot be empty".to_string());
    }
    if local.starts_with('.') || local.ends_with('.') {
        return Err("Email local part cannot start or end with .".to_string());
    }
    if !domain.contains('.') {
        return Err("Email domain must contain a .".to_string());
    }

    Ok(())
}

// ============================================================================
// Pattern 2: Configuration Parser with Validation
// ============================================================================

/// Configuration parser that demonstrates custom validation
pub struct ConfigParser {
    grammar: Grammar,
}

impl ConfigParser {
    pub fn new() -> Self {
        let grammar = GrammarBuilder::new()
            // Basic patterns
            .rule("identifier", re("[a-zA-Z_][a-zA-Z0-9_]*"))
            .rule("integer", re("[0-9]+"))
            .rule("email_local", re("[a-zA-Z0-9._%+-]+"))
            .rule("email_domain", re("[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}"))
            .rule(
                "email",
                seq(vec![
                    dynamic(re("[a-zA-Z0-9._%+-]+")),
                    dynamic(re("@")),
                    dynamic(re("[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}")),
                ]),
            )
            // Values
            .rule("value", re("[^\\n]+"))
            // Key-value pairs
            .rule(
                "pair",
                seq(vec![
                    dynamic(re("[a-zA-Z_][a-zA-Z0-9_]*")), // key
                    dynamic(re("[ \\t]*=[ \\t]*")),        // =
                    dynamic(re("[^\\n]+")),                // value
                ]),
            )
            // Full config (last rule is root)
            .rule("config", re("([^\\n]*\\n)*[^\\n]*"))
            .build();

        Self { grammar }
    }

    /// Parse and validate configuration
    pub fn parse(&self, input: &str) -> Result<Vec<(String, String, bool)>, String> {
        let mut arena = AstArena::for_input(input.len());
        let mut parser = PortableParser::new(&self.grammar, input, &mut arena);

        let _ast = parser
            .parse()
            .map_err(|e| format!("Parse error: {:?}", e))?;

        // Extract and validate pairs
        let mut pairs = Vec::new();

        for line in input.lines() {
            let trimmed = line.trim();

            // Skip empty lines and comments
            if trimmed.is_empty() || trimmed.starts_with('#') {
                continue;
            }

            // Parse key-value pair
            if let Some((key, value)) = trimmed.split_once('=') {
                let key = key.trim();
                let value = value.trim();

                // Validate key
                let key_valid = validate_identifier(key).is_ok();

                // Validate value based on key name
                let value_valid = if key.contains("port") {
                    value
                        .parse::<u16>()
                        .ok()
                        .is_some_and(|p| validate_port(p).is_ok())
                } else if key.contains("email") || key.contains("mail") {
                    validate_email(value).is_ok()
                } else {
                    true
                };

                pairs.push((key.to_string(), value.to_string(), key_valid && value_valid));
            }
        }

        Ok(pairs)
    }
}

impl Default for ConfigParser {
    fn default() -> Self {
        Self::new()
    }
}

// ============================================================================
// Pattern 3: Type-Safe Value Extraction
// ============================================================================

/// Extract and validate a port number from a string
pub fn extract_port(value: &str) -> Result<u16, String> {
    let port: u16 = value
        .parse()
        .map_err(|_| format!("'{}' is not a valid port", value))?;
    validate_port(port)?;
    Ok(port)
}

/// Extract and validate an email from a string
pub fn extract_email(value: &str) -> Result<String, String> {
    validate_email(value)?;
    Ok(value.to_string())
}

/// Extract and validate an identifier from a string
pub fn extract_identifier(value: &str) -> Result<String, String> {
    validate_identifier(value)?;
    Ok(value.to_string())
}

// ============================================================================
// Main: Demonstrate Patterns
// ============================================================================

fn main() {
    println!("Custom Atoms Example");
    println!("{}\n", "=".repeat(60));

    // Pattern 1: Semantic validation
    println!("Pattern 1: Semantic Validation");
    println!("{}", "-".repeat(60));

    println!("\nIdentifier validation:");
    for name in &["valid_name", "if", "_", "myVar123", "x"] {
        match validate_identifier(name) {
            Ok(()) => println!("  ✓ '{}' is valid", name),
            Err(e) => println!("  ✗ '{}': {}", name, e),
        }
    }

    println!("\nPort validation:");
    for port in &[80, 443, 8080, 0] {
        match validate_port(*port) {
            Ok(()) => println!("  ✓ Port {} is valid", port),
            Err(e) => println!("  ✗ Port {}: {}", port, e),
        }
    }

    println!("\nEmail validation:");
    for email in &[
        "user@example.com",
        "invalid",
        "@example.com",
        "user@",
        "user.name@example.org",
    ] {
        match validate_email(email) {
            Ok(()) => println!("  ✓ '{}' is valid", email),
            Err(e) => println!("  ✗ '{}': {}", email, e),
        }
    }

    // Pattern 2: Configuration parser
    println!("\n{}", "-".repeat(60));
    println!("Pattern 2: Configuration Parser with Validation");
    println!("{}", "-".repeat(60));

    let input = r#"# Configuration with various value types
server_port = 8080
admin_email = admin@example.com
debug_mode = true
_invalid = value
return = something
privileged_port = 80
"#;

    println!("\nInput:");
    println!("{}\n", input);

    let parser = ConfigParser::new();
    match parser.parse(input) {
        Ok(pairs) => {
            println!("Parsed configuration:");
            for (key, value, valid) in pairs {
                let status = if valid { "✓" } else { "✗" };
                println!("  {} {} = {}", status, key, value);
            }
        }
        Err(e) => {
            eprintln!("Error: {}", e);
        }
    }

    // Pattern 3: Type-safe extraction
    println!("\n{}", "-".repeat(60));
    println!("Pattern 3: Type-Safe Value Extraction");
    println!("{}", "-".repeat(60));

    println!("\nExtract port:");
    match extract_port("8080") {
        Ok(port) => println!("  ✓ Extracted port: {}", port),
        Err(e) => println!("  ✗ {}", e),
    }
    match extract_port("80") {
        Ok(port) => println!("  ✓ Extracted port: {}", port),
        Err(e) => println!("  ✗ {}", e),
    }

    println!("\nExtract email:");
    match extract_email("user@example.com") {
        Ok(email) => println!("  ✓ Extracted email: {}", email),
        Err(e) => println!("  ✗ {}", e),
    }

    println!("\nExtract identifier:");
    match extract_identifier("my_variable") {
        Ok(id) => println!("  ✓ Extracted identifier: {}", id),
        Err(e) => println!("  ✗ {}", e),
    }
    match extract_identifier("if") {
        Ok(id) => println!("  ✓ Extracted identifier: {}", id),
        Err(e) => println!("  ✗ {}", e),
    }
}