Skip to main content

aam_core/pipeline/
executer.rs

1//! Executer stage: materializes validated tasks into the final runtime state.
2//!
3//! The Executer receives an `ExecutionDescriptor` containing pre-computed tasks
4//! and executes them in a completely decoupled manner, WITHOUT any AAML struct dependency.
5//! This enables clean separation of concerns, better LSP integration, and potential
6//! parallel execution in future iterations.
7
8use crate::error::AamlError;
9use crate::error::ErrorDiagnostics;
10use crate::pipeline::PipelineHashMap;
11use crate::pipeline::execution_descriptor::ExecutionDescriptor;
12use crate::pipeline::tasks::ExecutionTask;
13use crate::pipeline::utils::resolve_relative_path;
14use bumpalo::Bump;
15use smol_str::SmolStr;
16use std::path::Path;
17
18/// Trait for executing the final materializationthe of configuration state.
19///
20/// The Executer operates purely on `ExecutionDescriptor` and task queues.
21/// It NO LONGER instantiates or depends on the AAML struct.
22pub trait Executer: Send + Sync {
23    /// Executes the manifest to produce a final `FoundValue`.
24    ///
25    /// # Arguments
26    /// - `manifest`: Comprehensive execution manifest with all tasks and context
27    ///
28    /// # Returns
29    /// - `Ok(FoundValue)` on successful execution
30    /// - `Err(AamlError)` if any execution phase fails
31    fn execute(&self, manifest: &mut ExecutionDescriptor) -> Result<(), AamlError>;
32}
33
34/// Default implementation of the Executer stage.
35///
36/// This executor handles all execution tasks in a clean, isolated manner:
37/// 1. Processes `SetValue` and `MergeValue` tasks to populate the output map
38/// 2. Handles `ApplySchema` tasks for schema validation and enforcement
39/// 3. Manages `ExecuteInheritance` for configuration inheritance
40/// 4. Resolves cross-references via `ResolveReference` tasks
41/// 5. Handles `ImportFile` tasks for external configuration merging
42pub struct DefaultExecuter {
43    // Can hold shared registries or execution strategies
44}
45
46impl DefaultExecuter {
47    #[must_use]
48    pub const fn new() -> Self {
49        Self {}
50    }
51
52    fn set_value(output_map: &mut PipelineHashMap<SmolStr, SmolStr>, key: &str, value: &str) {
53        output_map.insert(SmolStr::from(key), SmolStr::from(value));
54    }
55
56    fn merge_value(output_map: &mut PipelineHashMap<SmolStr, SmolStr>, key: &str, value: &str) {
57        let existing = output_map
58            .get(key)
59            .map(ToString::to_string)
60            .unwrap_or_default();
61
62        let merged: std::borrow::Cow<'_, str> = if existing.is_empty() {
63            value.into()
64        } else {
65            format!("{existing} {value}").into()
66        };
67
68        output_map.insert(SmolStr::from(key), SmolStr::from(merged.as_ref()));
69    }
70
71    fn build_full_key(root: &str, field: &str) -> String {
72        if root.is_empty() {
73            field.to_string()
74        } else {
75            format!("{root}.{field}")
76        }
77    }
78
79    fn schema_not_found_error(schema_name: &str) -> AamlError {
80        AamlError::NotFound {
81            key: schema_name.to_string(),
82            context: "schema registry".to_string(),
83            diagnostics: Some(Box::new(ErrorDiagnostics::new(
84                "Schema not found",
85                format!("Schema '{schema_name}' does not exist"),
86                "Check your @schema definitions",
87            ))),
88        }
89    }
90
91    fn schema_field_error(
92        schema_name: &str,
93        field: &str,
94        type_name: &str,
95        details: String,
96    ) -> AamlError {
97        AamlError::SchemaValidationError {
98            schema: schema_name.to_string(),
99            field: field.to_string(),
100            type_name: type_name.to_string(),
101            details,
102            diagnostics: None,
103        }
104    }
105
106    fn validate_schema_field(
107        schema_name: &str,
108        field: &str,
109        type_name: &str,
110        is_optional: bool,
111        full_key: &str,
112        output_map: &PipelineHashMap<SmolStr, SmolStr>,
113        context: &crate::pipeline::execution_descriptor::ExecutionContext,
114    ) -> Result<(), AamlError> {
115        let Some(value) = output_map.get(full_key) else {
116            if is_optional {
117                return Ok(());
118            }
119
120            return Err(Self::schema_field_error(
121                schema_name,
122                field,
123                type_name,
124                format!("Missing required field '{field}'"),
125            ));
126        };
127
128        if let Err(err_msg) = crate::pipeline::utils::validate_type_value(value, type_name, context)
129        {
130            return Err(Self::schema_field_error(
131                schema_name,
132                field,
133                type_name,
134                format!(
135                    "Type mismatch for field '{}': {}",
136                    field,
137                    err_msg.short_message()
138                ),
139            ));
140        }
141
142        Ok(())
143    }
144
145    fn apply_schema(
146        schema_name: &str,
147        root_keys: &[std::borrow::Cow<'_, str>],
148        output_map: &PipelineHashMap<SmolStr, SmolStr>,
149        context: &crate::pipeline::execution_descriptor::ExecutionContext,
150    ) -> Result<(), AamlError> {
151        let schema = context
152            .schemas
153            .get(schema_name)
154            .ok_or_else(|| Self::schema_not_found_error(schema_name))?;
155
156        for key in root_keys {
157            for (field, (type_name, is_optional)) in &schema.fields {
158                let full_key = Self::build_full_key(key.as_ref(), field);
159                Self::validate_schema_field(
160                    schema_name,
161                    field,
162                    type_name,
163                    *is_optional,
164                    &full_key,
165                    output_map,
166                    context,
167                )?;
168            }
169        }
170
171        Ok(())
172    }
173
174    fn child_field_key(child_key: &str, field: &str) -> String {
175        if child_key.is_empty() {
176            field.to_string()
177        } else {
178            format!("{child_key}.{field}")
179        }
180    }
181
182    fn apply_inherited_field_default(
183        child_key: &str,
184        field: &str,
185        type_name: &str,
186        output_map: &mut PipelineHashMap<SmolStr, SmolStr>,
187        context: &crate::pipeline::execution_descriptor::ExecutionContext,
188    ) {
189        let full_child_key = Self::child_field_key(child_key, field);
190        if output_map.contains_key(full_child_key.as_str()) {
191            return;
192        }
193
194        if let Some(default_val) = context.default_value_for_type(type_name) {
195            output_map.insert(SmolStr::new(&full_child_key), SmolStr::new(default_val));
196        }
197    }
198
199    fn execute_inheritance(
200        derive_path: &str,
201        child_key: &str,
202        output_map: &mut PipelineHashMap<SmolStr, SmolStr>,
203        source_dir: Option<&Path>,
204        context: &crate::pipeline::execution_descriptor::ExecutionContext,
205    ) -> Result<(), AamlError> {
206        let parts: Vec<&str> = derive_path.split("::").collect();
207        if parts.is_empty() {
208            return Ok(());
209        }
210
211        // Load the base file and merge its key-value pairs with child-wins
212        // semantics: existing child keys are never overwritten.
213        let raw_path = parts[0];
214        let resolved = resolve_relative_path(raw_path, source_dir);
215        let content = std::fs::read_to_string(&resolved).map_err(|e| AamlError::IoError {
216            details: e.to_string(),
217            diagnostics: Some(Box::new(ErrorDiagnostics::new(
218                "I/O operation failed",
219                format!(
220                    "Could not read derived file '{}': {}",
221                    resolved.display(),
222                    e
223                ),
224                "Check if the file exists and is readable",
225            ))),
226        })?;
227
228        let sub_pipeline = crate::pipeline::Pipeline::new();
229        let sub_output = sub_pipeline
230            .process_with_source_dir(&content, resolved.parent())
231            .map_err(|errors| {
232                errors
233                    .into_iter()
234                    .next()
235                    .unwrap_or_else(|| AamlError::DirectiveError {
236                        directive: "derive".to_string(),
237                        message: "Unknown error in derived file".to_string(),
238                        diagnostics: None,
239                    })
240            })?;
241
242        for (k, v) in sub_output.map {
243            if !output_map.contains_key(&*k) {
244                output_map.insert(k, v);
245            }
246        }
247
248        // Apply defaults for explicitly selected schemas.
249        for schema_name in &parts[1..] {
250            if let Some(schema) = context.schemas.get(*schema_name) {
251                for (field, (type_name, _is_optional)) in &schema.fields {
252                    Self::apply_inherited_field_default(
253                        child_key, field, type_name, output_map, context,
254                    );
255                }
256            }
257        }
258
259        Ok(())
260    }
261
262    fn import_file(
263        file_path: &str,
264        merge_strategy: &str,
265        output_map: &mut PipelineHashMap<SmolStr, SmolStr>,
266        source_dir: Option<&Path>,
267    ) -> Result<(), AamlError> {
268        let resolved = resolve_relative_path(file_path, source_dir);
269        let content = std::fs::read_to_string(&resolved).map_err(|e| AamlError::IoError {
270            details: e.to_string(),
271            diagnostics: Some(Box::new(ErrorDiagnostics::new(
272                "I/O operation failed",
273                format!(
274                    "Could not read imported file '{}': {}",
275                    resolved.display(),
276                    e
277                ),
278                "Check file permissions and ensure the path exists",
279            ))),
280        })?;
281
282        let sub_pipeline = crate::pipeline::Pipeline::new();
283        let arena = Bump::new();
284        let sub_output = match sub_pipeline.process_with_arena_and_source_dir(
285            &content,
286            &arena,
287            resolved.parent(),
288        ) {
289            Ok(out) => out,
290            Err(errors) => {
291                return Err(errors.into_iter().next().unwrap_or_else(|| {
292                    AamlError::DirectiveError {
293                        directive: "import".to_string(),
294                        message: "Unknown error in imported file".to_string(),
295                        diagnostics: None,
296                    }
297                }));
298            }
299        };
300
301        for (k, v) in sub_output.map {
302            if merge_strategy == "override" || !output_map.contains_key(&*k) {
303                output_map.insert(k, v);
304            }
305        }
306        Ok(())
307    }
308
309    fn resolve_reference(
310        source_key: &str,
311        target_key: &str,
312        output_map: &mut PipelineHashMap<SmolStr, SmolStr>,
313    ) -> Result<(), AamlError> {
314        if let Some(target_value) = output_map.get(target_key) {
315            output_map.insert(SmolStr::from(source_key), target_value.clone());
316            return Ok(());
317        }
318
319        Err(AamlError::NotFound {
320            key: target_key.to_string(),
321            context: format!(
322                "Reference target '{target_key}' not found when resolving '{source_key}'"
323            ),
324            diagnostics: None,
325        })
326    }
327
328    /// Executes a single execution task within the context.
329    fn execute_task(
330        task: &ExecutionTask,
331        output_map: &mut PipelineHashMap<SmolStr, SmolStr>,
332        context: &crate::pipeline::execution_descriptor::ExecutionContext,
333    ) -> Result<(), AamlError> {
334        match task {
335            ExecutionTask::SetValue { key, value, .. } => {
336                Self::set_value(output_map, key.as_ref(), value.as_ref());
337                Ok(())
338            }
339            ExecutionTask::MergeValue { key, value, .. } => {
340                Self::merge_value(output_map, key.as_ref(), value.as_ref());
341                Ok(())
342            }
343            ExecutionTask::ApplySchema {
344                schema_name,
345                root_keys,
346                line: _,
347            } => Self::apply_schema(schema_name.as_ref(), root_keys, output_map, context),
348            ExecutionTask::ExecuteInheritance {
349                derive_path,
350                child_key,
351                line: _,
352            } => Self::execute_inheritance(
353                derive_path.as_ref(),
354                child_key.as_ref(),
355                output_map,
356                crate::pipeline::source_dir::get().as_deref(),
357                context,
358            ),
359            ExecutionTask::ImportFile {
360                file_path,
361                merge_strategy,
362                line: _,
363            } => Self::import_file(
364                file_path.as_ref(),
365                merge_strategy.as_ref(),
366                output_map,
367                crate::pipeline::source_dir::get().as_deref(),
368            ),
369            ExecutionTask::ResolveReference {
370                source_key,
371                target_key,
372                ..
373            } => Self::resolve_reference(source_key.as_ref(), target_key.as_ref(), output_map),
374        }
375    }
376}
377
378impl Default for DefaultExecuter {
379    fn default() -> Self {
380        Self::new()
381    }
382}
383
384impl Executer for DefaultExecuter {
385    fn execute(&self, manifest: &mut ExecutionDescriptor) -> Result<(), AamlError> {
386        // Move map out to avoid cloning on every execution pass.
387        let mut output_map = std::mem::take(&mut manifest.context.map);
388        let context = &manifest.context;
389
390        // Execute all execution tasks in order
391        for task in &manifest.execution_tasks {
392            Self::execute_task(task, &mut output_map, context)?;
393        }
394
395        // Update the manifest context with final output
396        manifest.context.map = output_map;
397
398        Ok(())
399    }
400}