Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Using WCL as a Rust Library

WCL can be embedded directly into Rust programs. The wcl facade crate provides the full parsing pipeline, evaluated values, queries, and serde deserialization.

Adding the Dependency

Add wcl as a path dependency (or git dependency) in your Cargo.toml:

[dependencies]
wcl = { path = "../wcl" }

The wcl crate re-exports everything you need.

Parsing a WCL String

Use wcl::parse() to run the full 11-phase pipeline and get a Document:

#![allow(unused)]
fn main() {
use wcl::{parse, ParseOptions};

let source = r#"
    server web-prod {
        host = "0.0.0.0"
        port = 8080
        debug = false
    }
"#;

let doc = parse(source, ParseOptions::default());

// Check for errors
if doc.has_errors() {
    for diag in doc.errors() {
        eprintln!("error: {}", diag.message);
    }
} else {
    println!("Document parsed successfully");
}
}

Parsing a WCL File

Read the file yourself and pass the content to parse(). Set root_dir so that imports resolve correctly:

#![allow(unused)]
fn main() {
use wcl::{parse, ParseOptions};
use std::path::PathBuf;

let path = PathBuf::from("config/main.wcl");
let source = std::fs::read_to_string(&path).expect("read file");

let options = ParseOptions {
    root_dir: path.parent().unwrap().to_path_buf(),
    ..Default::default()
};

let doc = parse(&source, options);
assert!(!doc.has_errors());
}

Accessing Evaluated Values

After parsing, doc.values contains an ordered map of all evaluated top-level attributes and blocks:

#![allow(unused)]
fn main() {
use wcl::{parse, ParseOptions, Value};

let doc = parse(r#"
    name = "my-app"
    port = 8080
    tags = ["web", "prod"]
"#, ParseOptions::default());

// Access scalar values
if let Some(Value::String(name)) = doc.values.get("name") {
    println!("name: {}", name);
}

if let Some(Value::Int(port)) = doc.values.get("port") {
    println!("port: {}", port);
}

// Access list values
if let Some(Value::List(tags)) = doc.values.get("tags") {
    for tag in tags {
        println!("tag: {}", tag);
    }
}
}

Working with Blocks

Blocks are stored as Value::BlockRef in the values map. You can also use the convenience methods on Document:

#![allow(unused)]
fn main() {
use wcl::{parse, ParseOptions, Value};

let doc = parse(r#"
    server web-prod {
        host = "0.0.0.0"
        port = 8080
    }

    server web-staging {
        host = "staging.internal"
        port = 8081
    }
"#, ParseOptions::default());

// Get all blocks as resolved BlockRef values
let servers = doc.blocks_of_type_resolved("server");
for server in &servers {
    println!("server id={:?}", server.id);
    if let Some(Value::Int(port)) = server.get("port") {
        println!("  port: {}", port);
    }
    if let Some(Value::String(host)) = server.get("host") {
        println!("  host: {}", host);
    }
}

// Check decorators
let all_blocks = doc.blocks();
for block in &all_blocks {
    if block.has_decorator("deprecated") {
        let dec = block.decorator("deprecated").unwrap();
        println!("{} is deprecated: {:?}", block.kind, dec.args);
    }
}
}

Working with Tables

Tables evaluate to Value::List(Vec<Value::Map>) — a list of row maps where each row maps column names to cell values:

#![allow(unused)]
fn main() {
use wcl::{parse, ParseOptions};
use wcl::eval::value::Value;

let doc = parse(r#"
    table users {
        name : string
        age  : int
        | "alice" | 25 |
        | "bob"   | 30 |
    }
"#, ParseOptions::default());

if let Some(Value::List(rows)) = doc.values.get("users") {
    for row in rows {
        if let Value::Map(cols) = row {
            println!("{}: {}", cols["name"], cols["age"]);
        }
    }
}
// Output:
// alice: 25
// bob: 30
}

Tables inside blocks appear in the BlockRef.attributes map:

#![allow(unused)]
fn main() {
if let Some(Value::BlockRef(br)) = doc.values.get("main") {
    if let Some(Value::List(rows)) = br.attributes.get("users") {
        println!("Table has {} rows", rows.len());
    }
}
}

Running Queries

The Document::query() method accepts the same query syntax as the CLI:

#![allow(unused)]
fn main() {
use wcl::{parse, ParseOptions, Value};

let doc = parse(r#"
    server svc-api {
        port = 8080
        env = "prod"
    }

    server svc-admin {
        port = 9090
        env = "prod"
    }

    server svc-debug {
        port = 3000
        env = "dev"
    }
"#, ParseOptions::default());

// Select all server blocks
let all = doc.query("server").unwrap();

// Filter by attribute
let prod = doc.query(r#"server | .env == "prod""#).unwrap();

// Project a single attribute
let ports = doc.query("server | .port").unwrap();
if let Value::List(port_list) = ports {
    println!("ports: {:?}", port_list);
    // [Int(8080), Int(9090), Int(3000)]
}

// Filter and project
let prod_ports = doc.query(r#"server | .env == "prod" | .port"#).unwrap();

// Select by ID
let api = doc.query("server#svc-api").unwrap();
}

Deserializing into Rust Structs

With from_str

The simplest approach deserializes a WCL string directly into a Rust type via serde:

#![allow(unused)]
fn main() {
use wcl::from_str;
use serde::Deserialize;

#[derive(Deserialize, Debug)]
struct AppConfig {
    name: String,
    port: i64,
    debug: bool,
}

let config: AppConfig = from_str(r#"
    name = "my-app"
    port = 8080
    debug = false
"#).expect("parse error");

println!("{:?}", config);
}

Deserializing from Value

If you already have a parsed Document, you can deserialize individual values using from_value:

#![allow(unused)]
fn main() {
use wcl::{from_value, Value};
use serde::Deserialize;

#[derive(Deserialize, Debug)]
struct ServerConfig {
    port: i64,
    host: String,
}

// Assume `val` is a Value::Map obtained from doc.values or a BlockRef
let mut map = indexmap::IndexMap::new();
map.insert("port".to_string(), Value::Int(8080));
map.insert("host".to_string(), Value::String("localhost".to_string()));

let config: ServerConfig = from_value(Value::Map(map)).unwrap();
println!("{:?}", config);
}

Custom Functions

You can register custom Rust functions that are callable from WCL expressions. This lets host applications extend WCL with domain-specific logic:

#![allow(unused)]
fn main() {
use wcl::{parse, ParseOptions, FunctionRegistry, FunctionSignature, Value};
use std::sync::Arc;

let mut opts = ParseOptions::default();

// Register a custom function
opts.functions.functions.insert(
    "double".into(),
    Arc::new(|args: &[Value]| {
        match args.first() {
            Some(Value::Int(n)) => Ok(Value::Int(n * 2)),
            _ => Err("expected int".into()),
        }
    }),
);

// Optionally add a signature for LSP support (completions, signature help)
opts.functions.signatures.push(FunctionSignature {
    name: "double".into(),
    params: vec!["n: int".into()],
    return_type: "int".into(),
    doc: "Double a number".into(),
});

let doc = parse("result = double(21)", opts);
assert_eq!(doc.values.get("result"), Some(&Value::Int(42)));
}

You can also use FunctionRegistry::register() to add both the function and its signature at once:

#![allow(unused)]
fn main() {
use wcl::{FunctionRegistry, FunctionSignature, Value};
use std::sync::Arc;

let mut registry = FunctionRegistry::new();
registry.register(
    "greet",
    Arc::new(|args: &[Value]| {
        match args.first() {
            Some(Value::String(s)) => Ok(Value::String(format!("Hello, {}!", s))),
            _ => Err("expected string".into()),
        }
    }),
    FunctionSignature {
        name: "greet".into(),
        params: vec!["name: string".into()],
        return_type: "string".into(),
        doc: "Greet someone".into(),
    },
);
}

Library Files

Create .wcl library files manually and place them in ~/.local/share/wcl/lib/. Use wcl::library::list_libraries() to list installed libraries. See the Libraries guide for details.

Parse Options

ParseOptions controls the pipeline behavior:

#![allow(unused)]
fn main() {
use wcl::{ParseOptions, ConflictMode};
use std::path::PathBuf;

let options = ParseOptions {
    // Root directory for import path jail checking
    root_dir: PathBuf::from("./config"),

    // Maximum depth for nested imports (default: 32)
    max_import_depth: 32,

    // Set to false for untrusted input to forbid all imports
    allow_imports: true,

    // How to handle duplicate attributes in partial merges
    // Strict = error on duplicates, LastWins = later value wins
    merge_conflict_mode: ConflictMode::Strict,

    // Maximum macro expansion depth (default: 64)
    max_macro_depth: 64,

    // Maximum for-loop nesting depth (default: 32)
    max_loop_depth: 32,

    // Maximum total iterations across all for loops (default: 10,000)
    max_iterations: 10_000,

    // Custom functions (builtins are always included)
    functions: FunctionRegistry::default(),
};
}

When processing untrusted WCL input (e.g., from an API), disable imports to prevent file system access:

#![allow(unused)]
fn main() {
use wcl::ParseOptions;

let options = ParseOptions {
    allow_imports: false,
    ..Default::default()
};
}

Error Handling

The Document collects all diagnostics from every pipeline phase. Each Diagnostic includes a message, severity, source span, and optional error code:

#![allow(unused)]
fn main() {
use wcl::{parse, ParseOptions};

let doc = parse(r#"
    server web {
        port = "not_a_number"
    }

    schema "server" {
        port: int
    }
"#, ParseOptions::default());

for diag in &doc.diagnostics {
    let severity = if diag.is_error() { "ERROR" } else { "WARN" };
    let code = diag.code.as_deref().unwrap_or("----");
    eprintln!("[{}] {}: {}", severity, code, diag.message);
}
}

Complete Example

Putting it all together – parse a configuration file, validate it, query it, and extract values:

use wcl::{parse, ParseOptions, Value};
use std::path::PathBuf;

fn main() {
    let source = r#"
        schema "server" {
            port: int
            host: string @optional
        }

        server svc-api {
            port = 8080
            host = "api.internal"
        }

        server svc-admin {
            port = 9090
            host = "admin.internal"
        }
    "#;

    let doc = parse(source, ParseOptions::default());

    // 1. Check for errors
    if doc.has_errors() {
        for e in doc.errors() {
            eprintln!("{}", e.message);
        }
        std::process::exit(1);
    }

    // 2. Query for all server ports
    let ports = doc.query("server | .port").unwrap();
    println!("All ports: {}", ports);

    // 3. Iterate resolved blocks
    for server in doc.blocks_of_type_resolved("server") {
        let id = server.id.as_deref().unwrap_or("(no id)");
        let port = server.get("port").unwrap();
        let host = server.get("host").unwrap();
        println!("{}: {}:{}", id, host, port);
    }
}