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 pub functions: HashMap<String, FunctionSpecification>,
40 pub addons_context: AddonsContext,
42 pub concurrency: u64,
44 pub authorization_context: AuthorizationContext,
46 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 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 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 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 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 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 let mut object_values = IndexMap::new();
387
388 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 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 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 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 pub functions: HashMap<String, FunctionSpecification>,
590 pub commands: HashMap<CommandId, PreCommandSpecification>,
592 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}