1use super::types::{LocatedInputRef, ValidationResult};
9use crate::kit::types::commands::CommandSpecification;
10use crate::manifest::WorkspaceManifest;
11use std::collections::HashMap;
12use std::path::Path;
13
14#[derive(Clone)]
20pub struct ValidationContext {
21 pub content: String,
23
24 pub file_path: String,
26
27 pub manifest: Option<WorkspaceManifest>,
29
30 pub environment: Option<String>,
32
33 pub cli_inputs: Vec<(String, String)>,
35
36 pub addon_specs: Option<HashMap<String, Vec<(String, CommandSpecification)>>>,
38
39 effective_inputs: Option<HashMap<String, String>>,
41
42 pub input_refs: Vec<LocatedInputRef>,
44}
45
46impl ValidationContext {
47 pub fn new(content: impl Into<String>, file_path: impl Into<String>) -> Self {
49 Self {
50 content: content.into(),
51 file_path: file_path.into(),
52 manifest: None,
53 environment: None,
54 cli_inputs: Vec::new(),
55 addon_specs: None,
56 effective_inputs: None,
57 input_refs: Vec::new(),
58 }
59 }
60
61 pub fn with_manifest(mut self, manifest: WorkspaceManifest) -> Self {
63 self.manifest = Some(manifest);
64 self.effective_inputs = None; self
66 }
67
68 pub fn with_environment(mut self, environment: impl Into<String>) -> Self {
70 self.environment = Some(environment.into());
71 self.effective_inputs = None; self
73 }
74
75 pub fn with_cli_inputs(mut self, cli_inputs: Vec<(String, String)>) -> Self {
77 self.cli_inputs = cli_inputs;
78 self.effective_inputs = None; self
80 }
81
82 pub fn with_addon_specs(
84 mut self,
85 specs: HashMap<String, Vec<(String, CommandSpecification)>>,
86 ) -> Self {
87 self.addon_specs = Some(specs);
88 self
89 }
90
91 pub fn file_path_as_path(&self) -> &Path {
93 Path::new(&self.file_path)
94 }
95
96 pub fn environment_ref(&self) -> Option<&String> {
98 self.environment.as_ref()
99 }
100
101 pub fn effective_inputs(&mut self) -> &HashMap<String, String> {
103 if self.effective_inputs.is_none() {
104 self.effective_inputs = Some(self.compute_effective_inputs());
105 }
106 self.effective_inputs.as_ref().expect("effective_inputs was just initialized")
107 }
108
109 fn compute_effective_inputs(&self) -> HashMap<String, String> {
111 let mut inputs = HashMap::new();
112
113 if let Some(manifest) = &self.manifest {
114 if let Some(defaults) = manifest.environments.get("defaults") {
116 inputs.extend(defaults.iter().map(|(k, v)| (k.clone(), v.clone())));
117 }
118
119 if let Some(env_name) = &self.environment {
121 if let Some(env_vars) = manifest.environments.get(env_name) {
122 inputs.extend(env_vars.iter().map(|(k, v)| (k.clone(), v.clone())));
123 }
124 }
125 }
126
127 inputs.extend(self.cli_inputs.iter().cloned());
129
130 inputs
131 }
132
133 pub fn add_input_ref(&mut self, input_ref: LocatedInputRef) {
135 self.input_refs.push(input_ref);
136 }
137
138 pub fn load_addon_specs(&mut self) -> &HashMap<String, Vec<(String, CommandSpecification)>> {
140 if self.addon_specs.is_none() {
141 self.addon_specs = Some(HashMap::new());
162 }
163 self.addon_specs.as_ref().unwrap()
164 }
165}
166
167pub struct ValidationContextBuilder {
169 context: ValidationContext,
170}
171
172impl ValidationContextBuilder {
173 pub fn new(content: impl Into<String>, file_path: impl Into<String>) -> Self {
175 Self { context: ValidationContext::new(content, file_path) }
176 }
177
178 pub fn manifest(mut self, manifest: WorkspaceManifest) -> Self {
180 self.context.manifest = Some(manifest);
181 self
182 }
183
184 pub fn environment(mut self, environment: impl Into<String>) -> Self {
186 self.context.environment = Some(environment.into());
187 self
188 }
189
190 pub fn cli_inputs(mut self, cli_inputs: Vec<(String, String)>) -> Self {
192 self.context.cli_inputs = cli_inputs;
193 self
194 }
195
196 pub fn addon_specs(
198 mut self,
199 specs: HashMap<String, Vec<(String, CommandSpecification)>>,
200 ) -> Self {
201 self.context.addon_specs = Some(specs);
202 self
203 }
204
205 pub fn build(self) -> ValidationContext {
207 self.context
208 }
209}
210
211pub trait ValidationContextExt {
213 fn validate_hcl(&mut self, result: &mut ValidationResult) -> Result<(), String>;
215
216 fn validate_manifest(
218 &mut self,
219 config: super::ManifestValidationConfig,
220 result: &mut ValidationResult,
221 );
222
223 fn validate_full(&mut self, result: &mut ValidationResult) -> Result<(), String>;
225}
226
227impl ValidationContextExt for ValidationContext {
228 fn validate_hcl(&mut self, result: &mut ValidationResult) -> Result<(), String> {
229 if let Some(specs) = self.addon_specs.clone() {
231 let input_refs = super::hcl_validator::validate_with_hcl_and_addons(
232 &self.content,
233 result,
234 &self.file_path,
235 specs,
236 )?;
237 self.input_refs = input_refs;
238 } else {
239 let input_refs =
240 super::hcl_validator::validate_with_hcl(&self.content, result, &self.file_path)?;
241 self.input_refs = input_refs;
242 }
243 Ok(())
244 }
245
246 fn validate_manifest(
247 &mut self,
248 config: super::ManifestValidationConfig,
249 result: &mut ValidationResult,
250 ) {
251 if let Some(manifest) = &self.manifest {
252 super::manifest_validator::validate_inputs_against_manifest(
253 &self.input_refs,
254 &self.content,
255 manifest,
256 self.environment.as_ref(),
257 result,
258 &self.file_path,
259 &self.cli_inputs,
260 config,
261 );
262 }
263 }
264
265 fn validate_full(&mut self, result: &mut ValidationResult) -> Result<(), String> {
266 self.validate_hcl(result)?;
268
269 if self.manifest.is_some() {
271 let config = if self.environment.as_deref() == Some("production")
272 || self.environment.as_deref() == Some("prod")
273 {
274 let mut cfg = super::ManifestValidationConfig::strict();
276 cfg.custom_rules.extend(super::linter_rules::get_strict_linter_rules());
277 cfg
278 } else {
279 let mut cfg = super::ManifestValidationConfig::default();
281 cfg.custom_rules.extend(super::linter_rules::get_linter_rules());
282 cfg
283 };
284
285 self.validate_manifest(config, result);
286 }
287
288 Ok(())
289 }
290}
291
292#[cfg(test)]
293mod tests {
294 use super::*;
295 use txtx_addon_kit::indexmap::IndexMap;
296
297 fn create_test_manifest() -> WorkspaceManifest {
298 let mut environments = IndexMap::new();
299
300 let mut defaults = IndexMap::new();
301 defaults.insert("api_url".to_string(), "https://api.example.com".to_string());
302 environments.insert("defaults".to_string(), defaults);
303
304 let mut production = IndexMap::new();
305 production.insert("api_url".to_string(), "https://api.prod.example.com".to_string());
306 production.insert("api_token".to_string(), "prod-token".to_string());
307 environments.insert("production".to_string(), production);
308
309 WorkspaceManifest {
310 name: "test".to_string(),
311 id: "test-id".to_string(),
312 runbooks: Vec::new(),
313 environments,
314 location: None,
315 }
316 }
317
318 #[test]
319 fn test_validation_context_builder() {
320 let manifest = create_test_manifest();
321 let context = ValidationContextBuilder::new("test content", "test.tx")
322 .manifest(manifest)
323 .environment("production")
324 .cli_inputs(vec![("debug".to_string(), "true".to_string())])
325 .build();
326
327 assert_eq!(context.content, "test content");
328 assert_eq!(context.file_path, "test.tx");
329 assert_eq!(context.environment, Some("production".to_string()));
330 assert_eq!(context.cli_inputs.len(), 1);
331 }
332
333 #[test]
334 fn test_effective_inputs() {
335 let manifest = create_test_manifest();
336 let mut context = ValidationContext::new("test", "test.tx")
337 .with_manifest(manifest)
338 .with_environment("production")
339 .with_cli_inputs(vec![("api_url".to_string(), "https://override.com".to_string())]);
340
341 let inputs = context.effective_inputs();
342
343 assert_eq!(inputs.get("api_url"), Some(&"https://override.com".to_string()));
345 assert_eq!(inputs.get("api_token"), Some(&"prod-token".to_string()));
347 }
348}