use std::collections::{HashMap, VecDeque};
use crate::manifest::WorkspaceManifest;
use crate::runbook::RunbookSources;
use crate::runbook::location::SourceLocation;
use crate::validation::hcl_validator::validate_with_hcl_and_addons;
use crate::validation::types::ValidationResult;
use crate::kit::types::commands::CommandSpecification;
#[derive(Debug, Clone)]
pub struct RunbookVariable {
pub name: String,
pub full_path: String,
pub resolved_value: Option<String>,
pub source: VariableSource,
pub references: Vec<VariableReference>,
}
#[derive(Debug, Clone)]
pub enum VariableSource {
Environment { name: String },
CommandLineInput,
Undefined,
}
#[derive(Debug, Clone)]
pub struct VariableReference {
pub location: SourceLocation,
pub context: ReferenceContext,
}
#[derive(Debug, Clone)]
pub enum ReferenceContext {
Signer { signer_name: String },
Action { action_name: String },
Addon { addon_name: String },
Output { output_name: String },
Other,
}
pub struct RunbookVariableIterator {
variables: VecDeque<RunbookVariable>,
}
impl RunbookVariableIterator {
pub fn new(
runbook_sources: &RunbookSources,
manifest: &WorkspaceManifest,
environment: Option<&str>,
addon_specs: HashMap<String, Vec<(String, CommandSpecification)>>,
) -> Result<Self, String> {
Self::new_with_cli_inputs(runbook_sources, manifest, environment, addon_specs, &[])
}
pub fn new_with_cli_inputs(
runbook_sources: &RunbookSources,
manifest: &WorkspaceManifest,
environment: Option<&str>,
addon_specs: HashMap<String, Vec<(String, CommandSpecification)>>,
cli_inputs: &[(String, String)],
) -> Result<Self, String> {
let mut variables = HashMap::new();
let mut combined_content = String::new();
let mut file_boundaries = Vec::new();
let mut current_line = 1;
for (file_location, (_name, raw_content)) in runbook_sources.tree.iter() {
let path = file_location.to_string();
let content = raw_content.to_string();
let start_line = current_line;
combined_content.push_str(&content);
if !combined_content.ends_with('\n') {
combined_content.push('\n');
}
let lines = content.lines().count();
let end_line = current_line + lines;
file_boundaries.push((path, start_line, end_line));
current_line = end_line;
}
let mut validation_result = ValidationResult::default();
let input_refs = validate_with_hcl_and_addons(
&combined_content,
&mut validation_result,
"runbook",
addon_specs,
)?;
for input_ref in input_refs {
let var_name = Self::extract_variable_name(&input_ref.name);
let file = Self::find_file_for_line(&file_boundaries, input_ref.line)
.unwrap_or_else(|| "unknown".to_string());
let entry = variables.entry(var_name.clone()).or_insert_with(|| {
let (resolved_value, source) = Self::resolve_variable(
&var_name,
manifest,
environment,
cli_inputs,
);
RunbookVariable {
name: var_name.clone(),
full_path: input_ref.name.clone(),
resolved_value,
source,
references: Vec::new(),
}
});
entry.references.push(VariableReference {
location: SourceLocation::new(file.clone(), input_ref.line, input_ref.column),
context: Self::determine_context(&input_ref.name),
});
}
Self::process_signer_references(&mut variables, &validation_result, &file_boundaries, manifest, environment, cli_inputs);
Ok(Self {
variables: variables.into_values().collect(),
})
}
fn extract_variable_name(full_path: &str) -> String {
if let Some((_prefix, name)) = full_path.split_once('.') {
name.to_string()
} else {
full_path.to_string()
}
}
fn find_file_for_line(file_boundaries: &[(String, usize, usize)], line: usize) -> Option<String> {
for (file, start, end) in file_boundaries {
if line >= *start && line < *end {
return Some(file.clone());
}
}
None
}
fn resolve_variable(
name: &str,
manifest: &WorkspaceManifest,
environment: Option<&str>,
cli_inputs: &[(String, String)],
) -> (Option<String>, VariableSource) {
for (key, value) in cli_inputs {
if key == name {
return (Some(value.clone()), VariableSource::CommandLineInput);
}
}
if let Some(env_name) = environment {
if let Some(env_vars) = manifest.environments.get(env_name) {
if let Some(value) = env_vars.get(name) {
return (Some(value.clone()), VariableSource::Environment {
name: env_name.to_string()
});
}
}
}
if let Some(global_vars) = manifest.environments.get("global") {
if let Some(value) = global_vars.get(name) {
return (Some(value.clone()), VariableSource::Environment {
name: "global".to_string()
});
}
}
(None, VariableSource::Undefined)
}
fn determine_context(_full_path: &str) -> ReferenceContext {
ReferenceContext::Other
}
fn process_signer_references(
variables: &mut HashMap<String, RunbookVariable>,
validation_result: &ValidationResult,
_file_boundaries: &[(String, usize, usize)],
manifest: &WorkspaceManifest,
environment: Option<&str>,
cli_inputs: &[(String, String)],
) {
for error in &validation_result.errors {
if error.message.starts_with("Reference to undefined signer") {
if let Some(signer_name) = error.message.split('\'').nth(1) {
let input_name = if signer_name == "operator" {
"operator_eoa".to_string()
} else {
format!("{}_address", signer_name)
};
if !variables.contains_key(&input_name) {
let (resolved_value, source) = Self::resolve_variable(
&input_name,
manifest,
environment,
cli_inputs,
);
let file = error.file.clone().unwrap_or_default();
variables.insert(input_name.clone(), RunbookVariable {
name: input_name.clone(),
full_path: format!("input.{}", input_name),
resolved_value,
source,
references: vec![VariableReference {
location: SourceLocation::new(
file,
error.line.unwrap_or(0),
error.column.unwrap_or(0)
),
context: ReferenceContext::Signer {
signer_name: signer_name.to_string()
},
}],
});
}
}
}
}
}
pub fn undefined_only(self) -> impl Iterator<Item = RunbookVariable> {
self.variables.into_iter().filter(|v| v.resolved_value.is_none())
}
pub fn undefined_or_cli_provided(self) -> impl Iterator<Item = RunbookVariable> {
self.variables.into_iter().filter(|v| {
v.resolved_value.is_none() || matches!(v.source, VariableSource::CommandLineInput)
})
}
pub fn defined_only(self) -> impl Iterator<Item = RunbookVariable> {
self.variables.into_iter().filter(|v| v.resolved_value.is_some())
}
}
impl Iterator for RunbookVariableIterator {
type Item = RunbookVariable;
fn next(&mut self) -> Option<Self::Item> {
self.variables.pop_front()
}
}