clnrm_template/
validation.rs

1//! Template output validation and schema checking
2//!
3//! Provides comprehensive validation for template rendering results:
4//! - Schema validation (TOML, JSON, YAML)
5//! - Required field checking
6//! - Format validation
7//! - Custom validation rules
8
9use crate::error::{Result, TemplateError};
10use serde_json::Value;
11use std::collections::HashSet;
12
13/// Template output validator
14///
15/// Validates rendered template content for:
16/// - Correct format (TOML, JSON, YAML)
17/// - Required fields presence
18/// - Schema compliance
19/// - Custom validation rules
20#[derive(Clone)]
21pub struct TemplateValidator {
22    /// Required fields that must be present
23    required_fields: HashSet<String>,
24    /// Required top-level sections for TOML
25    required_sections: HashSet<String>,
26    /// Custom validation rules
27    pub(crate) rules: Vec<ValidationRule>,
28    /// Expected output format
29    format: OutputFormat,
30    /// Schema for validation (TOML/JSON schema)
31    schema: Option<Value>,
32    /// TOML-specific validation options
33    toml_options: TomlValidationOptions,
34}
35
36/// TOML-specific validation options
37#[derive(Debug, Clone, Default)]
38pub struct TomlValidationOptions {
39    /// Allow inline tables
40    pub allow_inline_tables: bool,
41    /// Allow multiline strings
42    pub allow_multiline_strings: bool,
43    /// Maximum nesting depth
44    pub max_nesting_depth: Option<usize>,
45    /// Maximum array length
46    pub max_array_length: Option<usize>,
47    /// Maximum string length
48    pub max_string_length: Option<usize>,
49}
50
51/// Supported output formats for validation
52#[derive(Debug, Clone, PartialEq, Default)]
53pub enum OutputFormat {
54    /// TOML format (default for Cleanroom)
55    #[default]
56    Toml,
57    /// JSON format
58    Json,
59    /// YAML format
60    Yaml,
61    /// Auto-detect based on content
62    Auto,
63}
64
65impl Default for TemplateValidator {
66    fn default() -> Self {
67        Self {
68            required_fields: HashSet::new(),
69            required_sections: HashSet::new(),
70            rules: Vec::new(),
71            format: OutputFormat::Toml,
72            schema: None,
73            toml_options: TomlValidationOptions::default(),
74        }
75    }
76}
77
78impl TemplateValidator {
79    /// Create new template validator
80    pub fn new() -> Self {
81        Self::default()
82    }
83
84    /// Set required fields that must be present in output
85    ///
86    /// # Arguments
87    /// * `fields` - Field paths (e.g., "service.name", "meta.version")
88    pub fn require_fields<I, S>(mut self, fields: I) -> Self
89    where
90        I: IntoIterator<Item = S>,
91        S: Into<String>,
92    {
93        for field in fields {
94            self.required_fields.insert(field.into());
95        }
96        self
97    }
98
99    /// Set required top-level sections for TOML output
100    ///
101    /// # Arguments
102    /// * `sections` - Section names that must exist (e.g., "service", "meta")
103    pub fn require_sections<I, S>(mut self, sections: I) -> Self
104    where
105        I: IntoIterator<Item = S>,
106        S: Into<String>,
107    {
108        for section in sections {
109            self.required_sections.insert(section.into());
110        }
111        self
112    }
113
114    /// Add custom validation rule
115    pub fn with_rule(mut self, rule: ValidationRule) -> Self {
116        self.rules.push(rule);
117        self
118    }
119
120    /// Set expected output format
121    pub fn format(mut self, format: OutputFormat) -> Self {
122        self.format = format;
123        self
124    }
125
126    /// Set validation schema
127    ///
128    /// # Arguments
129    /// * `schema` - JSON schema for validation
130    pub fn with_schema(mut self, schema: Value) -> Self {
131        self.schema = Some(schema);
132        self
133    }
134
135    /// Set TOML validation options
136    pub fn with_toml_options(mut self, options: TomlValidationOptions) -> Self {
137        self.toml_options = options;
138        self
139    }
140
141    /// Validate template output
142    ///
143    /// # Arguments
144    /// * `output` - Rendered template content
145    /// * `template_name` - Name of template for error reporting
146    pub fn validate(&self, output: &str, template_name: &str) -> Result<()> {
147        // Detect format if auto
148        let format = if matches!(self.format, OutputFormat::Auto) {
149            self.detect_format(output)
150        } else {
151            self.format.clone()
152        };
153
154        // Validate format
155        self.validate_format(&format, output, template_name)?;
156
157        // Parse content for further validation
158        let parsed = self.parse_content(&format, output, template_name)?;
159
160        // Validate required fields
161        self.validate_required_fields(&parsed, template_name)?;
162
163        // Validate required sections (TOML only)
164        if matches!(format, OutputFormat::Toml) {
165            self.validate_required_sections(&parsed, template_name)?;
166        }
167
168        // Validate against schema
169        if let Some(schema) = &self.schema {
170            self.validate_schema(&parsed, schema, template_name)?;
171        }
172
173        // Apply custom rules
174        for rule in &self.rules {
175            rule.validate(&parsed, template_name)?;
176        }
177
178        // TOML-specific validation
179        if matches!(format, OutputFormat::Toml) {
180            self.validate_toml_structure(&parsed, template_name)?;
181        }
182
183        Ok(())
184    }
185
186    /// Detect output format from content
187    fn detect_format(&self, content: &str) -> OutputFormat {
188        let trimmed = content.trim();
189
190        if trimmed.starts_with('{') && trimmed.ends_with('}') {
191            OutputFormat::Json
192        } else if trimmed.starts_with('[') && trimmed.contains('=') {
193            OutputFormat::Toml
194        } else if trimmed.contains(": ") && !trimmed.contains('=') {
195            OutputFormat::Yaml
196        } else {
197            OutputFormat::Toml // Default fallback
198        }
199    }
200
201    /// Validate content format
202    fn validate_format(
203        &self,
204        format: &OutputFormat,
205        content: &str,
206        template_name: &str,
207    ) -> Result<()> {
208        match format {
209            OutputFormat::Toml => self.validate_toml(content, template_name),
210            OutputFormat::Json => self.validate_json(content, template_name),
211            OutputFormat::Yaml => self.validate_yaml(content, template_name),
212            OutputFormat::Auto => Ok(()), // Already handled
213        }
214    }
215
216    /// Validate TOML format
217    fn validate_toml(&self, content: &str, template_name: &str) -> Result<()> {
218        toml::from_str::<Value>(content).map_err(|e| {
219            TemplateError::ValidationError(format!(
220                "Invalid TOML format in template '{}': {}",
221                template_name, e
222            ))
223        })?;
224        Ok(())
225    }
226
227    /// Validate JSON format
228    fn validate_json(&self, content: &str, template_name: &str) -> Result<()> {
229        serde_json::from_str::<Value>(content).map_err(|e| {
230            TemplateError::ValidationError(format!(
231                "Invalid JSON format in template '{}': {}",
232                template_name, e
233            ))
234        })?;
235        Ok(())
236    }
237
238    /// Validate YAML format
239    fn validate_yaml(&self, content: &str, template_name: &str) -> Result<()> {
240        serde_yaml::from_str::<Value>(content).map_err(|e| {
241            TemplateError::ValidationError(format!(
242                "Invalid YAML format in template '{}': {}",
243                template_name, e
244            ))
245        })?;
246        Ok(())
247    }
248
249    /// Parse content into JSON Value for validation
250    fn parse_content(
251        &self,
252        format: &OutputFormat,
253        content: &str,
254        template_name: &str,
255    ) -> Result<Value> {
256        match format {
257            OutputFormat::Toml => toml::from_str::<Value>(content).map_err(|e| {
258                TemplateError::ValidationError(format!(
259                    "Failed to parse TOML in template '{}': {}",
260                    template_name, e
261                ))
262            }),
263            OutputFormat::Json => serde_json::from_str::<Value>(content).map_err(|e| {
264                TemplateError::ValidationError(format!(
265                    "Failed to parse JSON in template '{}': {}",
266                    template_name, e
267                ))
268            }),
269            OutputFormat::Yaml => serde_yaml::from_str::<Value>(content).map_err(|e| {
270                TemplateError::ValidationError(format!(
271                    "Failed to parse YAML in template '{}': {}",
272                    template_name, e
273                ))
274            }),
275            OutputFormat::Auto => {
276                // Try TOML first (most common for Cleanroom)
277                if let Ok(value) = toml::from_str::<Value>(content) {
278                    Ok(value)
279                } else if let Ok(value) = serde_json::from_str::<Value>(content) {
280                    Ok(value)
281                } else if let Ok(value) = serde_yaml::from_str::<Value>(content) {
282                    Ok(value)
283                } else {
284                    Err(TemplateError::ValidationError(format!(
285                        "Could not parse template '{}' as TOML, JSON, or YAML",
286                        template_name
287                    )))
288                }
289            }
290        }
291    }
292
293    /// Validate required fields are present
294    fn validate_required_fields(&self, parsed: &Value, template_name: &str) -> Result<()> {
295        for field_path in &self.required_fields {
296            if !self.field_exists(parsed, field_path) {
297                return Err(TemplateError::ValidationError(format!(
298                    "Required field '{}' missing in template '{}'",
299                    field_path, template_name
300                )));
301            }
302        }
303        Ok(())
304    }
305
306    /// Validate required sections exist (TOML only)
307    fn validate_required_sections(&self, parsed: &Value, template_name: &str) -> Result<()> {
308        let obj = parsed.as_object().ok_or_else(|| {
309            TemplateError::ValidationError(format!(
310                "Template '{}' must be a TOML object for section validation",
311                template_name
312            ))
313        })?;
314
315        for section in &self.required_sections {
316            if !obj.contains_key(section) {
317                return Err(TemplateError::ValidationError(format!(
318                    "Required section '{}' missing in template '{}'",
319                    section, template_name
320                )));
321            }
322        }
323        Ok(())
324    }
325
326    /// Validate against JSON schema
327    fn validate_schema(&self, parsed: &Value, schema: &Value, template_name: &str) -> Result<()> {
328        // Simple schema validation - can be extended with proper JSON Schema
329        if let (Some(obj), Some(schema_obj)) = (parsed.as_object(), schema.as_object()) {
330            // Check required properties
331            if let Some(required) = schema_obj.get("required").and_then(|v| v.as_array()) {
332                for prop in required {
333                    if let Some(prop_str) = prop.as_str() {
334                        if !obj.contains_key(prop_str) {
335                            return Err(TemplateError::ValidationError(format!(
336                                "Schema validation failed: required property '{}' missing in template '{}'",
337                                prop_str, template_name
338                            )));
339                        }
340                    }
341                }
342            }
343
344            // Check property types
345            if let Some(properties) = schema_obj.get("properties").and_then(|v| v.as_object()) {
346                for (prop_name, prop_schema) in properties {
347                    if let Some(prop_value) = obj.get(prop_name) {
348                        self.validate_property_type(
349                            prop_value,
350                            prop_schema,
351                            prop_name,
352                            template_name,
353                        )?;
354                    }
355                }
356            }
357        }
358
359        Ok(())
360    }
361
362    /// Validate property type against schema
363    fn validate_property_type(
364        &self,
365        value: &Value,
366        schema: &Value,
367        prop_name: &str,
368        template_name: &str,
369    ) -> Result<()> {
370        if let Some(expected_type) = schema.get("type").and_then(|v| v.as_str()) {
371            match expected_type {
372                "string" => {
373                    if !value.is_string() {
374                        return Err(TemplateError::ValidationError(format!(
375                            "Schema validation failed: property '{}' must be string in template '{}'",
376                            prop_name, template_name
377                        )));
378                    }
379                }
380                "number" => {
381                    if !value.is_number() {
382                        return Err(TemplateError::ValidationError(format!(
383                            "Schema validation failed: property '{}' must be number in template '{}'",
384                            prop_name, template_name
385                        )));
386                    }
387                }
388                "boolean" => {
389                    if !value.is_boolean() {
390                        return Err(TemplateError::ValidationError(format!(
391                            "Schema validation failed: property '{}' must be boolean in template '{}'",
392                            prop_name, template_name
393                        )));
394                    }
395                }
396                "array" => {
397                    if !value.is_array() {
398                        return Err(TemplateError::ValidationError(format!(
399                            "Schema validation failed: property '{}' must be array in template '{}'",
400                            prop_name, template_name
401                        )));
402                    }
403                }
404                "object" => {
405                    if !value.is_object() {
406                        return Err(TemplateError::ValidationError(format!(
407                            "Schema validation failed: property '{}' must be object in template '{}'",
408                            prop_name, template_name
409                        )));
410                    }
411                }
412                _ => {} // Unknown type, skip validation
413            }
414        }
415
416        Ok(())
417    }
418
419    /// Check if field exists at dot-notation path
420    fn field_exists(&self, value: &Value, field_path: &str) -> bool {
421        let parts: Vec<&str> = field_path.split('.').collect();
422        let mut current = value;
423
424        for part in parts {
425            match current {
426                Value::Object(obj) => {
427                    if let Some(next) = obj.get(part) {
428                        current = next;
429                    } else {
430                        return false;
431                    }
432                }
433                _ => return false,
434            }
435        }
436
437        true
438    }
439
440    /// Validate TOML-specific structure
441    fn validate_toml_structure(&self, parsed: &Value, template_name: &str) -> Result<()> {
442        self.validate_toml_nesting(parsed, 0, template_name)?;
443        self.validate_toml_sizes(parsed, template_name)?;
444        Ok(())
445    }
446
447    /// Validate TOML nesting depth
448    fn validate_toml_nesting(
449        &self,
450        value: &Value,
451        depth: usize,
452        template_name: &str,
453    ) -> Result<()> {
454        if let Some(max_depth) = self.toml_options.max_nesting_depth {
455            if depth > max_depth {
456                return Err(TemplateError::ValidationError(format!(
457                    "TOML nesting depth exceeds maximum {} in template '{}'",
458                    max_depth, template_name
459                )));
460            }
461        }
462
463        match value {
464            Value::Object(obj) => {
465                for (_, value) in obj {
466                    self.validate_toml_nesting(value, depth + 1, template_name)?;
467                }
468            }
469            Value::Array(arr) => {
470                for value in arr {
471                    self.validate_toml_nesting(value, depth + 1, template_name)?;
472                }
473            }
474            _ => {}
475        }
476
477        Ok(())
478    }
479
480    /// Validate TOML value sizes
481    fn validate_toml_sizes(&self, value: &Value, template_name: &str) -> Result<()> {
482        match value {
483            Value::Array(arr) => {
484                if let Some(max_len) = self.toml_options.max_array_length {
485                    if arr.len() > max_len {
486                        return Err(TemplateError::ValidationError(format!(
487                            "Array length {} exceeds maximum {} in template '{}'",
488                            arr.len(),
489                            max_len,
490                            template_name
491                        )));
492                    }
493                }
494            }
495            Value::String(s) => {
496                if let Some(max_len) = self.toml_options.max_string_length {
497                    if s.len() > max_len {
498                        return Err(TemplateError::ValidationError(format!(
499                            "String length {} exceeds maximum {} in template '{}'",
500                            s.len(),
501                            max_len,
502                            template_name
503                        )));
504                    }
505                }
506            }
507            Value::Object(obj) => {
508                for (_, value) in obj {
509                    self.validate_toml_sizes(value, template_name)?;
510                }
511                for value in obj.values() {
512                    self.validate_toml_sizes(value, template_name)?;
513                }
514            }
515            _ => {}
516        }
517
518        Ok(())
519    }
520}
521
522/// Validation rule types for template validation
523pub enum ValidationRule {
524    /// Validate service name follows naming conventions
525    ServiceName,
526    /// Validate version follows semver format
527    Semver,
528    /// Validate environment is one of allowed values
529    Environment { allowed: Vec<String> },
530    /// Validate required OTEL configuration is present
531    OtelConfig,
532    /// Custom validation function
533    Custom { name: String },
534}
535
536impl ValidationRule {
537    /// Validate parsed template content
538    ///
539    /// # Arguments
540    /// * `parsed` - Parsed template content as JSON Value
541    /// * `template_name` - Template name for error reporting
542    pub fn validate(&self, parsed: &Value, template_name: &str) -> Result<()> {
543        match self {
544            ValidationRule::ServiceName => Self::validate_service_name(parsed, template_name),
545            ValidationRule::Semver => Self::validate_semver(parsed, template_name),
546            ValidationRule::Environment { allowed } => {
547                Self::validate_environment(parsed, template_name, allowed)
548            }
549            ValidationRule::OtelConfig => Self::validate_otel_config(parsed, template_name),
550            ValidationRule::Custom { .. } => {
551                // For now, custom validation is not implemented in this simplified version
552                // This would require a registry of custom validators
553                Ok(())
554            }
555        }
556    }
557
558    fn validate_service_name(parsed: &Value, template_name: &str) -> Result<()> {
559        if let Some(service_name) = parsed
560            .get("service")
561            .and_then(|v| v.get("name"))
562            .and_then(|v| v.as_str())
563        {
564            if !service_name
565                .chars()
566                .all(|c| c.is_alphanumeric() || c == '-' || c == '_')
567            {
568                return Err(TemplateError::ValidationError(format!(
569                    "Service name '{}' in template '{}' contains invalid characters (only alphanumeric, '-', '_' allowed)",
570                    service_name, template_name
571                )));
572            }
573
574            if service_name.len() > 63 {
575                return Err(TemplateError::ValidationError(format!(
576                    "Service name '{}' in template '{}' is too long (max 63 characters)",
577                    service_name, template_name
578                )));
579            }
580        }
581        Ok(())
582    }
583
584    fn validate_semver(parsed: &Value, template_name: &str) -> Result<()> {
585        if let Some(version) = parsed
586            .get("meta")
587            .and_then(|v| v.get("version"))
588            .and_then(|v| v.as_str())
589        {
590            // Simple semver regex check
591            let semver_regex =
592                regex::Regex::new(r"^\d+\.\d+\.\d+(-[a-zA-Z0-9.-]+)?(\+[a-zA-Z0-9.-]+)?$")
593                    .map_err(|_| {
594                        TemplateError::ValidationError("Failed to compile semver regex".to_string())
595                    })?;
596
597            if !semver_regex.is_match(version) {
598                return Err(TemplateError::ValidationError(format!(
599                    "Version '{}' in template '{}' does not follow semver format (x.y.z)",
600                    version, template_name
601                )));
602            }
603        }
604        Ok(())
605    }
606
607    fn validate_environment(parsed: &Value, template_name: &str, allowed: &[String]) -> Result<()> {
608        if let Some(env) = parsed
609            .get("meta")
610            .and_then(|v| v.get("environment"))
611            .and_then(|v| v.as_str())
612        {
613            if !allowed.contains(&env.to_string()) {
614                return Err(TemplateError::ValidationError(format!(
615                    "Environment '{}' in template '{}' not in allowed list: {:?}",
616                    env, template_name, allowed
617                )));
618            }
619        }
620        Ok(())
621    }
622
623    fn validate_otel_config(parsed: &Value, template_name: &str) -> Result<()> {
624        if let Some(otel) = parsed.get("otel") {
625            let required_fields = ["endpoint", "service_name"];
626
627            for field in &required_fields {
628                if otel.get(*field).is_none() {
629                    return Err(TemplateError::ValidationError(format!(
630                        "Required OTEL field '{}' missing in template '{}'",
631                        field, template_name
632                    )));
633                }
634            }
635
636            // Validate endpoint format
637            if let Some(endpoint) = otel.get("endpoint").and_then(|v| v.as_str()) {
638                if !endpoint.starts_with("http://") && !endpoint.starts_with("https://") {
639                    return Err(TemplateError::ValidationError(format!(
640                        "OTEL endpoint '{}' in template '{}' must start with http:// or https://",
641                        endpoint, template_name
642                    )));
643                }
644            }
645        }
646        Ok(())
647    }
648}
649
650impl std::fmt::Debug for ValidationRule {
651    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
652        match self {
653            ValidationRule::ServiceName => write!(f, "ServiceName"),
654            ValidationRule::Semver => write!(f, "Semver"),
655            ValidationRule::Environment { allowed } => write!(f, "Environment({:?})", allowed),
656            ValidationRule::OtelConfig => write!(f, "OtelConfig"),
657            ValidationRule::Custom { name } => write!(f, "Custom({})", name),
658        }
659    }
660}
661
662impl Clone for ValidationRule {
663    fn clone(&self) -> Self {
664        match self {
665            ValidationRule::ServiceName => ValidationRule::ServiceName,
666            ValidationRule::Semver => ValidationRule::Semver,
667            ValidationRule::Environment { allowed } => ValidationRule::Environment {
668                allowed: allowed.clone(),
669            },
670            ValidationRule::OtelConfig => ValidationRule::OtelConfig,
671            ValidationRule::Custom { name } => ValidationRule::Custom { name: name.clone() },
672        }
673    }
674}
675
676/// Common validation rules
677pub mod rules {
678    use super::*;
679
680    /// Create service name validation rule
681    pub fn service_name() -> ValidationRule {
682        ValidationRule::ServiceName
683    }
684
685    /// Create semver validation rule
686    pub fn semver() -> ValidationRule {
687        ValidationRule::Semver
688    }
689
690    /// Create environment validation rule
691    pub fn environment(allowed: Vec<&str>) -> ValidationRule {
692        ValidationRule::Environment {
693            allowed: allowed.iter().map(|s| s.to_string()).collect(),
694        }
695    }
696
697    /// Create OTEL configuration validation rule
698    pub fn otel_config() -> ValidationRule {
699        ValidationRule::OtelConfig
700    }
701
702    /// Create custom validation rule (simplified version)
703    pub fn custom(name: &str) -> ValidationRule {
704        ValidationRule::Custom {
705            name: name.to_string(),
706        }
707    }
708}
709
710/// Schema validator for JSON Schema validation
711pub struct SchemaValidator {
712    schema: Value,
713}
714
715impl SchemaValidator {
716    /// Create new schema validator
717    ///
718    /// # Arguments
719    /// * `schema` - JSON schema for validation
720    pub fn new(schema: Value) -> Self {
721        Self { schema }
722    }
723
724    /// Validate content against schema
725    pub fn validate(&self, content: &str, template_name: &str) -> Result<()> {
726        let parsed: Value = serde_json::from_str(content).map_err(|e| {
727            TemplateError::ValidationError(format!(
728                "Failed to parse content for schema validation in template '{}': {}",
729                template_name, e
730            ))
731        })?;
732
733        // Simple schema validation implementation
734        // In a real implementation, this would use a proper JSON Schema validator
735        self.validate_against_schema(&parsed, &self.schema, template_name)
736    }
737
738    /// Recursive schema validation
739    fn validate_against_schema(
740        &self,
741        value: &Value,
742        schema: &Value,
743        template_name: &str,
744    ) -> Result<()> {
745        // Check type
746        if let Some(expected_type) = schema.get("type").and_then(|v| v.as_str()) {
747            match expected_type {
748                "object" => {
749                    if !value.is_object() {
750                        return Err(TemplateError::ValidationError(format!(
751                            "Schema validation failed in template '{}': expected object",
752                            template_name
753                        )));
754                    }
755                }
756                "array" => {
757                    if !value.is_array() {
758                        return Err(TemplateError::ValidationError(format!(
759                            "Schema validation failed in template '{}': expected array",
760                            template_name
761                        )));
762                    }
763                }
764                "string" => {
765                    if !value.is_string() {
766                        return Err(TemplateError::ValidationError(format!(
767                            "Schema validation failed in template '{}': expected string",
768                            template_name
769                        )));
770                    }
771                }
772                "number" => {
773                    if !value.is_number() {
774                        return Err(TemplateError::ValidationError(format!(
775                            "Schema validation failed in template '{}': expected number",
776                            template_name
777                        )));
778                    }
779                }
780                "boolean" => {
781                    if !value.is_boolean() {
782                        return Err(TemplateError::ValidationError(format!(
783                            "Schema validation failed in template '{}': expected boolean",
784                            template_name
785                        )));
786                    }
787                }
788                _ => {}
789            }
790        }
791
792        // Check required properties for objects
793        if let (Value::Object(obj), Value::Object(schema_obj)) = (value, schema) {
794            if let Some(required) = schema_obj.get("required").and_then(|v| v.as_array()) {
795                for prop in required {
796                    if let Some(prop_str) = prop.as_str() {
797                        if !obj.contains_key(prop_str) {
798                            return Err(TemplateError::ValidationError(format!(
799                                "Schema validation failed: required property '{}' missing in template '{}'",
800                                prop_str, template_name
801                            )));
802                        }
803                    }
804                }
805            }
806        }
807
808        Ok(())
809    }
810}
811
812#[cfg(test)]
813mod tests {
814    use super::*;
815
816    #[test]
817    fn test_toml_validation() {
818        let validator = TemplateValidator::new()
819            .require_fields(vec!["service.name", "meta.version"])
820            .require_sections(vec!["service", "meta"]);
821
822        let valid_toml = r#"
823[service]
824name = "my-service"
825
826[meta]
827version = "1.0.0"
828        "#;
829
830        assert!(validator.validate(valid_toml, "test").is_ok());
831
832        let invalid_toml = r#"
833[service]
834# missing name field
835        "#;
836
837        assert!(validator.validate(invalid_toml, "test").is_err());
838    }
839
840    #[test]
841    fn test_custom_validation_rules() {
842        let validator = TemplateValidator::new()
843            .with_rule(rules::service_name())
844            .with_rule(rules::semver());
845
846        let valid_content = r#"
847[service]
848name = "my-service"
849
850[meta]
851version = "1.0.0"
852        "#;
853
854        assert!(validator.validate(valid_content, "test").is_ok());
855
856        let invalid_content = r#"
857[service]
858name = "my service!"  # invalid characters
859
860[meta]
861version = "not-semver"
862        "#;
863
864        assert!(validator.validate(invalid_content, "test").is_err());
865    }
866
867    #[test]
868    fn test_format_detection() {
869        let validator = TemplateValidator::new().format(OutputFormat::Auto);
870
871        assert!(validator.validate("name = \"test\"", "test").is_ok()); // TOML
872        assert!(validator.validate("{\"name\": \"test\"}", "test").is_ok()); // JSON
873    }
874}