Skip to main content

txtx_core/runbook/
mod.rs

1use diffing_context::ConsolidatedPlanChanges;
2use flow_context::FlowContext;
3use kit::indexmap::IndexMap;
4use kit::types::cloud_interface::CloudServiceContext;
5use kit::types::frontend::ActionItemRequestType;
6use kit::types::types::AddonJsonConverter;
7use kit::types::{ConstructDid, RunbookInstanceContext};
8use serde_json::{json, Value as JsonValue};
9use std::collections::{HashMap, HashSet, VecDeque};
10use txtx_addon_kit::hcl::structure::BlockLabel;
11use txtx_addon_kit::hcl::Span;
12use txtx_addon_kit::helpers::fs::FileLocation;
13use txtx_addon_kit::helpers::hcl::RawHclContent;
14use txtx_addon_kit::types::commands::{CommandExecutionResult, DependencyExecutionResultCache};
15use txtx_addon_kit::types::diagnostics::DiagnosticSpan;
16use txtx_addon_kit::types::stores::ValueStore;
17use txtx_addon_kit::types::types::RunbookSupervisionContext;
18use txtx_addon_kit::types::{diagnostics::Diagnostic, types::Value};
19use txtx_addon_kit::types::{AuthorizationContext, Did, PackageId, RunbookId};
20use txtx_addon_kit::Addon;
21
22pub mod collector;
23mod diffing_context;
24pub mod embedded_runbook;
25mod execution_context;
26pub mod flow_context;
27mod graph_context;
28pub mod location;
29mod runtime_context;
30pub mod variables;
31mod workspace_context;
32
33pub use diffing_context::ConsolidatedChanges;
34pub use diffing_context::{RunbookExecutionSnapshot, RunbookSnapshotContext, SynthesizedChange};
35pub use execution_context::{RunbookExecutionContext, RunbookExecutionMode};
36pub use graph_context::RunbookGraphContext;
37pub use runtime_context::{AddonConstructFactory, RuntimeContext};
38pub use workspace_context::RunbookWorkspaceContext;
39
40use crate::manifest::{RunbookStateLocation, RunbookTransientStateLocation};
41
42#[derive(Debug)]
43pub struct Runbook {
44    /// Id of the Runbook
45    pub runbook_id: RunbookId,
46    /// Description of the Runbook
47    pub description: Option<String>,
48    /// The runtime context keeps track of all the functions, commands, and signing commands in scope during execution
49    pub runtime_context: RuntimeContext,
50    /// Running contexts
51    pub flow_contexts: Vec<FlowContext>,
52    /// The supervision context keeps track of the supervision settings the runbook is executing under
53    pub supervision_context: RunbookSupervisionContext,
54    /// Source files
55    pub sources: RunbookSources,
56    // The store that will contain _all_ of the environment variables (mainnet,testnet,etc), consolidated with the CLI inputs
57    pub top_level_inputs_map: RunbookTopLevelInputsMap,
58}
59
60impl Runbook {
61    fn get_no_addons_by_namespace(_namepace: &str) -> Option<Box<dyn Addon>> {
62        None
63    }
64    pub fn new(runbook_id: RunbookId, description: Option<String>) -> Self {
65        Self {
66            runbook_id,
67            description,
68            flow_contexts: vec![],
69            runtime_context: RuntimeContext::new(
70                AuthorizationContext::empty(),
71                Runbook::get_no_addons_by_namespace,
72                CloudServiceContext::empty(),
73            ),
74            sources: RunbookSources::new(),
75            supervision_context: RunbookSupervisionContext::new(),
76            top_level_inputs_map: RunbookTopLevelInputsMap::new(),
77        }
78    }
79
80    pub fn runbook_id(&self) -> RunbookId {
81        self.runbook_id.clone()
82    }
83
84    pub fn to_instance_context(&self) -> RunbookInstanceContext {
85        RunbookInstanceContext {
86            runbook_id: self.runbook_id.clone(),
87            workspace_location: self
88                .runtime_context
89                .authorization_context
90                .workspace_location
91                .clone(),
92            environment_selector: self.top_level_inputs_map.current_environment.clone(),
93        }
94    }
95
96    pub fn enable_full_execution_mode(&mut self) {
97        for r in self.flow_contexts.iter_mut() {
98            r.execution_context.execution_mode = RunbookExecutionMode::Full
99        }
100    }
101
102    /// Initializes the flow contexts of the runbook.
103    /// This method is called when the runbook is first loaded.
104    /// It initializes the flow contexts of the runbook by parsing the source code and evaluating the top-level inputs.
105    /// If the runbook has no flow blocks, a default flow context is created based on the currently selected top-level inputs environment.
106    pub fn initialize_flow_contexts(
107        &self,
108        runtime_context: &RuntimeContext,
109        runbook_sources: &RunbookSources,
110        top_level_inputs_map: &RunbookTopLevelInputsMap,
111    ) -> Result<Vec<FlowContext>, Diagnostic> {
112        let mut dummy_workspace_context = RunbookWorkspaceContext::new(self.runbook_id.clone());
113        let mut dummy_execution_context = RunbookExecutionContext::new();
114
115        let current_top_level_value_store = top_level_inputs_map.current_top_level_inputs();
116
117        for (key, value) in current_top_level_value_store.iter() {
118            let construct_did = dummy_workspace_context.index_top_level_input(key, value);
119
120            let mut result = CommandExecutionResult::new();
121            result.outputs.insert("value".into(), value.clone());
122            dummy_execution_context.commands_execution_results.insert(construct_did, result);
123        }
124
125        let mut sources = runbook_sources.to_vec_dequeue();
126        let dependencies_execution_results = DependencyExecutionResultCache::new();
127
128        let mut flow_contexts = vec![];
129
130        let mut package_ids = vec![];
131
132        // we need to call flow_context.workspace_context.index_package and flow_context.graph_context.index_package
133        // for each flow_context and each package_id, even if the flow is defined in a different package.
134        // we also can't index the flow inputs until we have indexed the packages
135        // so first we need to create each of the flows by parsing the hcl and finding the flow blocks
136        let mut flow_map = vec![];
137        while let Some((location, package_name, raw_content)) = sources.pop_front() {
138            let package_id =
139                PackageId::from_file(&location, &self.runbook_id, &package_name).map_err(|e| e)?;
140            package_ids.push(package_id.clone());
141
142            let mut blocks = raw_content.into_typed_blocks().map_err(|diag| diag.location(&location))?;
143
144            while let Some(typed_block) = blocks.pop_front() {
145                use crate::types::ConstructType;
146                match &typed_block.construct_type {
147                    Ok(ConstructType::Flow) => {
148                        let Some(BlockLabel::String(name)) = typed_block.labels.first() else {
149                            continue;
150                        };
151                        let flow_name = name.to_string();
152                        let flow_context = FlowContext::new(
153                            &flow_name,
154                            &self.runbook_id,
155                            &current_top_level_value_store,
156                        );
157                        flow_map.push((flow_context, typed_block.body.attributes().cloned().collect()));
158                    }
159                    _ => {}
160                }
161            }
162        }
163
164        // if the user didn't specify any flows, we'll create a default one based on the current top-level inputs
165        if flow_map.is_empty() {
166            let flow_name = top_level_inputs_map.current_top_level_input_name();
167            let flow_context =
168                FlowContext::new(&flow_name, &self.runbook_id, &current_top_level_value_store);
169            flow_map.push((flow_context, vec![]));
170        }
171
172        // next we need to index the packages for each flow and evaluate the flow inputs
173        for (flow_context, attributes) in flow_map.iter_mut() {
174            for package_id in package_ids.iter() {
175                flow_context.workspace_context.index_package(package_id);
176                flow_context.graph_context.index_package(package_id);
177                flow_context.index_flow_inputs_from_attributes(
178                    attributes,
179                    &dependencies_execution_results,
180                    package_id,
181                    &dummy_workspace_context,
182                    &dummy_execution_context,
183                    runtime_context,
184                )?;
185            }
186            flow_contexts.push(flow_context.to_owned());
187        }
188
189        Ok(flow_contexts)
190    }
191
192    /// Clears all flow contexts stored on the runbook.
193    pub async fn build_contexts_from_sources(
194        &mut self,
195        sources: RunbookSources,
196        top_level_inputs_map: RunbookTopLevelInputsMap,
197        authorization_context: AuthorizationContext,
198        get_addon_by_namespace: fn(&str) -> Option<Box<dyn Addon>>,
199        cloud_service_context: CloudServiceContext,
200    ) -> Result<bool, Vec<Diagnostic>> {
201        // Re-initialize some shiny new contexts
202        self.flow_contexts.clear();
203        let mut runtime_context = RuntimeContext::new(
204            authorization_context,
205            get_addon_by_namespace,
206            cloud_service_context,
207        );
208
209        // Index our flow contexts
210        let mut flow_contexts = self
211            .initialize_flow_contexts(&runtime_context, &sources, &top_level_inputs_map)
212            .map_err(|e| vec![e])?;
213
214        // At this point we know if some batching is required
215        for flow_context in flow_contexts.iter_mut() {
216            // Step 1: identify the addons at play and their globals
217            runtime_context.register_addons_from_sources(
218                &mut flow_context.workspace_context,
219                &self.runbook_id,
220                &sources,
221                &flow_context.execution_context,
222                &top_level_inputs_map.current_environment,
223            )?;
224            // Step 2: identify and index all the constructs (nodes)
225            flow_context
226                .workspace_context
227                .build_from_sources(
228                    &sources,
229                    &mut runtime_context,
230                    &mut flow_context.graph_context,
231                    &mut flow_context.execution_context,
232                    &top_level_inputs_map.current_environment,
233                )
234                .await?;
235
236            // Step 3: simulate inputs evaluation - some more edges could be hidden in there
237            flow_context
238                .execution_context
239                .simulate_inputs_execution(&runtime_context, &flow_context.workspace_context)
240                .await
241                .map_err(|diag| {
242                    vec![diag
243                        .clone()
244                        .set_diagnostic_span(get_source_context_for_diagnostic(&diag, &sources))]
245                })?;
246            // Step 4: let addons build domain aware dependencies
247            let domain_specific_dependencies = runtime_context
248                .perform_addon_processing(&mut flow_context.execution_context)
249                .map_err(|(diag, construct_did)| {
250                    let construct_id =
251                        &flow_context.workspace_context.expect_construct_id(&construct_did);
252                    let command_instance = flow_context
253                        .execution_context
254                        .commands_instances
255                        .get(&construct_did)
256                        .unwrap();
257                    let diag = diag
258                        .location(&construct_id.construct_location)
259                        .set_span_range(command_instance.block.span());
260                    vec![diag
261                        .clone()
262                        .set_diagnostic_span(get_source_context_for_diagnostic(&diag, &sources))]
263                })?;
264            // Step 5: identify and index all the relationships between the constructs (edges)
265            flow_context
266                .graph_context
267                .build(
268                    &mut flow_context.execution_context,
269                    &flow_context.workspace_context,
270                    domain_specific_dependencies,
271                )
272                .map_err(|diags| {
273                    diags
274                        .into_iter()
275                        .map(|diag| {
276                            diag.clone().set_diagnostic_span(get_source_context_for_diagnostic(
277                                &diag, &sources,
278                            ))
279                        })
280                        .collect::<Vec<_>>()
281                })?;
282        }
283
284        // Final step: Update contexts
285        self.flow_contexts = flow_contexts;
286        self.runtime_context = runtime_context;
287        self.sources = sources;
288        self.top_level_inputs_map = top_level_inputs_map;
289        Ok(true)
290    }
291
292    pub fn find_expected_flow_context_mut(&mut self, key: &str) -> &mut FlowContext {
293        for flow_context in self.flow_contexts.iter_mut() {
294            if flow_context.name.eq(key) {
295                return flow_context;
296            }
297        }
298        unreachable!()
299    }
300
301    pub async fn update_inputs_selector(
302        &mut self,
303        selector: Option<String>,
304        force: bool,
305    ) -> Result<bool, Vec<Diagnostic>> {
306        // Ensure that the value of the selector is changing
307        if !force && selector.eq(&self.top_level_inputs_map.current_environment) {
308            return Ok(false);
309        }
310
311        // Ensure that the selector exists
312        if let Some(ref entry) = selector {
313            if !self.top_level_inputs_map.environments.contains(entry) {
314                return Err(vec![Diagnostic::error_from_string(format!(
315                    "input '{}' unknown from inputs map",
316                    entry
317                ))]);
318            }
319        }
320        // Rebuild contexts
321        let mut inputs_map = self.top_level_inputs_map.clone();
322        inputs_map.current_environment = selector;
323        let authorization_context: AuthorizationContext =
324            self.runtime_context.authorization_context.clone();
325        let cloud_service_context: CloudServiceContext =
326            self.runtime_context.cloud_service_context.clone();
327
328        self.build_contexts_from_sources(
329            self.sources.clone(),
330            inputs_map,
331            authorization_context,
332            self.runtime_context.addons_context.get_addon_by_namespace,
333            cloud_service_context,
334        )
335        .await
336    }
337
338    pub fn get_inputs_selectors(&self) -> Vec<String> {
339        self.top_level_inputs_map.environments.clone()
340    }
341
342    pub fn get_active_inputs_selector(&self) -> Option<String> {
343        self.top_level_inputs_map.current_environment.clone()
344    }
345
346    pub fn backup_execution_contexts(&self) -> HashMap<String, RunbookExecutionContext> {
347        let mut execution_context_backups = HashMap::new();
348        for flow_context in self.flow_contexts.iter() {
349            let execution_context_backup = flow_context.execution_context.clone();
350            execution_context_backups.insert(flow_context.name.clone(), execution_context_backup);
351        }
352        execution_context_backups
353    }
354
355    pub async fn simulate_and_snapshot_flows(
356        &mut self,
357        old_snapshot: &RunbookExecutionSnapshot,
358    ) -> Result<RunbookExecutionSnapshot, String> {
359        let ctx = RunbookSnapshotContext::new();
360
361        for flow_context in self.flow_contexts.iter_mut() {
362            let frontier = HashSet::new();
363            let _res = flow_context
364                .execution_context
365                .simulate_execution(
366                    &self.runtime_context,
367                    &flow_context.workspace_context,
368                    &self.supervision_context,
369                    &frontier,
370                )
371                .await;
372
373            let Some(flow_snapshot) = old_snapshot.flows.get(&flow_context.name) else {
374                continue;
375            };
376
377            // since our simulation results are limited, apply the old snapshot on top of the gaps in
378            // our simulated execution context
379            flow_context
380                .execution_context
381                .apply_snapshot_to_execution_context(flow_snapshot, &flow_context.workspace_context)
382                .map_err(|e| e.message)?;
383        }
384
385        let new = ctx
386            .snapshot_runbook_execution(
387                &self.runbook_id,
388                &self.flow_contexts,
389                None,
390                &self.top_level_inputs_map,
391            )
392            .map_err(|e| e.message)?;
393        Ok(new)
394    }
395
396    pub fn prepare_flows_for_new_plans(
397        &mut self,
398        new_plans_to_add: &Vec<String>,
399        execution_context_backups: HashMap<String, RunbookExecutionContext>,
400    ) {
401        for flow_context_key in new_plans_to_add.iter() {
402            let flow_context = self.find_expected_flow_context_mut(&flow_context_key);
403            flow_context.execution_context.execution_mode = RunbookExecutionMode::Full;
404            let pristine_execution_context =
405                execution_context_backups.get(flow_context_key).unwrap();
406            flow_context.execution_context = pristine_execution_context.clone();
407        }
408    }
409
410    pub fn prepared_flows_for_updated_plans(
411        &mut self,
412        plans_to_update: &IndexMap<String, ConsolidatedPlanChanges>,
413    ) -> (
414        IndexMap<String, Vec<(String, Option<String>)>>,
415        IndexMap<String, Vec<(String, Option<String>)>>,
416    ) {
417        let mut actions_to_re_execute = IndexMap::new();
418        let mut actions_to_execute = IndexMap::new();
419
420        for (flow_context_key, changes) in plans_to_update.iter() {
421            let critical_edits = changes
422                .constructs_to_update
423                .iter()
424                .filter(|c| !c.description.is_empty() && c.critical)
425                .collect::<Vec<_>>();
426
427            let additions = changes.new_constructs_to_add.iter().collect::<Vec<_>>();
428            let mut unexecuted =
429                changes.constructs_to_run.iter().map(|(e, _)| e.clone()).collect::<Vec<_>>();
430
431            let flow_context = self.find_expected_flow_context_mut(&flow_context_key);
432
433            if critical_edits.is_empty() && additions.is_empty() && unexecuted.is_empty() {
434                flow_context.execution_context.execution_mode = RunbookExecutionMode::Ignored;
435                continue;
436            }
437
438            let mut added_construct_dids: Vec<ConstructDid> =
439                additions.into_iter().map(|(construct_did, _)| construct_did.clone()).collect();
440
441            let mut descendants_of_critically_changed_commands = critical_edits
442                .iter()
443                .filter_map(|c| {
444                    if let Some(construct_did) = &c.construct_did {
445                        let mut segment = vec![];
446                        segment.push(construct_did.clone());
447                        let mut deps = flow_context
448                            .graph_context
449                            .get_downstream_dependencies_for_construct_did(&construct_did, true);
450                        segment.append(&mut deps);
451                        Some(segment)
452                    } else {
453                        None
454                    }
455                })
456                .flatten()
457                .filter(|d| !added_construct_dids.contains(d))
458                .collect::<Vec<_>>();
459            descendants_of_critically_changed_commands.sort();
460            descendants_of_critically_changed_commands.dedup();
461
462            let actions: Vec<(String, Option<String>)> = descendants_of_critically_changed_commands
463                .iter()
464                .map(|construct_did| {
465                    let documentation = flow_context
466                        .execution_context
467                        .commands_inputs_evaluation_results
468                        .get(construct_did)
469                        .and_then(|r| r.inputs.get_string("description"))
470                        .and_then(|d| Some(d.to_string()));
471                    let command = flow_context
472                        .execution_context
473                        .commands_instances
474                        .get(construct_did)
475                        .unwrap();
476                    (command.name.to_string(), documentation)
477                })
478                .collect();
479            actions_to_re_execute.insert(flow_context_key.clone(), actions);
480
481            let added_actions: Vec<(String, Option<String>)> = added_construct_dids
482                .iter()
483                .map(|construct_did| {
484                    let documentation = flow_context
485                        .execution_context
486                        .commands_inputs_evaluation_results
487                        .get(construct_did)
488                        .and_then(|r| r.inputs.get_string("description"))
489                        .and_then(|d| Some(d.to_string()));
490                    let command = flow_context
491                        .execution_context
492                        .commands_instances
493                        .get(construct_did)
494                        .unwrap();
495                    (command.name.to_string(), documentation)
496                })
497                .collect();
498            actions_to_execute.insert(flow_context_key.clone(), added_actions);
499
500            let mut great_filter = descendants_of_critically_changed_commands;
501            great_filter.append(&mut added_construct_dids);
502            great_filter.append(&mut unexecuted);
503
504            for construct_did in great_filter.iter() {
505                let _ =
506                    flow_context.execution_context.commands_execution_results.remove(construct_did);
507            }
508
509            flow_context.execution_context.order_for_commands_execution = flow_context
510                .execution_context
511                .order_for_commands_execution
512                .clone()
513                .into_iter()
514                .filter(|c| great_filter.contains(&c))
515                .collect();
516
517            flow_context.execution_context.execution_mode =
518                RunbookExecutionMode::Partial(great_filter);
519        }
520
521        (actions_to_re_execute, actions_to_execute)
522    }
523
524    pub fn write_runbook_state(
525        &self,
526        runbook_state_location: Option<RunbookStateLocation>,
527    ) -> Result<Option<FileLocation>, String> {
528        if let Some(state_file_location) = runbook_state_location {
529            let previous_snapshot = match state_file_location.load_execution_snapshot(
530                true,
531                &self.runbook_id.name,
532                &self.top_level_inputs_map.current_top_level_input_name(),
533            ) {
534                Ok(snapshot) => Some(snapshot),
535                Err(_e) => None,
536            };
537
538            let state_file_location = state_file_location.get_location_for_ctx(
539                &self.runbook_id.name,
540                Some(&self.top_level_inputs_map.current_top_level_input_name()),
541            );
542            if let Some(RunbookTransientStateLocation(lock_file)) =
543                RunbookTransientStateLocation::from_state_file_location(&state_file_location)
544            {
545                let _ = std::fs::remove_file(&lock_file.to_string());
546            }
547
548            let diff = RunbookSnapshotContext::new();
549            let snapshot = diff
550                .snapshot_runbook_execution(
551                    &self.runbook_id,
552                    &self.flow_contexts,
553                    previous_snapshot,
554                    &self.top_level_inputs_map,
555                )
556                .map_err(|e| e.message)?;
557            state_file_location
558                .write_content(serde_json::to_string_pretty(&snapshot).unwrap().as_bytes())
559                .expect("unable to save state");
560            Ok(Some(state_file_location))
561        } else {
562            Ok(None)
563        }
564    }
565
566    pub fn mark_failed_and_write_transient_state(
567        &mut self,
568        runbook_state_location: Option<RunbookStateLocation>,
569    ) -> Result<Option<FileLocation>, String> {
570        for running_context in self.flow_contexts.iter_mut() {
571            running_context.execution_context.execution_mode = RunbookExecutionMode::FullFailed;
572        }
573
574        if let Some(runbook_state_location) = runbook_state_location {
575            let previous_snapshot = match runbook_state_location.load_execution_snapshot(
576                false,
577                &self.runbook_id.name,
578                &self.top_level_inputs_map.current_top_level_input_name(),
579            ) {
580                Ok(snapshot) => Some(snapshot),
581                Err(_e) => None,
582            };
583
584            let lock_file = RunbookTransientStateLocation::get_location_from_state_file_location(
585                &runbook_state_location.get_location_for_ctx(
586                    &self.runbook_id.name,
587                    Some(&self.top_level_inputs_map.current_top_level_input_name()),
588                ),
589            );
590            let diff = RunbookSnapshotContext::new();
591            let snapshot = diff
592                .snapshot_runbook_execution(
593                    &self.runbook_id,
594                    &self.flow_contexts,
595                    previous_snapshot,
596                    &self.top_level_inputs_map,
597                )
598                .map_err(|e| e.message)?;
599            lock_file
600                .write_content(serde_json::to_string_pretty(&snapshot).unwrap().as_bytes())
601                .map_err(|e| format!("unable to save state ({})", e.to_string()))?;
602            Ok(Some(lock_file))
603        } else {
604            Ok(None)
605        }
606    }
607
608    pub fn collect_formatted_outputs(&self) -> RunbookOutputs {
609        let mut runbook_outputs = RunbookOutputs::new();
610        for flow_context in self.flow_contexts.iter() {
611            let grouped_actions_items = flow_context
612                .execution_context
613                .collect_outputs_constructs_results(&self.runtime_context.authorization_context);
614            for (_, items) in grouped_actions_items.iter() {
615                for item in items.iter() {
616                    if let ActionItemRequestType::DisplayOutput(ref output) = item.action_type {
617                        runbook_outputs.add_output(
618                            &flow_context.name,
619                            &output.name,
620                            &output.value,
621                            &output.description,
622                        );
623                    }
624                }
625            }
626        }
627        runbook_outputs
628    }
629}
630
631#[derive(Clone, Debug)]
632pub struct RunbookOutputs {
633    outputs: IndexMap<String, IndexMap<String, (Value, Option<String>)>>,
634}
635impl RunbookOutputs {
636    pub fn new() -> Self {
637        Self { outputs: IndexMap::new() }
638    }
639
640    pub fn add_output(
641        &mut self,
642        flow_name: &str,
643        output_name: &str,
644        output_value: &Value,
645        output_description: &Option<String>,
646    ) {
647        let flow_outputs = self.outputs.entry(flow_name.to_string()).or_insert_with(IndexMap::new);
648        flow_outputs
649            .insert(output_name.to_string(), (output_value.clone(), output_description.clone()));
650    }
651
652    /// Organizes the outputs in a format suitable to be displayed using the `AsciiTable` crate.
653    pub fn get_output_row_data(
654        &self,
655        filter: &Option<String>,
656    ) -> IndexMap<String, Vec<Vec<String>>> {
657        let mut output_row_data = IndexMap::new();
658        for (flow_name, flow_outputs) in self.outputs.iter() {
659            let mut flow_output_row =
660                vec![vec!["name".to_string(), "value".to_string(), "description".to_string()]];
661            for (output_name, (output_value, output_description)) in flow_outputs.iter() {
662                if let Some(ref filter) = filter {
663                    if !output_name.contains(filter) {
664                        continue;
665                    }
666                }
667
668                let mut row = vec![];
669                row.push(output_name.to_string());
670                row.push(output_value.to_string());
671                row.push(output_description.clone().unwrap_or_else(|| "".to_string()));
672                flow_output_row.push(row);
673            }
674            output_row_data.insert(flow_name.to_string(), flow_output_row);
675        }
676        output_row_data
677    }
678
679    pub fn to_json(&self, addon_converters: &Vec<AddonJsonConverter>) -> JsonValue {
680        let mut json = json!({});
681        let only_one_flow = self.outputs.len() == 1;
682        for (flow_name, flow_outputs) in self.outputs.iter() {
683            let mut flow_json = json!({});
684            for (output_name, (output_value, output_description)) in flow_outputs.iter() {
685                let mut output_json = json!({});
686                output_json["value"] = output_value.to_json(Some(&addon_converters));
687                if let Some(ref output_description) = output_description {
688                    output_json["description"] = output_description.clone().into();
689                }
690                flow_json[output_name] = output_json;
691            }
692            if only_one_flow {
693                return flow_json;
694            }
695            json[flow_name] = flow_json;
696        }
697        json
698    }
699
700    pub fn is_empty(&self) -> bool {
701        if self.outputs.is_empty() {
702            return true;
703        }
704        let mut empty = true;
705        for (_, outputs) in self.outputs.iter() {
706            if !outputs.is_empty() {
707                empty = false;
708            }
709        }
710        empty
711    }
712}
713
714#[derive(Clone, Debug)]
715pub struct RunbookTopLevelInputsMap {
716    current_environment: Option<String>,
717    environments: Vec<String>,
718    values: HashMap<Option<String>, Vec<(String, Value)>>,
719}
720
721pub const DEFAULT_TOP_LEVEL_INPUTS_NAME: &str = "default";
722pub const GLOBAL_TOP_LEVEL_INPUTS_NAME: &str = "global";
723
724impl RunbookTopLevelInputsMap {
725    pub fn new() -> Self {
726        Self { current_environment: None, environments: vec![], values: HashMap::new() }
727    }
728    pub fn from_environment_map(
729        selector: &Option<String>,
730        environments_map: &IndexMap<String, IndexMap<String, String>>,
731    ) -> Self {
732        let mut environments = vec![];
733        let mut values = HashMap::from_iter([(None, vec![])]);
734
735        let mut global_values = vec![];
736        if let Some(global_env_vars) = environments_map.get(GLOBAL_TOP_LEVEL_INPUTS_NAME) {
737            for (key, value) in global_env_vars.iter() {
738                global_values.push((key.to_string(), Value::parse_and_default_to_string(value)));
739            }
740        };
741
742        for (selector, inputs) in environments_map.iter() {
743            if selector.eq(GLOBAL_TOP_LEVEL_INPUTS_NAME) {
744                continue; // Skip global inputs, their values are added to all environments but should not be listed as an environment
745            }
746            let mut env_values = vec![];
747            // Add global values to all environments
748            for (key, value) in global_values.iter() {
749                env_values.push((key.to_string(), value.clone()));
750            }
751            // _Then_ add the environment specific values, overwriting the global ones in the case of collisions
752            for (key, value) in inputs.iter() {
753                env_values.push((key.to_string(), Value::parse_and_default_to_string(value)));
754            }
755            environments.push(selector.to_string());
756            values.insert(Some(selector.to_string()), env_values);
757        }
758
759        Self {
760            current_environment: selector.clone().or(environments.get(0).map(|v| v.to_string())),
761            environments,
762            values,
763        }
764    }
765
766    pub fn current_top_level_input_name(&self) -> String {
767        self.current_environment
768            .clone()
769            .unwrap_or_else(|| DEFAULT_TOP_LEVEL_INPUTS_NAME.to_string())
770    }
771
772    pub fn current_top_level_inputs(&self) -> ValueStore {
773        let empty_vec = vec![];
774        let name = self.current_top_level_input_name();
775        let raw_inputs = self.values.get(&self.current_environment).unwrap_or(&empty_vec);
776        let current_map = ValueStore::new(&name, &Did::zero()).with_inputs_from_vec(raw_inputs);
777        current_map
778    }
779
780    pub fn override_values_with_cli_inputs(
781        &mut self,
782        inputs: &Vec<String>,
783        buffer_stdin: Option<String>,
784    ) -> Result<(), String> {
785        for input in inputs.iter() {
786            let Some((input_name, input_value)) = input.split_once("=") else {
787                return Err(format!(
788                    "expected --input argument to be formatted as '{}', got '{}'",
789                    "key=value", input
790                ));
791            };
792            let input_value = match (input_value.eq("←"), &buffer_stdin) {
793                (true, Some(v)) => v.to_string(),
794                _ => input_value.to_string(),
795            };
796            let new_value = Value::parse_and_default_to_string(&input_value);
797            for (_, values) in self.values.iter_mut() {
798                let mut found = false;
799                for (k, old_value) in values.iter_mut() {
800                    if k.eq(&input_name) {
801                        *old_value = new_value.clone();
802                        found = true;
803                    }
804                }
805                if !found {
806                    values.push((input_name.to_string(), new_value.clone()));
807                }
808            }
809        }
810        Ok(())
811    }
812}
813
814#[derive(Clone, Debug)]
815pub struct RunbookSources {
816    /// Map of files required to construct the runbook
817    pub tree: HashMap<FileLocation, (String, RawHclContent)>,
818}
819
820impl RunbookSources {
821    pub fn new() -> Self {
822        Self { tree: HashMap::new() }
823    }
824
825    pub fn add_source(&mut self, name: String, location: FileLocation, content: String) {
826        self.tree.insert(location, (name, RawHclContent::from_string(content)));
827    }
828
829    pub fn to_vec_dequeue(&self) -> VecDeque<(FileLocation, String, RawHclContent)> {
830        self.tree
831            .iter()
832            .map(|(file_location, (package_name, raw_content))| {
833                (file_location.clone(), package_name.clone(), raw_content.clone())
834            })
835            .collect()
836    }
837}
838
839pub fn get_source_context_for_diagnostic(
840    diag: &Diagnostic,
841    runbook_sources: &RunbookSources,
842) -> Option<DiagnosticSpan> {
843    let Some(construct_location) = &diag.location else {
844        return None;
845    };
846    let Some(span_range) = &diag.span_range() else {
847        return None;
848    };
849
850    let Some((_, (_, raw_content))) =
851        runbook_sources.tree.iter().find(|(location, _)| location.eq(&construct_location))
852    else {
853        return None;
854    };
855    let raw_content_string = raw_content.to_string();
856    let mut lines = 1;
857    let mut cols = 1;
858    let mut span = DiagnosticSpan::new();
859
860    let mut chars = raw_content_string.chars().enumerate().peekable();
861    while let Some((i, ch)) = chars.next() {
862        if i == span_range.start {
863            span.line_start = lines;
864            span.column_start = cols;
865        }
866        if i == span_range.end {
867            span.line_end = lines;
868            span.column_end = cols;
869        }
870        match ch {
871            '\n' => {
872                lines += 1;
873                cols = 1;
874            }
875            '\r' => {
876                // check for \r\n
877                if let Some((_, '\n')) = chars.peek() {
878                    // Skip the next character
879                    chars.next();
880                    lines += 1;
881                    cols = 1;
882                } else {
883                    cols += 1;
884                }
885            }
886            _ => {
887                cols += 1;
888            }
889        }
890    }
891    Some(span)
892}