Skip to main content

aam_core/aaml/
mod.rs

1//! Core AAML parser and runtime.
2//!
3//! The [`AAML`] struct is the legacy entry point for parsing `.aam`
4//! configuration files. It is available when the `legacy` feature is enabled
5//! (enabled by default). The parsing helpers in [`parsing`] remain available
6//! regardless of the feature flag because the newer `AAM` pipeline reuses them.
7//!
8//! [`AAML`] supports:
9//! - Key-value assignments (`key = value`)
10//! - Directives: `@import`, `@derive`, `@schema`, `@type`
11//! - Runtime type validation via registered or built-in types
12//! - Schema-based struct validation with [`AAML::apply_schema`]
13
14#![allow(clippy::missing_errors_doc)]
15
16#[cfg(feature = "legacy")]
17use crate::error::ErrorDiagnostics;
18
19#[cfg(feature = "legacy")]
20use crate::commands::schema::SchemaDef;
21#[cfg(feature = "legacy")]
22use crate::commands::{self, Command};
23#[cfg(feature = "legacy")]
24use crate::error::AamlError;
25#[cfg(feature = "legacy")]
26use crate::types::list::ListType;
27#[cfg(feature = "legacy")]
28use crate::types::{Type, resolve_builtin};
29#[cfg(feature = "legacy")]
30use std::collections::HashMap;
31#[cfg(feature = "legacy")]
32use std::fs;
33#[cfg(feature = "legacy")]
34use std::ops::{Add, AddAssign};
35#[cfg(feature = "legacy")]
36use std::path::{Path, PathBuf};
37#[cfg(feature = "legacy")]
38use std::sync::Arc;
39
40#[cfg(feature = "legacy")]
41mod lookup;
42pub mod parsing;
43#[cfg(feature = "legacy")]
44mod validation;
45
46#[cfg(all(feature = "serde", feature = "legacy"))]
47pub mod serialize;
48
49#[cfg(feature = "perf-hash")]
50#[cfg(feature = "legacy")]
51#[cfg(feature = "perf-hash")]
52type Hasher = ahash::RandomState;
53
54#[cfg(feature = "legacy")]
55#[cfg(not(feature = "perf-hash"))]
56type Hasher = std::collections::hash_map::RandomState;
57
58#[cfg(feature = "legacy")]
59type AamlString = Box<str>;
60
61/// The main AAML parser and configuration store.
62///
63/// Holds a flat key-value map, registered type definitions, command handlers,
64/// and schema definitions. All directives (`@derive`, `@schema`, etc.) are
65/// processed at parse time.
66///
67/// # Example
68/// ```no_run
69/// use aam_core::aaml::AAML;
70///
71/// let cfg = AAML::parse("host = localhost\nport = 8080").unwrap();
72/// assert_eq!(cfg.find_obj("host").unwrap().as_str(), "localhost");
73/// ```
74#[cfg(feature = "legacy")]
75#[deprecated(since = "2.0.0", note = "please use AAM and Pipeline instead")]
76pub struct AAML {
77    map: HashMap<AamlString, AamlString, Hasher>,
78    commands: HashMap<String, Arc<dyn Command>>,
79    types: HashMap<String, Box<dyn Type>>,
80    schemas: HashMap<String, SchemaDef>,
81    /// Base directory for resolving relative paths in @import/@derive directives.
82    pub(crate) source_dir: Option<PathBuf>,
83}
84
85#[cfg(feature = "legacy")]
86impl std::fmt::Debug for AAML {
87    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
88        f.debug_struct("AAML")
89            .field("map", &self.map)
90            .field("commands_count", &self.commands.len())
91            .field("types_count", &self.types.len())
92            .field("schemas", &self.schemas)
93            .field("source_dir", &self.source_dir)
94            .finish()
95    }
96}
97
98#[cfg(feature = "legacy")]
99impl AAML {
100    /// Creates a new empty [`AAML`] instance with all default commands registered.
101    #[must_use]
102    pub fn new() -> Self {
103        let mut instance = Self {
104            map: HashMap::with_hasher(Hasher::new()),
105            commands: HashMap::new(),
106            types: HashMap::new(),
107            schemas: HashMap::new(),
108            source_dir: None,
109        };
110        instance.register_default_commands();
111        instance
112    }
113
114    #[must_use]
115    pub const fn get_schemas(&self) -> &HashMap<String, SchemaDef> {
116        &self.schemas
117    }
118
119    /// Creates a new [`AAML`] instance pre-allocated for `capacity` key-value entries.
120    #[must_use]
121    pub fn with_capacity(capacity: usize) -> Self {
122        let mut instance = Self {
123            map: HashMap::with_capacity_and_hasher(capacity, Hasher::default()),
124            commands: HashMap::new(),
125            types: HashMap::new(),
126            schemas: HashMap::new(),
127            source_dir: None,
128        };
129        instance.register_default_commands();
130        instance
131    }
132
133    // ── Internal accessors used by commands ──────────────────────────────────
134
135    pub(crate) const fn get_schemas_mut(&mut self) -> &mut HashMap<String, SchemaDef> {
136        &mut self.schemas
137    }
138
139    #[must_use]
140    pub fn get_schema(&self, name: &str) -> Option<&SchemaDef> {
141        self.schemas.get(name)
142    }
143
144    pub(crate) const fn get_map_mut(&mut self) -> &mut HashMap<AamlString, AamlString, Hasher> {
145        &mut self.map
146    }
147
148    // ── Accessors for pipeline/executer ──────────────────────────────────
149
150    /// Returns a reference to the command map (for internal pipeline use)
151    #[allow(dead_code)]
152    pub(crate) fn get_commands(&self) -> &HashMap<String, Arc<dyn Command>> {
153        &self.commands
154    }
155
156    /// Returns a copy of the current map (for pipeline output)
157    #[allow(dead_code)]
158    pub(crate) fn get_map_copy(&self) -> HashMap<AamlString, AamlString, Hasher> {
159        self.map.clone()
160    }
161
162    // ── Type registry ────────────────────────────────────────────────────────
163
164    /// Registers a custom command handler.
165    pub fn register_command<C: Command + 'static>(&mut self, command: C) {
166        self.commands
167            .insert(command.name().to_string(), Arc::new(command));
168    }
169
170    /// Registers a named type definition for use in schema field validation.
171    pub fn register_type<T: Type + 'static>(&mut self, name: String, type_def: T) {
172        self.types.insert(name, Box::new(type_def));
173    }
174
175    /// Returns the type handler registered under `name`, or `None`.
176    #[must_use]
177    pub fn get_type(&self, name: &str) -> Option<&dyn Type> {
178        self.types.get(name).map(AsRef::as_ref)
179    }
180
181    /// Removes the type registered under `name`.
182    pub fn unregister_type(&mut self, name: &str) {
183        self.types.remove(name);
184    }
185
186    /// Validates `value` against a type registered under `type_name`.
187    ///
188    /// # Errors
189    /// Returns `AamlError` if the type is not found or validation fails.
190    pub fn check_type(&self, type_name: &str, value: &str) -> Result<(), AamlError> {
191        self.types
192            .get(type_name)
193            .ok_or_else(|| AamlError::NotFound {
194                key: type_name.to_string(),
195                context: "type registry".to_string(),
196                diagnostics: Some(Box::new(ErrorDiagnostics::new(
197                    "Type not found",
198                    format!("Type '{type_name}' is not registered"),
199                    "Check your @type directives or use a built-in type",
200                ))),
201            })?
202            .validate(value, self)
203    }
204
205    /// Validates `value` against the type registered as `type_name`, also
206    /// resolving built-in primitive types and module paths.
207    ///
208    /// # Errors
209    /// Returns `AamlError` if the type is not found or validation fails.
210    pub fn validate_value(&self, type_name: &str, value: &str) -> Result<(), AamlError> {
211        let make_err = |e: AamlError| AamlError::InvalidType {
212            type_name: type_name.to_string(),
213            details: e.to_string(),
214            provided: value.to_string(),
215            diagnostics: None,
216        };
217
218        if let Some(type_def) = self.types.get(type_name) {
219            return type_def.validate(value, self).map_err(make_err);
220        }
221
222        resolve_builtin(type_name)
223            .map_err(|_| AamlError::NotFound {
224                key: type_name.to_string(),
225                context: "builtin type registry".to_string(),
226                diagnostics: Some(Box::new(ErrorDiagnostics::new(
227                    "Type not found",
228                    format!("Type '{type_name}' is neither registered nor a built-in type"),
229                    "Check the type name or register a custom type with @type",
230                ))),
231            })?
232            .validate(value, self)
233            .map_err(make_err)
234    }
235
236    // ── Parsing ──────────────────────────────────────────────────────────────
237
238    /// Parses AAML content from a string, merging it into this instance.
239    ///
240    /// # Errors
241    /// Returns `AamlError` if parsing fails (e.g., syntax errors, IO errors).
242    pub fn merge_content(&mut self, content: &str) -> Result<(), AamlError> {
243        self.map.reserve(content.len() / 40);
244        let mut pending: Option<(String, usize)> = None;
245
246        for (i, line) in content.lines().enumerate() {
247            let line_num = i + 1;
248            if let Some(result) = self.accumulate_or_process(line, line_num, &mut pending)? {
249                self.process_line(&result.0, result.1)?;
250            }
251        }
252
253        if let Some((buf, start)) = pending {
254            self.process_line(&buf, start)?;
255        }
256        Ok(())
257    }
258
259    /// Returns a mutable reference to the registered type definitions.
260    /// Used by commands like `@type` to add new types at parse time.
261    /// The keys are the type names, and the values are boxed trait objects implementing [`Type`].
262    pub(crate) fn get_types_mut(&mut self) -> &mut HashMap<String, Box<dyn Type>> {
263        &mut self.types
264    }
265
266    /// Handles one source line: either appends it to a pending multi-line block
267    /// or processes it immediately. Returns `Some((text, line_num))` when a
268    /// complete directive has been accumulated and is ready to process.
269    fn accumulate_or_process(
270        &mut self,
271        line: &str,
272        line_num: usize,
273        pending: &mut Option<(String, usize)>,
274    ) -> Result<Option<(String, usize)>, AamlError> {
275        if let Some((buf, start)) = pending {
276            buf.push(' ');
277            buf.push_str(parsing::strip_comment(line).trim());
278            if parsing::block_is_complete(buf) {
279                let complete = buf.clone();
280                let start_line = *start;
281                *pending = None;
282                return Ok(Some((complete, start_line)));
283            }
284            return Ok(None);
285        }
286
287        let stripped = parsing::strip_comment(line).trim();
288        if parsing::needs_accumulation(stripped) {
289            *pending = Some((stripped.to_string(), line_num));
290            return Ok(None);
291        }
292
293        self.process_stripped_line(stripped, line_num)?;
294        Ok(None)
295    }
296
297    /// Reads a file from the disk and merges its content into this instance.
298    pub fn merge_file<P: AsRef<Path>>(&mut self, file_path: P) -> Result<(), AamlError> {
299        let content = fs::read_to_string(file_path)?;
300        self.merge_content(&content)
301    }
302
303    /// Parses an AAML string and returns a new [`AAML`] instance.
304    pub fn parse(content: &str) -> Result<Self, AamlError> {
305        let mut aaml = Self::new();
306        aaml.merge_content(content)?;
307        Ok(aaml)
308    }
309
310    /// Loads an AAML file from the disk and returthe ns a new [`AAML`] instance.
311    pub fn load<P: AsRef<Path>>(file_path: P) -> Result<Self, AamlError> {
312        let file_path = file_path.as_ref();
313        let content = fs::read_to_string(file_path)?;
314        let mut aaml = Self::parse(&content)?;
315        aaml.source_dir = file_path.parent().map(Path::to_path_buf);
316        Ok(aaml)
317    }
318
319    /// Strips surrounding `"…"` or `'…'` quotes. Returns the trimmed string unchanged
320    /// if it is not quoted.
321    #[must_use]
322    pub fn unwrap_quotes(s: &str) -> &str {
323        parsing::unwrap_quotes(s)
324    }
325
326    pub fn import_schema(&mut self, name: &str, source: &mut Self) -> Result<(), AamlError> {
327        // Already imported (e.g., as a transitive dependency of another schema) — skip.
328        if self.get_schemas().contains_key(name) {
329            return Ok(());
330        }
331
332        let schema =
333            source
334                .get_schemas_mut()
335                .remove(name)
336                .ok_or_else(|| AamlError::DirectiveError {
337                    directive: "derive".to_string(),
338                    message: format!("Schema '{name}' not found"),
339                    diagnostics: Some(Box::new(ErrorDiagnostics::new(
340                        "Schema not found for inheritance",
341                        format!("Cannot derive from schema '{name}': it does not exist"),
342                        "Ensure the schema is defined before using @derive",
343                    ))),
344                })?;
345
346        // Collect field type strings before `schema` is moved into the map.
347        let field_type_strings: Vec<String> = schema.fields.values().cloned().collect();
348
349        // Insert the schema first so that circular references do not recurse infinitely.
350        self.get_schemas_mut()
351            .entry(name.to_string())
352            .or_insert(schema);
353
354        for ty_str in &field_type_strings {
355            let ty_name = ListType::parse_inner(ty_str).unwrap_or_else(|| ty_str.clone());
356
357            // Try to pull in a registered type alias first.
358            if let Some(ty_def) = source.get_types_mut().remove(&ty_name) {
359                self.get_types_mut().entry(ty_name).or_insert(ty_def);
360                continue;
361            }
362
363            // If the inner type name refers to another schema, import it recursively
364            // so that nested schema validation (e.g. `list<Author>`) works correctly.
365            if source.get_schemas().contains_key(&ty_name)
366                && !self.get_schemas().contains_key(&ty_name)
367            {
368                self.import_schema(&ty_name, source)?;
369            }
370        }
371
372        Ok(())
373    }
374
375    pub fn merge_map_weak(&mut self, other_map: &mut HashMap<AamlString, AamlString, Hasher>) {
376        for (k, v) in other_map.drain() {
377            self.get_map_mut().entry(k).or_insert(v);
378        }
379    }
380
381    /// Returns all keys currently stored in the map.
382    #[must_use]
383    pub fn keys(&self) -> Vec<&str> {
384        self.map.keys().map(AsRef::as_ref).collect()
385    }
386
387    /// Returns all key-value pairs as a `HashMap<String, String>`.
388    #[must_use]
389    pub fn to_map(&self) -> HashMap<String, String> {
390        self.map
391            .iter()
392            .map(|(k, v)| (k.to_string(), v.to_string()))
393            .collect()
394    }
395
396    // ── Private helpers ──────────────────────────────────────────────────────
397
398    fn register_default_commands(&mut self) {
399        self.register_command(commands::import::ImportCommand);
400        self.register_command(commands::typecm::TypeCommand);
401        self.register_command(commands::schema::SchemaCommand);
402        self.register_command(commands::derive::DeriveCommand);
403    }
404
405    fn process_line(&mut self, raw_line: &str, line_num: usize) -> Result<(), AamlError> {
406        let line = parsing::strip_comment(raw_line).trim();
407        self.process_stripped_line(line, line_num)
408    }
409
410    fn process_stripped_line(&mut self, line: &str, line_num: usize) -> Result<(), AamlError> {
411        if line.is_empty() {
412            return Ok(());
413        }
414        if let Some(rest) = line.strip_prefix('@') {
415            return self.process_directive(rest, line_num);
416        }
417        self.process_assignment(line, line_num)
418    }
419
420    fn process_assignment(&mut self, line: &str, line_num: usize) -> Result<(), AamlError> {
421        match parsing::parse_assignment(line) {
422            Ok((key, value)) => {
423                self.validate_against_schemas(key, value)?;
424                self.map.insert(Box::from(key), Box::from(value));
425                Ok(())
426            }
427            Err(mut err) => {
428                if let AamlError::MalformedLiteral { diagnostics, .. } = &mut err
429                    && diagnostics.is_none()
430                {
431                    *diagnostics = Some(Box::new(ErrorDiagnostics::new(
432                        "Failed to parse assignment",
433                        format!("Line {line_num}: '{line}'"),
434                        "Check the format: key = value",
435                    )));
436                }
437                if let AamlError::InvalidValue { diagnostics, .. } = &mut err
438                    && diagnostics.is_none()
439                {
440                    *diagnostics = Some(Box::new(ErrorDiagnostics::new(
441                        "Invalid assignment value",
442                        format!("Line {line_num}: '{line}'"),
443                        "Ensure key and value are properly formatted",
444                    )));
445                }
446                Err(err)
447            }
448        }
449    }
450
451    fn process_directive(&mut self, content: &str, line_num: usize) -> Result<(), AamlError> {
452        let mut parts = content.splitn(2, char::is_whitespace);
453        let command_name = parts.next().unwrap_or("").trim();
454        let args = parts.next().unwrap_or("");
455
456        if command_name.is_empty() {
457            return Err(AamlError::ParseError {
458                line: line_num,
459                content: content.to_string(),
460                details: "Empty directive".to_string(),
461                diagnostics: Some(Box::new(ErrorDiagnostics::new(
462                    "Empty directive",
463                    "Directive name is missing after '@'",
464                    "Use format: @directive_name args",
465                ))),
466            });
467        }
468
469        self.commands.get(command_name).cloned().map_or_else(
470            || {
471                Err(AamlError::ParseError {
472                    line: line_num,
473                    content: content.to_string(),
474                    details: format!("Unknown directive: @{command_name}"),
475                    diagnostics: Some(Box::new(ErrorDiagnostics::new(
476                        "Unknown directive",
477                        format!("Directive '@{command_name}' is not recognized"),
478                        "Known directives: @import, @derive, @schema, @type",
479                    ))),
480                })
481            },
482            |cmd| cmd.execute(self, args),
483        )
484    }
485}
486
487#[cfg(feature = "legacy")]
488impl Add for AAML {
489    type Output = Self;
490
491    fn add(mut self, rhs: Self) -> Self {
492        self.map.reserve(rhs.map.len());
493        self.map.extend(rhs.map);
494        self.types.extend(rhs.types);
495        self
496    }
497}
498
499#[cfg(feature = "legacy")]
500impl AddAssign for AAML {
501    fn add_assign(&mut self, rhs: Self) {
502        self.map.reserve(rhs.map.len());
503        self.map.extend(rhs.map);
504        self.types.extend(rhs.types);
505    }
506}
507
508#[cfg(feature = "legacy")]
509impl Default for AAML {
510    fn default() -> Self {
511        Self::new()
512    }
513}