Skip to main content

txtx_core/runbook/
runtime_context.rs

1use kit::indexmap::IndexMap;
2use kit::types::cloud_interface::CloudServiceContext;
3use std::collections::HashMap;
4use std::collections::VecDeque;
5use txtx_addon_kit::types::commands::DependencyExecutionResultCache;
6use txtx_addon_kit::types::stores::AddonDefaults;
7use txtx_addon_kit::types::stores::ValueStore;
8use txtx_addon_kit::{
9    hcl::structure::{Block, BlockLabel},
10    helpers::fs::FileLocation,
11    types::{
12        commands::{
13            CommandId, CommandInputsEvaluationResult, CommandInstance, CommandInstanceType,
14            PreCommandSpecification,
15        },
16        diagnostics::Diagnostic,
17        functions::FunctionSpecification,
18        signers::{SignerInstance, SignerSpecification},
19        types::Value,
20        AuthorizationContext, ConstructDid, ContractSourceTransform, Did, PackageDid, PackageId,
21        RunbookId,
22    },
23    Addon,
24};
25
26use crate::eval::eval_expression;
27use crate::{
28    eval::{self, ExpressionEvaluationStatus},
29    std::StdAddon,
30};
31
32use super::{
33    RunbookExecutionContext, RunbookSources, RunbookTopLevelInputsMap, RunbookWorkspaceContext,
34};
35
36#[derive(Debug)]
37pub struct RuntimeContext {
38    /// Functions accessible at runtime
39    pub functions: HashMap<String, FunctionSpecification>,
40    /// Addons instantiated by runtime
41    pub addons_context: AddonsContext,
42    /// Number of threads allowed to work on the inputs_sets concurrently
43    pub concurrency: u64,
44    /// Authorizations settings to propagate to function execution
45    pub authorization_context: AuthorizationContext,
46    /// Cloud service configuration
47    pub cloud_service_context: CloudServiceContext,
48}
49
50impl RuntimeContext {
51    pub fn new(
52        authorization_context: AuthorizationContext,
53        get_addon_by_namespace: fn(&str) -> Option<Box<dyn Addon>>,
54        cloud_service_context: CloudServiceContext,
55    ) -> RuntimeContext {
56        RuntimeContext {
57            functions: HashMap::new(),
58            addons_context: AddonsContext::new(get_addon_by_namespace),
59            concurrency: 1,
60            authorization_context,
61            cloud_service_context,
62        }
63    }
64
65    pub fn generate_initial_input_sets(
66        &self,
67        inputs_map: &RunbookTopLevelInputsMap,
68    ) -> Vec<ValueStore> {
69        let mut inputs_sets = vec![];
70        let default_name = "default".to_string();
71        let name = inputs_map.current_environment.as_ref().unwrap_or(&default_name);
72
73        let mut values = ValueStore::new(name, &Did::zero());
74
75        if let Some(current_inputs) = inputs_map.values.get(&inputs_map.current_environment) {
76            values = values.with_inputs_from_vec(current_inputs);
77        }
78        inputs_sets.push(values);
79        inputs_sets
80    }
81
82    pub fn perform_addon_processing(
83        &self,
84        runbook_execution_context: &mut RunbookExecutionContext,
85    ) -> Result<HashMap<ConstructDid, Vec<ConstructDid>>, (Diagnostic, ConstructDid)> {
86        let mut consolidated_dependencies = HashMap::new();
87        let mut grouped_commands: HashMap<
88            String,
89            Vec<(ConstructDid, &CommandInstance, Option<&CommandInputsEvaluationResult>)>,
90        > = HashMap::new();
91        for (did, command_instance) in runbook_execution_context.commands_instances.iter() {
92            let inputs_simulation_results =
93                runbook_execution_context.commands_inputs_evaluation_results.get(did);
94            grouped_commands
95                .entry(command_instance.namespace.clone())
96                .and_modify(|e: &mut _| {
97                    e.push((did.clone(), command_instance, inputs_simulation_results))
98                })
99                .or_insert(vec![(did.clone(), command_instance, inputs_simulation_results)]);
100        }
101        let mut post_processing = vec![];
102        for (addon_key, commands_instances) in grouped_commands.drain() {
103            let Some((addon, _)) = self.addons_context.registered_addons.get(&addon_key) else {
104                continue;
105            };
106            let res =
107                addon.get_domain_specific_commands_inputs_dependencies(&commands_instances)?;
108            for (k, v) in res.dependencies.into_iter() {
109                consolidated_dependencies.insert(k, v);
110            }
111            post_processing.push(res.transforms);
112        }
113
114        let mut remapping_required = vec![];
115        for res in post_processing.iter() {
116            for (construct_did, transforms) in res.iter() {
117                let Some(inputs_evaluation_results) = runbook_execution_context
118                    .commands_inputs_evaluation_results
119                    .get_mut(construct_did)
120                else {
121                    continue;
122                };
123
124                for transform in transforms.iter() {
125                    match transform {
126                        ContractSourceTransform::FindAndReplace(from, to) => {
127                            let Ok(mut contract) =
128                                inputs_evaluation_results.inputs.get_expected_object("contract")
129                            else {
130                                continue;
131                            };
132                            let mut contract_source = match contract.get_mut("contract_source") {
133                                Some(Value::String(source)) => source.to_string(),
134                                _ => continue,
135                            };
136                            contract_source = contract_source.replace(from, to);
137                            contract
138                                .insert("contract_source".into(), Value::string(contract_source));
139                            inputs_evaluation_results
140                                .inputs
141                                .insert("contract", Value::object(contract));
142                        }
143                        ContractSourceTransform::RemapDownstreamDependencies(from, to) => {
144                            remapping_required.push((from, to));
145                        }
146                    }
147                }
148            }
149        }
150
151        Ok(consolidated_dependencies)
152    }
153
154    /// Checks if the provided `addon_id` matches the namespace of a supported addon
155    /// that is available in the `get_addon_by_namespace` fn of the [RuntimeContext].
156    /// If there is no match, returns [Vec<Diagnostic>].
157    /// If there is a match, the addon is registered to the `addons_context`, storing the [PackageDid] as additional context.
158    pub fn register_addon(
159        &mut self,
160        addon_id: &str,
161        package_did: &PackageDid,
162    ) -> Result<(), Vec<Diagnostic>> {
163        self.addons_context.register(package_did, addon_id, true).map_err(|e| vec![e])
164    }
165
166    pub fn register_standard_functions(&mut self) {
167        let std_addon = StdAddon::new();
168        for function in std_addon.get_functions().iter() {
169            self.functions.insert(function.name.clone(), function.clone());
170        }
171    }
172
173    pub fn register_addons_from_sources(
174        &mut self,
175        runbook_workspace_context: &mut RunbookWorkspaceContext,
176        runbook_id: &RunbookId,
177        runbook_sources: &RunbookSources,
178        runbook_execution_context: &RunbookExecutionContext,
179        _environment_selector: &Option<String>,
180    ) -> Result<(), Vec<Diagnostic>> {
181        {
182            let mut diagnostics = vec![];
183
184            let mut sources = runbook_sources.to_vec_dequeue();
185
186            // Register standard functions at the root level
187            self.register_standard_functions();
188
189            while let Some((location, package_name, raw_content)) = sources.pop_front() {
190                let package_id = PackageId::from_file(&location, &runbook_id, &package_name)
191                    .map_err(|e| vec![e])?;
192
193                self.addons_context.register(&package_id.did(), "std", false).unwrap();
194
195                let blocks = raw_content
196                    .into_typed_blocks()
197                    .map_err(|diag| vec![diag.location(&location)])?;
198
199                let _ = self
200                    .register_addons_from_blocks(
201                        blocks,
202                        &package_id,
203                        &location,
204                        runbook_workspace_context,
205                        runbook_execution_context,
206                    )
207                    .map_err(|diags| {
208                        diagnostics.extend(diags);
209                    });
210            }
211
212            if diagnostics.is_empty() {
213                return Ok(());
214            } else {
215                return Err(diagnostics);
216            }
217        }
218    }
219
220    pub fn register_addons_from_blocks(
221        &mut self,
222        mut blocks: VecDeque<txtx_addon_kit::types::typed_block::OwnedTypedBlock>,
223        package_id: &PackageId,
224        location: &FileLocation,
225        runbook_workspace_context: &mut RunbookWorkspaceContext,
226        runbook_execution_context: &RunbookExecutionContext,
227    ) -> Result<(), Vec<Diagnostic>> {
228        use crate::types::ConstructType;
229        let mut diagnostics = vec![];
230        let dependencies_execution_results = DependencyExecutionResultCache::new();
231        while let Some(typed_block) = blocks.pop_front() {
232            // parse addon blocks to load that addon
233            match &typed_block.construct_type {
234                Ok(ConstructType::Addon) => {
235                    let Some(BlockLabel::String(name)) = typed_block.labels.first() else {
236                        diagnostics.push(
237                            Diagnostic::error_from_string("addon name missing".into())
238                                .location(&location),
239                        );
240                        continue;
241                    };
242                    let addon_id = name.to_string();
243                    self.register_addon(&addon_id, &package_id.did())?;
244
245                    let existing_addon_defaults = runbook_workspace_context
246                        .addons_defaults
247                        .get(&(package_id.did(), addon_id.clone()))
248                        .cloned();
249                    let addon_defaults = self
250                        .generate_addon_defaults_from_block(
251                            existing_addon_defaults,
252                            &*typed_block,
253                            &addon_id,
254                            &package_id,
255                            &dependencies_execution_results,
256                            runbook_workspace_context,
257                            runbook_execution_context,
258                        )
259                        .map_err(|diag| vec![diag.location(&location)])?;
260
261                    runbook_workspace_context
262                        .addons_defaults
263                        .insert((package_id.did(), addon_id.clone()), addon_defaults);
264                }
265                _ => {}
266            }
267        }
268        if diagnostics.is_empty() {
269            return Ok(());
270        } else {
271            return Err(diagnostics);
272        }
273    }
274
275    pub fn generate_addon_defaults_from_block(
276        &self,
277        existing_addon_defaults: Option<AddonDefaults>,
278        block: &Block,
279        addon_id: &str,
280        package_id: &PackageId,
281        dependencies_execution_results: &DependencyExecutionResultCache,
282        runbook_workspace_context: &mut RunbookWorkspaceContext,
283        runbook_execution_context: &RunbookExecutionContext,
284    ) -> Result<AddonDefaults, Diagnostic> {
285        let mut addon_defaults = existing_addon_defaults.unwrap_or(AddonDefaults::new(addon_id));
286
287        let map_entries = self.evaluate_hcl_map_blocks(
288            block.body.blocks().collect(),
289            dependencies_execution_results,
290            package_id,
291            runbook_workspace_context,
292            runbook_execution_context,
293        )?;
294
295        for (key, value) in map_entries {
296            // don't check for duplicate keys in map evaluation
297            addon_defaults.insert(&key, Value::array(value));
298        }
299
300        for attribute in block.body.attributes() {
301            let eval_result: Result<ExpressionEvaluationStatus, Diagnostic> = eval::eval_expression(
302                &attribute.value,
303                &dependencies_execution_results,
304                &package_id,
305                runbook_workspace_context,
306                runbook_execution_context,
307                self,
308            );
309            let key = attribute.key.to_string();
310            let value = match eval_result {
311                Ok(ExpressionEvaluationStatus::CompleteOk(value)) => value,
312                Err(diag) => return Err(diag),
313                w => unimplemented!("{:?}", w),
314            };
315            if addon_defaults.contains_key(&key) {
316                return Err(diagnosed_error!(
317                    "duplicate key '{}' in '{}' addon defaults",
318                    key,
319                    addon_id
320                ));
321            }
322            addon_defaults.insert(&key, value);
323        }
324        Ok(addon_defaults)
325    }
326
327    /// Evaluates a list of map blocks, returning a map of the evaluated values.
328    /// The following hcl:
329    /// ```hcl
330    /// my_map_key {
331    ///     val1 = "test"
332    ///     nested_map {
333    ///         val2 = "test2"
334    ///     }
335    ///     nested_map {
336    ///         val2 = "test3"
337    ///     }
338    /// }
339    /// ```
340    /// will return:
341    /// ```rust,compile_fail
342    /// use txtx_addon_kit::indexmap::IndexMap;
343    /// use txtx_addon_kit::types::types::Value;
344    /// IndexMap::from_iter([
345    ///     (
346    ///         "my_map_key".to_string(),
347    ///         vec![
348    ///             Value::object(IndexMap::from_iter([
349    ///                 (
350    ///                     "val1".to_string(),
351    ///                     Value::string("test".to_string())
352    ///                 ),
353    ///                 (
354    ///                     "nested_map".to_string(),
355    ///                     Value::array(vec![
356    ///                         Value::object(
357    ///                             IndexMap::from_iter([
358    ///                                 ("val2".to_string(), Value::string("test2".to_string()))
359    ///                             ])
360    ///                         ),
361    ///                         Value::object(
362    ///                             IndexMap::from_iter([
363    ///                                 ("val2".to_string(), Value::string("test3".to_string()))
364    ///                             ])
365    ///                         )
366    ///                     ])
367    ///                 )
368    ///             ]))
369    ///         ]
370    ///     )
371    /// ]);
372    /// ```
373    ///
374    fn evaluate_hcl_map_blocks(
375        &self,
376        blocks: Vec<&Block>,
377        dependencies_execution_results: &DependencyExecutionResultCache,
378        package_id: &PackageId,
379        runbook_workspace_context: &RunbookWorkspaceContext,
380        runbook_execution_context: &RunbookExecutionContext,
381    ) -> Result<IndexMap<String, Vec<Value>>, Diagnostic> {
382        let mut entries: IndexMap<String, Vec<Value>> = IndexMap::new();
383
384        for block in blocks.iter() {
385            // We'll store up all map entries as an Object
386            let mut object_values = IndexMap::new();
387
388            // Check if this map has nested maps within, and evaluate them
389            let sub_entries = self.evaluate_hcl_map_blocks(
390                block.body.blocks().collect(),
391                dependencies_execution_results,
392                package_id,
393                runbook_workspace_context,
394                runbook_execution_context,
395            )?;
396
397            for (sub_key, sub_values) in sub_entries {
398                object_values.insert(sub_key, Value::array(sub_values));
399            }
400
401            for attribute in block.body.attributes() {
402                let value = match eval_expression(
403                    &attribute.value,
404                    dependencies_execution_results,
405                    package_id,
406                    runbook_workspace_context,
407                    runbook_execution_context,
408                    &self,
409                ) {
410                    Ok(ExpressionEvaluationStatus::CompleteOk(result)) => result,
411                    Err(diag) => return Err(diag),
412                    w => unimplemented!("{:?}", w),
413                };
414                match value.clone() {
415                    Value::Object(obj) => {
416                        for (k, v) in obj.into_iter() {
417                            object_values.insert(k, v);
418                        }
419                    }
420                    v => {
421                        object_values.insert(attribute.key.to_string(), v);
422                    }
423                };
424            }
425            let block_ident = block.ident.to_string();
426            match entries.get_mut(&block_ident) {
427                Some(vals) => {
428                    vals.push(Value::object(object_values));
429                }
430                None => {
431                    entries.insert(block_ident, vec![Value::object(object_values)]);
432                }
433            }
434        }
435
436        Ok(entries)
437    }
438
439    pub fn execute_function(
440        &self,
441        package_did: PackageDid,
442        namespace_opt: Option<String>,
443        name: &str,
444        args: &Vec<Value>,
445        authorization_context: &AuthorizationContext,
446    ) -> Result<Value, Diagnostic> {
447        let function = match namespace_opt {
448            Some(namespace) => match self
449                .addons_context
450                .addon_construct_factories
451                .get(&(package_did, namespace.clone()))
452            {
453                Some(addon) => match addon.functions.get(name) {
454                    Some(function) => function,
455                    None => {
456                        return Err(diagnosed_error!(
457                            "could not find function {name} in namespace {}",
458                            namespace
459                        ))
460                    }
461                },
462                None => return Err(diagnosed_error!("could not find namespace {}", namespace)),
463            },
464            None => match self.functions.get(name) {
465                Some(function) => function,
466                None => {
467                    return Err(diagnosed_error!("could not find function {name}"));
468                }
469            },
470        };
471        (function.runner)(function, authorization_context, args)
472    }
473}
474
475#[derive(Debug)]
476pub struct AddonsContext {
477    pub registered_addons: HashMap<String, (Box<dyn Addon>, bool)>,
478    pub addon_construct_factories: HashMap<(PackageDid, String), AddonConstructFactory>,
479    /// Function to get an available addon by namespace
480    pub get_addon_by_namespace: fn(&str) -> Option<Box<dyn Addon>>,
481}
482
483impl AddonsContext {
484    pub fn new(get_addon_by_namespace: fn(&str) -> Option<Box<dyn Addon>>) -> Self {
485        Self {
486            registered_addons: HashMap::new(),
487            addon_construct_factories: HashMap::new(),
488            get_addon_by_namespace,
489        }
490    }
491
492    pub fn is_addon_registered(&self, addon_id: &str) -> bool {
493        self.registered_addons.get(addon_id).is_some()
494    }
495
496    /// Registers an addon with this new package if the addon has already been registered
497    /// by a different package.
498    pub fn register_if_already_registered(
499        &mut self,
500        package_did: &PackageDid,
501        addon_id: &str,
502        scope: bool,
503    ) -> Result<(), Diagnostic> {
504        if self.is_addon_registered(&addon_id) {
505            self.register(package_did, addon_id, scope)?;
506            Ok(())
507        } else {
508            Err(diagnosed_error!("addon '{}' not registered", addon_id))
509        }
510    }
511
512    pub fn register(
513        &mut self,
514        package_did: &PackageDid,
515        addon_id: &str,
516        scope: bool,
517    ) -> Result<(), Diagnostic> {
518        let key = (package_did.clone(), addon_id.to_string());
519        let Some(addon) = (self.get_addon_by_namespace)(addon_id) else {
520            return Err(diagnosed_error!("unable to find addon {}", addon_id));
521        };
522        if self.addon_construct_factories.contains_key(&key) {
523            return Ok(());
524        }
525
526        // Build and register factory
527        let factory = AddonConstructFactory {
528            functions: addon.build_function_lookup(),
529            commands: addon.build_command_lookup(),
530            signers: addon.build_signer_lookup(),
531        };
532        self.registered_addons.insert(addon_id.to_string(), (addon, scope));
533        self.addon_construct_factories.insert(key, factory);
534        Ok(())
535    }
536
537    fn get_factory(
538        &self,
539        namespace: &str,
540        package_did: &PackageDid,
541    ) -> Result<&AddonConstructFactory, Diagnostic> {
542        let key = (package_did.clone(), namespace.to_string());
543        let Some(factory) = self.addon_construct_factories.get(&key) else {
544            return Err(diagnosed_error!(
545                "unable to instantiate construct, addon '{}' unknown",
546                namespace
547            ));
548        };
549        Ok(factory)
550    }
551
552    pub fn create_action_instance(
553        &self,
554        namespace: &str,
555        command_id: &str,
556        command_name: &str,
557        package_id: &PackageId,
558        block: &Block,
559        location: &FileLocation,
560    ) -> Result<CommandInstance, Diagnostic> {
561        let factory = self
562            .get_factory(namespace, &package_id.did())
563            .map_err(|diag| diag.location(location))?;
564        let command_id = CommandId::Action(command_id.to_string());
565        factory.create_command_instance(&command_id, namespace, command_name, block, package_id)
566    }
567
568    pub fn create_signer_instance(
569        &self,
570        namespaced_action: &str,
571        signer_name: &str,
572        package_id: &PackageId,
573        block: &Block,
574        location: &FileLocation,
575    ) -> Result<SignerInstance, Diagnostic> {
576        let Some((namespace, signer_id)) = namespaced_action.split_once("::") else {
577            todo!("return diagnostic")
578        };
579        let ctx = self
580            .get_factory(namespace, &package_id.did())
581            .map_err(|diag| diag.location(location))?;
582        ctx.create_signer_instance(signer_id, namespace, signer_name, block, package_id)
583    }
584}
585
586#[derive(Debug, Clone)]
587pub struct AddonConstructFactory {
588    /// Functions supported by addon
589    pub functions: HashMap<String, FunctionSpecification>,
590    /// Commands supported by addon
591    pub commands: HashMap<CommandId, PreCommandSpecification>,
592    /// Signing commands supported by addon
593    pub signers: HashMap<String, SignerSpecification>,
594}
595
596impl AddonConstructFactory {
597    pub fn create_command_instance(
598        self: &Self,
599        command_id: &CommandId,
600        namespace: &str,
601        command_name: &str,
602        block: &Block,
603        package_id: &PackageId,
604    ) -> Result<CommandInstance, Diagnostic> {
605        let Some(pre_command_spec) = self.commands.get(command_id) else {
606            return Err(diagnosed_error!(
607                "action '{}': unknown command '{}::{}'",
608                command_name,
609                namespace,
610                command_id.action_name(),
611            ));
612        };
613        let typing = match command_id {
614            CommandId::Action(command_id) => CommandInstanceType::Action(command_id.clone()),
615        };
616        match pre_command_spec {
617            PreCommandSpecification::Atomic(command_spec) => {
618                let command_instance = CommandInstance {
619                    specification: command_spec.clone(),
620                    name: command_name.to_string(),
621                    block: block.clone(),
622                    package_id: package_id.clone(),
623                    typing,
624                    namespace: namespace.to_string(),
625                };
626                Ok(command_instance)
627            }
628            PreCommandSpecification::Composite(_) => unimplemented!(),
629        }
630    }
631
632    pub fn create_signer_instance(
633        self: &Self,
634        signer_id: &str,
635        namespace: &str,
636        signer_name: &str,
637        block: &Block,
638        package_id: &PackageId,
639    ) -> Result<SignerInstance, Diagnostic> {
640        let Some(signer_spec) = self.signers.get(signer_id) else {
641            return Err(Diagnostic::error_from_string(format!(
642                "unknown signer specification: {} ({})",
643                signer_id, signer_name
644            )));
645        };
646        Ok(SignerInstance {
647            name: signer_name.to_string(),
648            specification: signer_spec.clone(),
649            block: block.clone(),
650            package_id: package_id.clone(),
651            namespace: namespace.to_string(),
652        })
653    }
654}