use super::types::{LocatedInputRef, ValidationResult};
use crate::kit::types::commands::CommandSpecification;
use crate::manifest::WorkspaceManifest;
use std::collections::HashMap;
use std::path::Path;
#[derive(Clone)]
pub struct ValidationContext {
pub content: String,
pub file_path: String,
pub manifest: Option<WorkspaceManifest>,
pub environment: Option<String>,
pub cli_inputs: Vec<(String, String)>,
pub addon_specs: Option<HashMap<String, Vec<(String, CommandSpecification)>>>,
effective_inputs: Option<HashMap<String, String>>,
pub input_refs: Vec<LocatedInputRef>,
}
impl ValidationContext {
pub fn new(content: impl Into<String>, file_path: impl Into<String>) -> Self {
Self {
content: content.into(),
file_path: file_path.into(),
manifest: None,
environment: None,
cli_inputs: Vec::new(),
addon_specs: None,
effective_inputs: None,
input_refs: Vec::new(),
}
}
pub fn with_manifest(mut self, manifest: WorkspaceManifest) -> Self {
self.manifest = Some(manifest);
self.effective_inputs = None; self
}
pub fn with_environment(mut self, environment: impl Into<String>) -> Self {
self.environment = Some(environment.into());
self.effective_inputs = None; self
}
pub fn with_cli_inputs(mut self, cli_inputs: Vec<(String, String)>) -> Self {
self.cli_inputs = cli_inputs;
self.effective_inputs = None; self
}
pub fn with_addon_specs(
mut self,
specs: HashMap<String, Vec<(String, CommandSpecification)>>,
) -> Self {
self.addon_specs = Some(specs);
self
}
pub fn file_path_as_path(&self) -> &Path {
Path::new(&self.file_path)
}
pub fn environment_ref(&self) -> Option<&String> {
self.environment.as_ref()
}
pub fn effective_inputs(&mut self) -> &HashMap<String, String> {
if self.effective_inputs.is_none() {
self.effective_inputs = Some(self.compute_effective_inputs());
}
self.effective_inputs.as_ref().expect("effective_inputs was just initialized")
}
fn compute_effective_inputs(&self) -> HashMap<String, String> {
let mut inputs = HashMap::new();
if let Some(manifest) = &self.manifest {
if let Some(defaults) = manifest.environments.get("defaults") {
inputs.extend(defaults.iter().map(|(k, v)| (k.clone(), v.clone())));
}
if let Some(env_name) = &self.environment {
if let Some(env_vars) = manifest.environments.get(env_name) {
inputs.extend(env_vars.iter().map(|(k, v)| (k.clone(), v.clone())));
}
}
}
inputs.extend(self.cli_inputs.iter().cloned());
inputs
}
pub fn add_input_ref(&mut self, input_ref: LocatedInputRef) {
self.input_refs.push(input_ref);
}
pub fn load_addon_specs(&mut self) -> &HashMap<String, Vec<(String, CommandSpecification)>> {
if self.addon_specs.is_none() {
self.addon_specs = Some(HashMap::new());
}
self.addon_specs.as_ref().unwrap()
}
}
pub struct ValidationContextBuilder {
context: ValidationContext,
}
impl ValidationContextBuilder {
pub fn new(content: impl Into<String>, file_path: impl Into<String>) -> Self {
Self { context: ValidationContext::new(content, file_path) }
}
pub fn manifest(mut self, manifest: WorkspaceManifest) -> Self {
self.context.manifest = Some(manifest);
self
}
pub fn environment(mut self, environment: impl Into<String>) -> Self {
self.context.environment = Some(environment.into());
self
}
pub fn cli_inputs(mut self, cli_inputs: Vec<(String, String)>) -> Self {
self.context.cli_inputs = cli_inputs;
self
}
pub fn addon_specs(
mut self,
specs: HashMap<String, Vec<(String, CommandSpecification)>>,
) -> Self {
self.context.addon_specs = Some(specs);
self
}
pub fn build(self) -> ValidationContext {
self.context
}
}
pub trait ValidationContextExt {
fn validate_hcl(&mut self, result: &mut ValidationResult) -> Result<(), String>;
fn validate_manifest(
&mut self,
config: super::ManifestValidationConfig,
result: &mut ValidationResult,
);
fn validate_full(&mut self, result: &mut ValidationResult) -> Result<(), String>;
}
impl ValidationContextExt for ValidationContext {
fn validate_hcl(&mut self, result: &mut ValidationResult) -> Result<(), String> {
if let Some(specs) = self.addon_specs.clone() {
let input_refs = super::hcl_validator::validate_with_hcl_and_addons(
&self.content,
result,
&self.file_path,
specs,
)?;
self.input_refs = input_refs;
} else {
let input_refs =
super::hcl_validator::validate_with_hcl(&self.content, result, &self.file_path)?;
self.input_refs = input_refs;
}
Ok(())
}
fn validate_manifest(
&mut self,
config: super::ManifestValidationConfig,
result: &mut ValidationResult,
) {
if let Some(manifest) = &self.manifest {
super::manifest_validator::validate_inputs_against_manifest(
&self.input_refs,
&self.content,
manifest,
self.environment.as_ref(),
result,
&self.file_path,
&self.cli_inputs,
config,
);
}
}
fn validate_full(&mut self, result: &mut ValidationResult) -> Result<(), String> {
self.validate_hcl(result)?;
if self.manifest.is_some() {
let config = if self.environment.as_deref() == Some("production")
|| self.environment.as_deref() == Some("prod")
{
let mut cfg = super::ManifestValidationConfig::strict();
cfg.custom_rules.extend(super::linter_rules::get_strict_linter_rules());
cfg
} else {
let mut cfg = super::ManifestValidationConfig::default();
cfg.custom_rules.extend(super::linter_rules::get_linter_rules());
cfg
};
self.validate_manifest(config, result);
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use txtx_addon_kit::indexmap::IndexMap;
fn create_test_manifest() -> WorkspaceManifest {
let mut environments = IndexMap::new();
let mut defaults = IndexMap::new();
defaults.insert("api_url".to_string(), "https://api.example.com".to_string());
environments.insert("defaults".to_string(), defaults);
let mut production = IndexMap::new();
production.insert("api_url".to_string(), "https://api.prod.example.com".to_string());
production.insert("api_token".to_string(), "prod-token".to_string());
environments.insert("production".to_string(), production);
WorkspaceManifest {
name: "test".to_string(),
id: "test-id".to_string(),
runbooks: Vec::new(),
environments,
location: None,
}
}
#[test]
fn test_validation_context_builder() {
let manifest = create_test_manifest();
let context = ValidationContextBuilder::new("test content", "test.tx")
.manifest(manifest)
.environment("production")
.cli_inputs(vec![("debug".to_string(), "true".to_string())])
.build();
assert_eq!(context.content, "test content");
assert_eq!(context.file_path, "test.tx");
assert_eq!(context.environment, Some("production".to_string()));
assert_eq!(context.cli_inputs.len(), 1);
}
#[test]
fn test_effective_inputs() {
let manifest = create_test_manifest();
let mut context = ValidationContext::new("test", "test.tx")
.with_manifest(manifest)
.with_environment("production")
.with_cli_inputs(vec![("api_url".to_string(), "https://override.com".to_string())]);
let inputs = context.effective_inputs();
assert_eq!(inputs.get("api_url"), Some(&"https://override.com".to_string()));
assert_eq!(inputs.get("api_token"), Some(&"prod-token".to_string()));
}
}