aam-rs 2.3.0

A Rust implementation of the Abstract Alias Mapping (AAM) framework for aliasing and maping aam files.
Documentation
//! Example: schemas inside schemas, lists, optional fields, @derive with schema selection
//!
//! Demonstrates:
//! 1. Nested schemas — a field is typed by another schema (`Item` inside `Weapon`).
//! 2. Lists — `list<string>`, `list<i32>`, `list<Item>` with inline objects.
//! 3. Optional fields (`field*: type`) — can be omitted without error.
//! 4. `@derive path.aam` — full import of all schemas and values.
//! 5. `@derive path.aam::Schema1::Schema2` — import only selected schemas.
//!
//! Run with:
//! ```sh
//! cargo run --example advanced
//! ```

use aam_rs::aam::AAM;
use aam_rs::error::AamlError;
use std::collections::HashMap;
use std::path::Path;

fn main() {
    let examples_dir = Path::new(env!("CARGO_MANIFEST_DIR")).join("examples");
    std::env::set_current_dir(&examples_dir).expect("Cannot change dir to examples/");

    header("AAM: schemas in schemas • lists • optional fields • @derive");

    demo_full_derive();
    demo_selective_derive();
    demo_nested_schema_validation();
    demo_list_of_schemas();
    demo_derive_two_schemas();
    demo_derive_nonexistent_schema();

    footer();
}

// ── Section 1: full @derive ───────────────────────────────────────────────────

fn demo_full_derive() {
    section("1. @derive advanced_base.aam  (full import: all schemas + values)");
    println!("   File contains schemas: Item, Weapon, Player\n");

    match AAM::load("advanced_base.aam").map_err(first_error) {
        Ok(cfg) => {
            println!("   ✔ Loaded successfully\n");
            print_all_schemas(&cfg, &["Item", "Weapon", "Player"]);
            divider();
            print_key(&cfg, "player_name");
            print_key(&cfg, "level");
            print_key(&cfg, "tags");
            print_key(&cfg, "equipped_ids");
            print_key(&cfg, "sword");
        }
        Err(e) => eprintln!("{e}"),
    }
}

// ── Section 2: @derive with a single schema ───────────────────────────────────

fn demo_selective_derive() {
    section("2. @derive advanced_child.aam  (@derive base.aam::Player, score* not set)");
    println!("   Expected: Player ✔, Item ✗, Weapon ✗, score* absent — not an error\n");

    match AAM::load("advanced_child.aam").map_err(first_error) {
        Ok(cfg) => {
            println!("   ✔ Loaded successfully\n");
            println!("   Schema presence after @derive::Player:");
            println!("     Player : {}", schema_marker(&cfg, "Player", true));
            println!("     Item   : {}", schema_marker(&cfg, "Item", false));
            println!("     Weapon : {}", schema_marker(&cfg, "Weapon", false));
            println!();
            print_all_schemas(&cfg, &["Player"]);
            divider();
            print_key(&cfg, "player_name");
            print_key(&cfg, "level");
            print_key(&cfg, "tags");
            print_key(&cfg, "equipped_ids");
            match cfg.get("score") {
                Some(v) => println!("   {:>15} = {v}", "score"),
                None => println!("   {:>15} = <not set — optional ✔>", "score"),
            }
        }
        Err(e) => eprintln!("   ✘ Unexpected error: {e}"),
    }
}

// ── Section 3: programmatic validation of nested schemas ─────────────────────

fn demo_nested_schema_validation() {
    section("3. Programmatic validation — Weapon { base: Item }  (no file)");

    let src = r#"
        @schema Item   { item_name: string, item_weight: f64, item_rare*: bool }
        @schema Weapon { base: Item, damage: i32, description*: string }
    "#;
    let cfg = match AAM::parse(src).map_err(first_error) {
        Ok(cfg) => cfg,
        Err(e) => {
            eprintln!("   ~ Runtime schema demo skipped on this parser build: {e}");
            return;
        }
    };

    let base_ok = "{ item_name = Axe, item_weight = 5.0 }";

    let mut w = make_weapon(base_ok, "80", None);
    validate(&cfg, "Weapon", &w, "Weapon without description*  → ✔");

    w.insert("description".into(), "Heavy two-handed axe".into());
    validate(&cfg, "Weapon", &w, "Weapon with description*     → ✔");

    let w_bad = make_weapon("{ item_name = Stick, item_weight = bad_num }", "5", None);
    validate(
        &cfg,
        "Weapon",
        &w_bad,
        "item_weight = bad_num        → ✘ expect error",
    );

    let w_no_base: HashMap<String, String> = [("damage".into(), "10".into())].into();
    validate(
        &cfg,
        "Weapon",
        &w_no_base,
        "base missing                 → ✘ expect error",
    );
}

fn make_weapon(base: &str, damage: &str, desc: Option<&str>) -> HashMap<String, String> {
    let mut m: HashMap<String, String> = [
        ("base".into(), base.into()),
        ("damage".into(), damage.into()),
    ]
    .into();
    if let Some(d) = desc {
        m.insert("description".into(), d.into());
    }
    m
}

fn validate(cfg: &AAM, schema: &str, data: &HashMap<String, String>, label: &str) {
    let schema_present = cfg.get_schema(schema).is_some();
    let fields: Vec<_> = data.keys().cloned().collect();
    println!(
        "   ~ {label}\n       ↳ runtime apply_schema is not part of AAM API; schema='{schema}' present={schema_present}, fields={fields:?}"
    );
}

// ── Section 4: list<Schema> ───────────────────────────────────────────────────

fn demo_list_of_schemas() {
    section("4. list<Item> — list of inline objects of type Item");

    let src = r#"
        @schema Item  { item_name: string, item_weight: f64, item_rare*: bool }
        @schema Chest { title: string, loot: list<Item> }
    "#;
    let cfg = match AAM::parse(src).map_err(first_error) {
        Ok(cfg) => cfg,
        Err(e) => {
            eprintln!("   ~ Runtime schema demo skipped on this parser build: {e}");
            return;
        }
    };

    let mut chest_ok: HashMap<String, String> = HashMap::new();
    chest_ok.insert("title".into(), "Golden Chest".into());
    chest_ok.insert(
        "loot".into(),
        "[{ item_name = Gold Coin, item_weight = 0.1 }, \
          { item_name = Gem, item_weight = 0.3, item_rare = true }]"
            .into(),
    );
    validate(&cfg, "Chest", &chest_ok, "two valid Items in loot   → ✔");

    let mut chest_bad = chest_ok.clone();
    chest_bad.insert(
        "loot".into(),
        "[{ item_name = Broken, item_weight = bad_weight }]".into(),
    );
    validate(
        &cfg,
        "Chest",
        &chest_bad,
        "item_weight = bad_weight  → ✘ expect error",
    );
}

// ── Section 5: @derive with two schemas ──────────────────────────────────────

fn demo_derive_two_schemas() {
    section("5. @derive advanced_base.aam::Player::Item  (two :: selectors)");
    println!("   Syntax: @derive <file>::<Schema1>::<Schema2>");
    println!("   Expected: Player ✔, Item ✔, Weapon ✗\n");

    let content = "\
        @derive advanced_base.aam::Player::Item\n\
        player_name  = TwoSchemaHero\n\
        level        = 99\n\
        tags         = [master, dual]\n\
        equipped_ids = [300]\n\
    ";

    match AAM::parse(content).map_err(first_error) {
        Ok(cfg) => {
            println!("   ✔ Loaded successfully\n");
            println!("   Schema presence:");
            println!("     Player : {}", schema_marker(&cfg, "Player", true));
            println!("     Item   : {}", schema_marker(&cfg, "Item", true));
            println!("     Weapon : {}", schema_marker(&cfg, "Weapon", false));
            println!();
            print_all_schemas(&cfg, &["Player", "Item"]);
            divider();
            print_key(&cfg, "player_name");
            print_key(&cfg, "level");
            print_key(&cfg, "tags");
            print_key(&cfg, "equipped_ids");
        }
        Err(e) => eprintln!("{e}"),
    }
}

// ── Section 6: error — non-existent schema ────────────────────────────────────

fn demo_derive_nonexistent_schema() {
    section("6. @derive with a non-existent schema → DirectiveError");
    println!("   @derive advanced_base.aam::NonExistentSchema\n");

    match AAM::parse("@derive advanced_base.aam::NonExistentSchema\n").map_err(first_error) {
        Err(AamlError::DirectiveError {
            directive: cmd,
            message: msg,
            ..
        }) => println!("   ✔ @{cmd}: {msg}"),
        other => eprintln!("   ✘ Unexpected result: {other:?}"),
    }
}

// ── Helper functions ──────────────────────────────────────────────────────────

fn header(title: &str) {
    println!("\n{}", "".repeat(62));
    println!("  {title}");
    println!("{}\n", "".repeat(62));
}

fn section(title: &str) {
    println!("\n┌─{}─┐", "".repeat(58));
    println!("│  {:<58}│", title);
    println!("└─{}─┘", "".repeat(58));
}

fn divider() {
    println!("   {}", "".repeat(52));
}

fn footer() {
    println!("\n{}", "".repeat(62));
    println!("  Done.");
    println!("{}", "".repeat(62));
}

/// Returns a display string showing whether a schema is present or absent,
/// and whether that matches the expectation (`expect_present`).
fn schema_marker(cfg: &AAM, name: &str, expect_present: bool) -> &'static str {
    match (cfg.get_schema(name).is_some(), expect_present) {
        (true, true) => "present  ✔",
        (false, false) => "absent   ✔",
        (true, false) => "present  ✗ (unexpected!)",
        (false, true) => "absent   ✗ (expected!)",
    }
}

fn print_key(cfg: &AAM, key: &str) {
    match cfg.get(key) {
        Some(v) => println!("   {:>15} = {v}", key),
        None => println!("   {:>15} = <not found>", key),
    }
}

fn print_all_schemas(cfg: &AAM, names: &[&str]) {
    for &name in names {
        match cfg.get_schema(name) {
            Some(schema) => {
                let mut fields: Vec<_> = schema.fields.iter().collect();
                fields.sort_by_key(|(k, _)| k.as_str());
                println!("   Schema '{name}':");
                for (field, (ty, optional)) in &fields {
                    let opt = if *optional { "*" } else { " " };
                    println!("     {opt} {field:<20} : {ty}");
                }
                println!();
            }
            None => println!("   Schema '{name}' not found\n"),
        }
    }
}

fn first_error(errors: Vec<AamlError>) -> AamlError {
    errors.into_iter().next().unwrap_or(AamlError::ParseError {
        line: 1,
        content: String::new(),
        details: "unexpected empty error list".to_string(),
        diagnostics: None,
    })
}