Skip to main content

aam_core/commands/
derive.rs

1//! `@derive` directive — inherits keys and schemas from another `.aam` file.
2//!
3//! # Syntax
4//! ```text
5//! @derive path/to/base.aam
6//! @derive "path/to/base.aam"
7//! @derive path/to/base.aam::Schema1
8//! @derive path/to/base.aam::Schema1::Schema2
9//! ```
10//!
11//! # Semantics
12//! - All key-value pairs from the base file are imported into the current document.
13//! - Child values take precedence: existing keys are **never** overwritten.
14//! - Schema definitions follow the same rule: a child schema beats a base schema
15//!   with the same name.
16//! - After the merge, all schemas that are now in scope are checked for
17//!   completeness — every declared field must have a value assigned somewhere
18//!   in the resulting document. Missing fields produce a
19//!   [`AamlError::SchemaValidationError`].
20//!   Optional fields (declared with `*`) are ignored during completeness check.
21
22use crate::aaml::AAML;
23use crate::commands::Command;
24use crate::error::AamlError;
25use crate::pipeline::utils::resolve_relative_path;
26
27/// Command handler for the `@derive` directive.
28#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
29pub struct DeriveCommand;
30
31/// Splits a raw `@derive` argument into `(file_path, schema_selectors)`.
32///
33/// Supported forms:
34/// - `base.aam` → `("base.aam", [])`
35/// - `base.aam::Foo::Bar` → `("base.aam", ["Foo", "Bar"])`
36/// - `"base.aam"::Foo` → `("base.aam", ["Foo"])`
37fn parse_derive_arg(raw: &str) -> (&str, Vec<&str>) {
38    let (path_raw, rest) = if raw.starts_with('"') || raw.starts_with('\'') {
39        let q = raw.chars().next().unwrap();
40        raw[1..].find(q).map_or((raw, ""), |end| {
41            let path = &raw[1..=end];
42            let after = raw[end + 2..].trim_start_matches(':').trim();
43            (path, after)
44        })
45    } else {
46        raw.find("::")
47            .map_or((raw, ""), |pos| (&raw[..pos], &raw[pos + 2..]))
48    };
49
50    let selectors = rest
51        .split("::")
52        .map(str::trim)
53        .filter(|s| !s.is_empty())
54        .collect();
55
56    (path_raw.trim(), selectors)
57}
58
59impl Command for DeriveCommand {
60    fn name(&self) -> &'static str {
61        "derive"
62    }
63
64    fn execute(&self, aaml: &mut AAML, args: &str) -> Result<(), AamlError> {
65        let (path, selectors) = parse_derive_arg(args.trim());
66        let resolved = resolve_relative_path(path, aaml.source_dir.as_deref());
67        let mut base = AAML::load(resolved)?;
68
69        let original_schemas: Vec<String> = aaml.get_schemas().keys().cloned().collect();
70
71        if selectors.is_empty() {
72            let names: Vec<String> = base.get_schemas().keys().cloned().collect();
73            for name in names {
74                aaml.import_schema(&name, &mut base)?;
75            }
76        } else {
77            for name in selectors {
78                aaml.import_schema(name, &mut base)?;
79            }
80        }
81
82        aaml.merge_map_weak(base.get_map_mut());
83
84        let refs: Vec<&str> = original_schemas.iter().map(String::as_str).collect();
85        aaml.validate_schemas_completeness_for(&refs)?;
86
87        Ok(())
88    }
89}