Skip to main content

aam_core/
aam.rs

1#[cfg(feature = "aot")]
2use crate::aot::{AamCompiler, AamLoader, MappedAam};
3use crate::error::{AamlError, set_error_render_context};
4use crate::pipeline::{
5    DefaultLexer, DefaultParser, FormatRange, FormattingOptions, Lexer, Parser, Pipeline,
6    PipelineHashMap, PipelineOutput, SchemaInfo, TypeInfo,
7};
8use smol_str::SmolStr;
9use std::path::Path;
10
11/// Represents the underlying storage backend for AAM.
12/// Can be either a dynamic in-memory structure or a zero-copy memory-mapped file.
13#[derive(Debug)]
14pub enum AamBackend {
15    /// Fully parsed dynamic in-memory output.
16    Dynamic(PipelineOutput),
17    /// Zero-copy memory-mapped AOT (Ahead-of-Time) binary cache.
18    #[cfg(feature = "aot")]
19    Mapped(MappedAam),
20}
21
22/// The main AAM configuration store.
23///
24/// Holds the final, validated output of the AAM pipeline, including
25/// the key-value map, schemas, and registered types.
26#[derive(Debug)]
27pub struct AAM {
28    backend: AamBackend,
29}
30
31/// LSP-oriented helper payload that returns diagnostics plus optional formatting output.
32#[derive(Debug)]
33pub struct AamLspAssist {
34    pub diagnostics: Vec<AamlError>,
35    pub formatted: Option<String>,
36}
37
38impl AAM {
39    /// Attempts to read `text` as a file path. Returns `Some((content, source_dir))` if it is,
40    /// or `None` if it should be treated as raw AAM content.
41    fn try_read_file(text: &str) -> Result<Option<(String, Option<&Path>)>, Vec<AamlError>> {
42        let path = Path::new(text);
43        if path.is_file() {
44            let content = std::fs::read_to_string(path).map_err(|e| {
45                vec![AamlError::IoError {
46                    details: format!("failed to read '{}': {e}", path.display()),
47                    diagnostics: None,
48                }]
49            })?;
50            Ok(Some((content, path.parent())))
51        } else {
52            Ok(None)
53        }
54    }
55
56    fn parse_with_source_name(source_name: &str, text: &str) -> Result<Self, Vec<AamlError>> {
57        set_error_render_context(source_name.to_string(), text);
58        let pipeline = Pipeline::new();
59        let output = pipeline.process(text)?;
60
61        Ok(Self {
62            backend: AamBackend::Dynamic(output),
63        })
64    }
65
66    /// Creates an empty dynamic AAM document.
67    #[must_use]
68    pub fn new() -> Self {
69        Self {
70            backend: AamBackend::Dynamic(PipelineOutput::new()),
71        }
72    }
73
74    /// Parses an AAM string using the default Pipeline and returns a new [`AAM`] instance.
75    ///
76    /// If `text` is a path to an existing file on disk, it is loaded and parsed dynamically
77    /// (with full schema support), bypassing the AOT cache. Otherwise, `text` is treated as
78    /// raw AAM content.
79    ///
80    /// # Errors
81    ///
82    /// Returns errors if the file cannot be read, the content fails to parse, or validation fails.
83    pub fn parse(text: &str) -> Result<Self, Vec<AamlError>> {
84        if let Some((content, source_dir)) = Self::try_read_file(text)? {
85            set_error_render_context(text.to_string(), &content);
86            let pipeline = Pipeline::new();
87            let output = pipeline.process_with_source_dir(&content, source_dir)?;
88            Ok(Self {
89                backend: AamBackend::Dynamic(output),
90            })
91        } else {
92            Self::parse_with_source_name("raw_string", text)
93        }
94    }
95
96    /// Creates an [`AAM`] instance from a custom configured Pipeline.
97    /// Use this if you need to register custom commands, parsers, or validators.
98    ///
99    /// If `text` is a path to an existing file on disk, it is loaded and parsed as a file.
100    /// Otherwise, `text` is treated as raw AAM content.
101    ///
102    /// # Errors
103    ///
104    /// Returns errors if the file cannot be read, the content fails to parse, or validation fails.
105    pub fn from_pipeline(pipeline: &Pipeline, text: &str) -> Result<Self, Vec<AamlError>> {
106        if let Some((content, source_dir)) = Self::try_read_file(text)? {
107            set_error_render_context(text.to_string(), &content);
108            let output = pipeline.process_with_source_dir(&content, source_dir)?;
109            Ok(Self {
110                backend: AamBackend::Dynamic(output),
111            })
112        } else {
113            set_error_render_context("raw_string", text);
114            let output = pipeline.process(text)?;
115            Ok(Self {
116                backend: AamBackend::Dynamic(output),
117            })
118        }
119    }
120
121    /// Loads an `.aam` file from disk.
122    ///
123    /// With `aot` enabled (default), this uses the cooked `.aam.bin ` cache as the
124    /// primary path and only invokes parsing/cooking when the cache is missing/stale.
125    /// This provides zero-copy memory mapping without any allocations.
126    ///
127    /// # Errors
128    ///
129    /// Returns errors if the file cannot be read, the cache cannot be memory-mapped,
130    /// or the content fails to parse/validate.
131    pub fn load(path: impl AsRef<Path>) -> Result<Self, Vec<AamlError>> {
132        #[cfg(feature = "aot")]
133        {
134            let mapped = AamLoader::load_fast(path)?;
135            Ok(Self {
136                backend: AamBackend::Mapped(mapped),
137            })
138        }
139
140        #[cfg(not(feature = "aot"))]
141        {
142            let path = path.as_ref();
143            let content = std::fs::read_to_string(path).map_err(|e| {
144                vec![AamlError::IoError {
145                    details: format!("failed to read '{}': {e}", path.display()),
146                    diagnostics: None,
147                }]
148            })?;
149
150            let source_name = path.display().to_string();
151            set_error_render_context(source_name.clone(), &content);
152            let pipeline = Pipeline::new();
153            let source_dir = path.parent();
154            let output = pipeline.process_with_source_dir(&content, source_dir)?;
155
156            Ok(Self {
157                backend: AamBackend::Dynamic(output),
158            })
159        }
160    }
161
162    /// Formats arbitrary AAM content using parser + pipeline formatter.
163    ///
164    /// # Errors
165    ///
166    /// Returns errors if parsing or formatting fails.
167    pub fn format(&self, content: &str, options: &FormattingOptions) -> Result<String, AamlError> {
168        set_error_render_context("<format>", content);
169        let lexer = DefaultLexer::new();
170        let parser = DefaultParser::new();
171        let tokens = lexer.tokenize(content)?;
172        let crate::pipeline::parser::ParseOutput { ast, errors } =
173            parser.parse_with_recovery(&tokens);
174
175        if let Some(first_error) = errors.into_iter().next() {
176            return Err(first_error);
177        }
178
179        Pipeline::new().format(&ast, options)
180    }
181
182    /// Formats only a selected line range of arbitrary AAM content.
183    ///
184    /// # Errors
185    ///
186    /// Returns errors if parsing or formatting fails.
187    pub fn format_range(
188        &self,
189        content: &str,
190        range: FormatRange,
191        options: &FormattingOptions,
192    ) -> Result<String, AamlError> {
193        set_error_render_context("<format>", content);
194        let lexer = DefaultLexer::new();
195        let parser = DefaultParser::new();
196        let tokens = lexer.tokenize(content)?;
197        let crate::pipeline::parser::ParseOutput { ast, errors } =
198            parser.parse_with_recovery(&tokens);
199
200        if let Some(first_error) = errors.into_iter().next() {
201            return Err(first_error);
202        }
203
204        Pipeline::new().format_range(&ast, range, options)
205    }
206
207    /// Convenience method for LSP servers: parse with recovery and optional formatting result.
208    #[must_use]
209    pub fn lsp_assist(content: &str, options: &FormattingOptions) -> AamLspAssist {
210        set_error_render_context("<lsp>", content);
211        let lexer = DefaultLexer::new();
212        let parser = DefaultParser::new();
213
214        let tokens = match lexer.tokenize(content) {
215            Ok(tokens) => tokens,
216            Err(err) => {
217                return AamLspAssist {
218                    diagnostics: vec![err],
219                    formatted: None,
220                };
221            }
222        };
223
224        let parse_output = parser.parse_with_recovery(&tokens);
225        let formatted = if parse_output.errors.is_empty() {
226            Pipeline::new().format(&parse_output.ast, options).ok()
227        } else {
228            None
229        };
230
231        AamLspAssist {
232            diagnostics: parse_output.errors,
233            formatted,
234        }
235    }
236
237    /// Explicitly cooks an `.aam` file into `.aam.bin` cache.
238    ///
239    /// # Errors
240    ///
241    /// Returns errors if the file cannot be read, the cache cannot be created,
242    /// or the content fails to parse/validate.
243    #[cfg(feature = "aot")]
244    pub fn cook(path: impl AsRef<Path>) -> Result<std::path::PathBuf, Vec<AamlError>> {
245        AamCompiler::cook(path)
246    }
247
248    /// Exposes zero-copy fast loading for advanced runtime integrations.
249    ///
250    /// # Errors
251    ///
252    /// Returns errors if the file cannot be read, the cache cannot be memory-mapped,
253    /// or the source file is invalid and rebuilding fails.
254    #[cfg(feature = "aot")]
255    pub fn load_fast(path: impl AsRef<Path>) -> Result<MappedAam, Vec<AamlError>> {
256        AamLoader::load_fast(path)
257    }
258
259    // ── Search & Filtering ───────────────────────────────────────────
260
261    /// Deep Search: Finds all key-value pairs where the key contains the specified pattern.
262    #[must_use]
263    pub fn deep_search(&self, pattern: &str) -> Vec<(&str, &str)> {
264        self.iter().filter(|(k, _)| k.contains(pattern)).collect()
265    }
266
267    /// Reverse Search: Finds all keys that match the specified target value.
268    #[must_use]
269    pub fn reverse_search(&self, target_value: &str) -> Vec<&str> {
270        self.iter()
271            .filter(|(_, v)| *v == target_value)
272            .map(|(k, _)| k)
273            .collect()
274    }
275
276    /// Advanced search using a custom predicate function.
277    pub fn find_by<F>(&self, predicate: F) -> Vec<(&str, &str)>
278    where
279        F: Fn(&str, &str) -> bool,
280    {
281        self.iter().filter(|(k, v)| predicate(k, v)).collect()
282    }
283
284    /// Find by key or value with fallback.
285    /// First tries to find exactly by key (O(1) lookup), if not found,
286    /// searches for matching values (O(N) iteration).
287    #[must_use]
288    pub fn find<'a>(&'a self, query: &'a str) -> Vec<(&'a str, &'a str)> {
289        if let Some(v) = self.get(query) {
290            return vec![(query, v)];
291        }
292
293        self.iter().filter(|(_, v)| *v == query).collect()
294    }
295
296    // ── Key-Value Data Accessors ─────────────────────────────────────────────
297
298    /// Retrieves a string value by its key. Performs an O(1) lookup.
299    /// When AOT is enabled, this is a zero-copy operation straight from the memory-mapped file.
300    #[inline]
301    #[must_use]
302    pub fn get(&self, key: &str) -> Option<&str> {
303        match &self.backend {
304            AamBackend::Dynamic(output) => output.map.get(key).map(AsRef::as_ref),
305            #[cfg(feature = "aot")]
306            AamBackend::Mapped(mapped) => mapped.get(key),
307        }
308    }
309
310    /// Iterates over all key-value pairs without allocating memory.
311    /// Supports both dynamic `HashMaps` and memory-mapped AOT iterators.
312    #[must_use]
313    pub fn iter(&self) -> Box<dyn Iterator<Item = (&str, &str)> + '_> {
314        match &self.backend {
315            AamBackend::Dynamic(output) => {
316                Box::new(output.map.iter().map(|(k, v)| (k.as_ref(), v.as_ref())))
317            }
318            #[cfg(feature = "aot")]
319            AamBackend::Mapped(mapped) => Box::new(mapped.iter_pairs()),
320        }
321    }
322
323    /// Returns all keys currently stored.
324    /// Prefer [`AAM::iter`] for zero-allocation iteration.
325    #[inline]
326    #[must_use]
327    pub fn keys(&self) -> Vec<&str> {
328        self.iter().map(|(k, _)| k).collect()
329    }
330
331    /// Returns all key-value pairs as a standard allocated map.
332    /// Prefer [`AAM::iter`] for zero-allocation iteration.
333    #[inline]
334    #[must_use]
335    pub fn to_map(&self) -> PipelineHashMap<String, String> {
336        self.iter()
337            .map(|(k, v)| (k.to_string(), v.to_string()))
338            .collect()
339    }
340
341    // ── Schema & Type Accessors ──────────────────────────────────────────────
342
343    /// Returns a reference to all registered schemas, if loaded dynamically.
344    /// Returns `None` if the configuration was loaded via an AOT memory map.
345    #[inline]
346    #[must_use]
347    pub const fn schemas(&self) -> Option<&PipelineHashMap<SmolStr, SchemaInfo>> {
348        match &self.backend {
349            AamBackend::Dynamic(output) => Some(&output.schemas),
350            #[cfg(feature = "aot")]
351            AamBackend::Mapped(_) => None,
352        }
353    }
354
355    /// Returns a specific schema by name, if it exists and was loaded dynamically.
356    #[must_use]
357    pub fn get_schema(&self, name: &str) -> Option<&SchemaInfo> {
358        self.schemas().and_then(|schemas| schemas.get(name))
359    }
360
361    /// Returns a reference to all registered types, if loaded dynamically.
362    /// Returns `None` if the configuration was loaded via an AOT memory map.
363    #[inline]
364    #[must_use]
365    pub const fn types(&self) -> Option<&PipelineHashMap<SmolStr, TypeInfo>> {
366        match &self.backend {
367            AamBackend::Dynamic(output) => Some(&output.types),
368            #[cfg(feature = "aot")]
369            AamBackend::Mapped(_) => None,
370        }
371    }
372
373    /// Returns a specific type info by name, if it exists and was loaded dynamically.
374    #[must_use]
375    pub fn get_type(&self, name: &str) -> Option<&TypeInfo> {
376        self.types().and_then(|types| types.get(name))
377    }
378}
379
380impl Default for AAM {
381    fn default() -> Self {
382        Self::new()
383    }
384}
385
386impl<'a> IntoIterator for &'a AAM {
387    type Item = (&'a str, &'a str);
388    type IntoIter = Box<dyn Iterator<Item = (&'a str, &'a str)> + 'a>;
389
390    fn into_iter(self) -> Self::IntoIter {
391        self.iter()
392    }
393}