Skip to content
noml-langPublic

About

Rust library for NOML (Nested Object Markup Language) - blazing-fast config parser with variable interpolation, imports, and schema validation.

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

1 star

Watchers

0 watching

Forks

Latest commit

Β 

History

98 Commits

Folders and files

NOML +Β  RUST
HIGH-PERFORMANCE DYNAMIC CONFIGURATION

Crates.io Β  Crates.io Downloads Β  docs.rs Β  CI Β  MSRV


NOML (Nested Object Markup Language) is a blazing-fast dynamic configuration language that revolutionizes how you handle configuration files. With industry-leading format preservation, zero-copy architecture, and 25Β΅s parsing performance, NOML delivers both speed and power.

Unlike static markup languages, NOML is a markup/scripting hybrid that combines the simplicity of TOML with dynamic capabilities that traditional config formats simply cannot match.

πŸš€ Performance That Matters

  • ⚑ 25Β΅s parsing - Legitimately high-performance by industry standards
  • ⚑ 37ns reads - Blazing-fast value access with path-based navigation
  • ⚑ Zero-copy architecture - Optimized for real-world performance
  • ⚑ 47% faster - Massive optimization improvements over previous versions

🎯 Revolutionary Features

πŸ”₯ Format Preservation

Industry-first complete format preservation - maintain exact whitespace, comments, indentation, and styling during parsing and round-trip editing. Perfect for configuration management tools and IDEs.

🌍 Dynamic Configuration

  • Environment Variables: env("DATABASE_URL", "default")
  • String Interpolation: "Welcome ${user.name}!"
  • File Inclusion: include "database.noml"
  • Native Types: @duration("30s"), @size("10MB"), @url("https://api.com")

πŸ“„ TOML Compatibility

Parse most TOML files with full format preservation - get advanced features while maintaining compatibility with existing TOML configurations.

πŸ’ͺ Key Advantages

NOML vs Static Config Languages:

  • 146% more features than TOML for only 2x performance cost
  • Path-based access - config.get("server.database.port") vs manual navigation
  • Type system - Native parsing of sizes, durations, URLs, IPs
  • Dynamic resolution - Runtime environment integration
  • Format preservation - Perfect for editing tools and automation

πŸš€ Quick Start

Add NOML to your Cargo.toml:

[dependencies]
noml = "0.10"

Basic Usage

use noml::parse;

let config = parse(r#"
    app_name = "my-service"
    debug = env("DEBUG", "false")
    
    [server]
    host = "0.0.0.0" 
    port = env("PORT", "8080")
    timeout = @duration("30s")
    
    [database]
    url = "postgresql://localhost/mydb"
    max_connections = 100
    pool_timeout = @duration("5m")
"#)?;

// Fast path-based access
let app_name = config.get("app_name").unwrap().as_string()?;
let port = config.get("server.port").unwrap().as_integer()?;     // "8080" from env() converts
let timeout = config.get("server.timeout").unwrap().as_float()?; // 30.0 seconds

Format Preservation

use noml::parser::ast::AstValue;
use noml::{modify_preserving, parse_preserving_from_file, save_preserving};

// Parse with comments, key order and quote styles kept
let doc = parse_preserving_from_file("config.noml")?;

// Change a value in the syntax tree
let doc = modify_preserving(doc, |root| {
    if let AstValue::Table { entries, .. } = &mut root.value {
        for entry in entries.iter_mut() {
            if entry.key.to_string() == "port" {
                entry.value.value = AstValue::Integer { value: 9090, raw: "9090".into() };
            }
        }
    }
    Ok(())
})?;

// Write it back; everything you did not change keeps its comments and style
save_preserving(&doc, "config.noml")?;

Advanced Configuration Management

use noml::Config;

let mut config = Config::from_file("app.noml")?;

// Merge another config on top (its values win)
let overrides = Config::from_file("local-overrides.noml")?;
config.merge(&overrides)?;

// Read with a fallback that is stored in the config
let port = config.get_or_insert("server.port", 8080)?.as_integer()?;

// Dynamic updates
config.set("last_updated", "2026-10-09T12:00:00Z")?;
config.save_to_file("app.noml")?;

πŸ“– NOML Syntax

Environment Variables & Native Types

# Environment integration
database_url = env("DATABASE_URL", "sqlite:memory:")
api_key = env("API_KEY")  # Required - will error if missing

# Native type parsing
max_file_size = @size("100MB")     # Bytes: 104857600
cache_timeout = @duration("1h30m") # Seconds: 5400  
api_endpoint = @url("https://api.example.com/v1")
server_ip = @ip("192.168.1.100")

String Interpolation & File Includes

app_name = "my-service"
port = 8080
log_file = "/var/log/${app_name}.log"   # "/var/log/my-service.log"
health = "http://localhost:${port}/up"  # numbers are written as text
listen_port = ${port}                   # bare: copies the value, stays an integer

[database]
name = "${app_name}_db"
url = "postgres://localhost/${database.name}"

# Include other configuration files
database_settings = include "database.noml"
secrets = include "secrets.noml"

How ${...} works:

  • A path is a dotted key from the document root: ${app_name}, ${database.name}, ${servers.0.host} (or ${servers[0].host}) for array elements. Quote keys that contain dots: ${"dotted.key".x}.
  • Inside a double-quoted string ("..." or """...""") the value is converted to text. Tables and arrays can't be converted; use a bare ${table} to copy them.
  • A bare ${path} copies the value with its type, including whole tables.
  • References can point forward, chain, and appear in env() defaults and native type arguments: @size("${max_mb}MB").
  • Single-quoted ('...', '''...''') and raw (r"...") strings are literal and never interpolated. In a double-quoted string, $${ gives a literal ${.
  • A missing path is an error with its line and column; a reference cycle (a = "${b}", b = "${a}") is reported as a circular reference.
  • In an included file, paths are looked up in that file first, then in the file that included it. Values set with Resolver::set_variable are used when the document has no value at the path.

Advanced Nesting & Arrays

[server.ssl]
enabled = true
cert_file = "/etc/ssl/cert.pem"
key_file = "/etc/ssl/private.key"

[[workers]]
name = "background-processor"
threads = 4
memory_limit = @size("512MB")

[[workers]]
name = "api-handler" 
threads = 8
memory_limit = @size("1GB")

πŸ“Š Performance Comparison

NOML delivers high-performance parsing while providing 146% more features than static alternatives:

Parser Parse Time Features Format Preservation
NOML 25Β΅s 32 βœ… Complete
TOML 16¡s 13 ❌ None
JSON 10¡s 8 ❌ None
YAML 125¡s 15 ❌ None

Real-world usage (parse once + 10,000 reads): NOML is only 1.95x slower than TOML while delivering exponentially more functionality.

πŸ›  Command-Line Interface

Install and use the NOML CLI:

cargo install noml

# Validate configuration files
noml validate config.noml

# Parse and display structure  
noml parse app.noml

# Check version
noml version

πŸ”§ Features & Compatibility

Cargo Features

[dependencies]
noml = { version = "0.10", features = ["chrono", "async"] }
  • chrono - Conversions between noml::Datetime and chrono date-times
  • async - Async file operations and HTTP includes

TOML Compatibility

NOML can parse most TOML files with full format preservation:

// Parse TOML files with NOML for advanced features
let config = noml::parse_from_file("config.toml")?;
let port = config.get("server.port").unwrap().as_integer()?;  // Path-based access

Supported from TOML: bare, quoted and dotted keys, [tables] and [[arrays of tables]] in any order, basic and literal strings (single and multi-line, with TOML escapes such as \u00E9), integers in decimal/hex/octal/binary with _ separators, inf/nan floats, date-time literals (1979-05-27T07:32:00Z, 1979-05-27, 07:32:00, read as Value::DateTime(noml::Datetime)), and comments anywhere, including inside arrays. Defining the same key twice is an error, as in TOML.

🎯 Why Choose NOML?

For Configuration Management:

  • Format Preservation - Perfect for automated configuration tools
  • Environment Integration - Runtime environment variable resolution
  • Type Safety - Native parsing eliminates custom conversion code
  • Path Access - Clean dot-notation navigation

For Performance:

  • 25Β΅s parsing - Legitimately fast by industry standards
  • 37ns reads - Blazing-fast value access
  • Zero-copy architecture - Optimized for real-world usage
  • Production ready - 124 comprehensive tests

For Developer Experience:

  • Rich error messages - Precise source locations and helpful context
  • Complete API - Covers all real-world configuration use cases
  • TOML compatibility - Drop-in replacement for many TOML files
  • Async support - Modern Rust patterns with tokio integration

Examples

Here are some examples of how to use the noml library in your Rust code.

Basic Parsing

You can easily parse a NOML string and access its values.

## πŸ“š **Documentation & Resources**

- **[API Documentation](https://docs.rs/noml)** - Complete API reference with examples
- **[NOML Language Specification](https://github.com/noml-lang/spec)** - Official language specification
- **[Examples](examples/)** - Real-world usage examples and benchmarks
- **[GitHub Repository](https://github.com/noml-lang/noml-rust)** - Source code and issue tracking

## 🀝 **Contributing**

We welcome contributions! Please see our [Contributing Guidelines](CONTRIBUTING.md) for details.

## πŸ“„ **License**

Licensed under the Apache License, Version 2.0. See [LICENSE](LICENSE) for details.

---

<div align="center">
    <strong>NOML 0.10.0 - High-Performance Dynamic Configuration</strong><br>
    <em>Blazing-fast β€’ Feature-rich β€’ Format-preserving</em>
</div>

<br>

### **HTTP Includes (Async Feature)**

With the `async` feature enabled, you can include configuration from remote HTTP/HTTPS URLs:

```rust
use noml::parse_from_file_async;

// config.noml content:
// base_config = include "https://config.example.com/app-defaults.noml"
// api_key = env("API_KEY")
// debug = true

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let config = parse_from_file_async("config.noml").await?;
    
    // The remote config is automatically fetched and merged
    println!("Loaded config with remote includes: {:?}", config);
    Ok(())
}

HTTP Includes Features:

  • βœ… Secure HTTPS Support: Full support for HTTPS URLs with proper certificate validation
  • ⚑ Automatic Caching: Remote configs are cached to improve performance and reduce network requests
  • πŸ”’ Timeout Protection: Configurable request timeouts prevent hanging operations
  • πŸ”„ Error Handling: Clear error messages for network issues, HTTP errors, and parse failures
  • πŸ“¦ No Nested HTTP: HTTP includes cannot contain other HTTP includes (prevents security issues)
[dependencies]
noml = { version = "0.10", features = ["async"] }
tokio = { version = "1.0", features = ["full"] }


Working with Native Types

noml supports special native types for common configuration values like file sizes, durations, IP addresses, and more.

use noml::parse;

let source = r#"
    # File sizes and durations
    max_upload_size = @size("256MB")
    request_timeout = @duration("90s")
    
    # Network and web
    website = @url("https://example.com")
    server_ip = @ip("192.168.1.100")
    
    # Data formats
    app_version = @semver("2.1.0")
    secret_data = @base64("SGVsbG8gV29ybGQ=")
    user_id = @uuid("550e8400-e29b-41d4-a716-446655440000")
"#;

let config = parse(source)?;

// The values are parsed and validated
assert_eq!(config.get("max_upload_size").unwrap().as_integer()?, 256 * 1024 * 1024);
assert_eq!(config.get("request_timeout").unwrap().as_float()?, 90.0);
assert_eq!(config.get("website").unwrap().as_string()?, "https://example.com");
assert_eq!(config.get("server_ip").unwrap().as_string()?, "192.168.1.100");
assert_eq!(config.get("app_version").unwrap().as_string()?, "2.1.0");

Available Native Types:

  • @size("10MB") - File/memory sizes (KB, MB, GB, etc.)
  • @duration("30s") - Time durations (ms, s, m, h, d, w), compound forms like "1h30m"
  • @url("https://...") - URL validation
  • @ip("192.168.1.1") - IP address validation (IPv4/IPv6)
  • @semver("1.2.3") - Semantic version parsing
  • @base64("SGVsbG8=") - Base64 encoded data
  • @uuid("550e8400-...") - UUID format validation


Working with Arrays and Tables

noml fully supports arrays, inline tables, and arrays of tables, similar to TOML.

use noml::parse;

let source = r#"
    # An array of strings
    allowed_roles = ["admin", "editor", "viewer"]

    # An inline table
    point = { x = 1.0, y = -1.0 }

    # An array of tables
    [[users]]
    name = "Alice"
    email = "alice@example.com"

    [[users]]
    name = "Bob"
    email = "bob@example.com"
"#;

let config = parse(source)?;

// Access array elements
let roles = config.get("allowed_roles").unwrap().as_array()?;
assert_eq!(roles.len(), 3);
assert_eq!(roles[0].as_string()?, "admin");

// Access inline table values
assert_eq!(config.get("point.x").unwrap().as_float()?, 1.0);

// Access values from an array of tables
let users = config.get("users").unwrap().as_array()?;
assert_eq!(users[0].get("name").unwrap().as_string()?, "Alice");
assert_eq!(users[1].get("name").unwrap().as_string()?, "Bob");


High-Level Configuration Management

For more advanced use cases, the Config struct provides a high-level API for loading, modifying, and saving configurations.

use noml::Config;
use std::fs;

// Create a temporary file for the example
let temp_dir = tempfile::tempdir()?;
let file_path = temp_dir.path().join("config.noml");
fs::write(&file_path, "version = \"1.0.0\"")?;

// Load the configuration from a file
let mut config = Config::from_file(&file_path)?;
assert_eq!(config.get("version").unwrap().as_string()?, "1.0.0");

// Modify the configuration
config.set("debug", true)?;
config.set("database.port", 5432)?;

// Save the changes back to the file
config.save()?;

// Verify the changes
let updated_config = Config::from_file(&file_path)?;
assert_eq!(updated_config.get("debug").unwrap().as_bool()?, true);
assert_eq!(updated_config.get("database.port").unwrap().as_integer()?, 5432);


Async Support πŸš€

noml supports async operations for modern Rust applications! Enable the async feature:

[dependencies]
noml = { version = "0.10", features = ["async"] }
tokio = { version = "1.0", features = ["full"] }

All parsing and file operations are available in async variants:

use noml::{parse_async, Config};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    // Parse configurations asynchronously
    let config = parse_async(r#"
        app_name = "AsyncApp"
        port = env("PORT", "8080")
        
        [database]
        url = env("DATABASE_URL", "sqlite:memory:")
    "#).await?;
    
    println!("App: {}", config.get("app_name").unwrap().as_string()?);

    // Load, modify, and save configurations asynchronously
    let mut config = Config::load_async("config.noml").await?;
    config.set("last_started", "2025-09-19T12:00:00Z")?;
    config.save_async("config.noml").await?;
    
    // Reload to get latest changes
    config.reload_async().await?;
    
    Ok(())
}

Thread Safety: All NOML types (Value, Config) are Send + Sync, making them safe to share between async tasks and threads. Perfect for concurrent applications and microservices!

Performance: Async operations are non-blocking and integrate seamlessly with tokio, async-std, and other async runtimes.

Run the async demo: cargo run --example async_demo --features async



Schema Validation βœ…

noml includes built-in schema validation to catch configuration errors early:

use noml::{Config, Schema, FieldType, SchemaBuilder};

// Load your configuration
let config = Config::from_string(r#"
    app_name = "MyApp"
    port = 8080
    debug = true
    
    [database]
    host = "localhost"
    max_connections = 100
"#)?;

// Define expected schema
let schema = SchemaBuilder::new()
    .require_string("app_name")
    .require_integer("port")
    .optional_bool("debug")
    .build();

// Validate configuration against schema
config.validate_schema(&schema)?;

// Or create more complex schemas
let db_schema = Schema::new()
    .required_field("host", FieldType::String)
    .required_field("max_connections", FieldType::Integer)
    .allow_additional(false);

let app_schema = Schema::new()
    .required_field("app_name", FieldType::String)
    .required_field("port", FieldType::Integer)
    .required_field("database", FieldType::Table(db_schema));

config.validate_schema(&app_schema)?;

Benefits:

  • πŸ›‘οΈ Early Error Detection: Catch configuration issues before runtime
  • 🎯 Type Safety: Ensure values are the expected types
  • πŸ“‹ Required Fields: Validate that critical configuration is present
  • πŸ” Clear Error Messages: Detailed validation failure reports

About

Rust library for NOML (Nested Object Markup Language) - blazing-fast config parser with variable interpolation, imports, and schema validation.

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages