1use 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
18pub trait Executer: Send + Sync {
23 fn execute(&self, manifest: &mut ExecutionDescriptor) -> Result<(), AamlError>;
32}
33
34pub struct DefaultExecuter {
43 }
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 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 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 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 let mut output_map = std::mem::take(&mut manifest.context.map);
388 let context = &manifest.context;
389
390 for task in &manifest.execution_tasks {
392 Self::execute_task(task, &mut output_map, context)?;
393 }
394
395 manifest.context.map = output_map;
397
398 Ok(())
399 }
400}