Skip to main content

mockforge_intelligence/intelligent_behavior/
rule_generator.rs

1//! Rule auto-generation engine for MockAI
2//!
3//! This module analyzes example request/response pairs and OpenAPI specifications
4//! to automatically generate behavioral rules, validation rules, pagination patterns,
5//! and state machines.
6
7use super::config::BehaviorModelConfig;
8use super::llm_client::LlmClient;
9use super::rules::{ConsistencyRule, RuleAction, StateMachine, StateTransition};
10use super::types::{BehaviorRules, LlmGenerationRequest};
11use mockforge_foundation::Result;
12// Data types re-exported from foundation so consumers can use them without
13// depending on deprecated core modules.
14pub use mockforge_foundation::intelligent_behavior::rule_types::{
15    CrudExample, ErrorExample, ExamplePair, PaginatedResponse, PaginationRule, PatternMatch,
16    RuleExplanation, RuleType, ValidationRule,
17};
18use serde_json::Value;
19use std::collections::HashMap;
20
21/// Rule generator that learns from examples
22pub struct RuleGenerator {
23    /// LLM client for intelligent rule generation
24    llm_client: Option<LlmClient>,
25    /// Configuration
26    #[allow(dead_code)]
27    config: BehaviorModelConfig,
28}
29
30impl RuleGenerator {
31    /// Create a new rule generator
32    pub fn new(config: BehaviorModelConfig) -> Self {
33        let llm_client = if config.llm_provider != "disabled" {
34            Some(LlmClient::new(config.clone()))
35        } else {
36            None
37        };
38
39        Self { llm_client, config }
40    }
41
42    /// Generate behavioral rules from example pairs
43    ///
44    /// Analyzes request/response examples to infer:
45    /// - Consistency rules
46    /// - Resource schemas
47    /// - State machines
48    /// - System prompts
49    pub async fn generate_rules_from_examples(
50        &self,
51        examples: Vec<ExamplePair>,
52    ) -> Result<BehaviorRules> {
53        if examples.is_empty() {
54            return Ok(BehaviorRules::default());
55        }
56
57        // Group examples by path pattern
58        let path_groups = self.group_by_path_pattern(&examples);
59
60        // Generate consistency rules from patterns
61        let consistency_rules = self.infer_consistency_rules(&examples, &path_groups).await?;
62
63        // Extract schemas from responses
64        let schemas = self.extract_schemas_from_examples(&examples).await?;
65
66        // Generate state machines from CRUD patterns
67        let state_machines = self.infer_state_machines(&examples).await?;
68
69        // Generate system prompt
70        let system_prompt = self.generate_system_prompt(&examples).await?;
71
72        Ok(BehaviorRules {
73            system_prompt,
74            schemas,
75            consistency_rules,
76            state_transitions: state_machines,
77            max_context_interactions: 10,
78            enable_semantic_search: true,
79        })
80    }
81
82    /// Generate behavioral rules with explanations from example pairs
83    ///
84    /// Similar to `generate_rules_from_examples`, but also returns
85    /// detailed explanations for each generated rule.
86    pub async fn generate_rules_with_explanations(
87        &self,
88        examples: Vec<ExamplePair>,
89    ) -> Result<(BehaviorRules, Vec<RuleExplanation>)> {
90        if examples.is_empty() {
91            return Ok((BehaviorRules::default(), Vec::new()));
92        }
93
94        // Generate rules first
95        let rules = self.generate_rules_from_examples(examples.clone()).await?;
96
97        // Generate explanations for each rule
98        let mut explanations = Vec::new();
99
100        // Explain consistency rules
101        for (idx, rule) in rules.consistency_rules.iter().enumerate() {
102            let rule_id = format!("consistency_rule_{}", idx);
103            let explanation = RuleExplanation::new(
104                rule_id,
105                RuleType::Consistency,
106                0.8, // Default confidence for consistency rules
107                format!(
108                    "Inferred from {} examples matching pattern: {}",
109                    examples.len(),
110                    rule.condition
111                ),
112            )
113            .with_source_example(format!("example_{}", idx));
114            explanations.push(explanation);
115        }
116
117        // Explain state machines
118        for (resource_type, state_machine) in &rules.state_transitions {
119            let rule_id = format!("state_machine_{}", resource_type);
120            let explanation = RuleExplanation::new(
121                rule_id,
122                RuleType::StateTransition,
123                0.85, // Higher confidence for state machines
124                format!(
125                    "State machine for {} with {} states and {} transitions inferred from CRUD patterns",
126                    resource_type,
127                    state_machine.states.len(),
128                    state_machine.transitions.len()
129                ),
130            );
131            explanations.push(explanation);
132        }
133
134        // Explain schemas
135        for resource_name in rules.schemas.keys() {
136            let rule_id = format!("schema_{}", resource_name);
137            let explanation = RuleExplanation::new(
138                rule_id,
139                RuleType::Other,
140                0.75, // Moderate confidence for inferred schemas
141                format!("Schema for {} resource inferred from response examples", resource_name),
142            );
143            explanations.push(explanation);
144        }
145
146        Ok((rules, explanations))
147    }
148
149    /// Infer validation rules from error examples
150    pub async fn infer_validation_rules(
151        &self,
152        error_examples: Vec<ErrorExample>,
153    ) -> Result<Vec<ValidationRule>> {
154        if error_examples.is_empty() {
155            return Ok(Vec::new());
156        }
157
158        let mut rules = Vec::new();
159
160        // Group errors by field and type
161        let mut field_errors: HashMap<String, Vec<&ErrorExample>> = HashMap::new();
162        for error in &error_examples {
163            if let Some(ref field) = error.field {
164                field_errors.entry(field.clone()).or_default().push(error);
165            }
166        }
167
168        // Analyze each field's error patterns
169        for (field, errors) in field_errors {
170            // Determine validation type from error patterns
171            let validation_type = self.determine_validation_type(&errors)?;
172            let error_message = self.extract_error_message_template(&errors)?;
173            let status_code = errors[0].status;
174
175            let mut parameters = HashMap::new();
176            match validation_type.as_str() {
177                "required" => {
178                    parameters.insert("required".to_string(), Value::Bool(true));
179                }
180                "format" => {
181                    // Try to infer format from error message
182                    if let Some(format) = self.infer_format_from_errors(&errors) {
183                        parameters.insert("format".to_string(), Value::String(format));
184                    }
185                }
186                "min_length" | "max_length" => {
187                    // Try to infer length constraints
188                    if let Some(length) = self.infer_length_constraint(&errors, &validation_type) {
189                        parameters.insert(validation_type.clone(), Value::Number(length));
190                    }
191                }
192                _ => {}
193            }
194
195            rules.push(ValidationRule {
196                field,
197                validation_type,
198                parameters,
199                error_message,
200                status_code,
201            });
202        }
203
204        Ok(rules)
205    }
206
207    /// Extract pagination pattern from examples
208    pub async fn extract_pagination_pattern(
209        &self,
210        examples: Vec<PaginatedResponse>,
211    ) -> Result<PaginationRule> {
212        if examples.is_empty() {
213            return Ok(PaginationRule {
214                default_page_size: 20,
215                max_page_size: 100,
216                min_page_size: 1,
217                parameter_names: HashMap::new(),
218                format: "page-based".to_string(),
219            });
220        }
221
222        // Analyze pagination parameters
223        let mut parameter_names = HashMap::new();
224        let mut page_sizes = Vec::new();
225        let mut formats = Vec::new();
226
227        for example in &examples {
228            // Detect pagination parameters
229            for key in example.query_params.keys() {
230                match key.to_lowercase().as_str() {
231                    "page" | "p" => {
232                        parameter_names.insert("page".to_string(), key.clone());
233                    }
234                    "limit" | "per_page" | "size" => {
235                        parameter_names.insert("limit".to_string(), key.clone());
236                    }
237                    "offset" => {
238                        parameter_names.insert("offset".to_string(), key.clone());
239                        formats.push("offset-based".to_string());
240                    }
241                    "cursor" => {
242                        parameter_names.insert("cursor".to_string(), key.clone());
243                        formats.push("cursor-based".to_string());
244                    }
245                    _ => {}
246                }
247            }
248
249            if let Some(size) = example.page_size {
250                page_sizes.push(size);
251            }
252        }
253
254        // Determine format (default to page-based if not detected)
255        let format = formats.first().cloned().unwrap_or_else(|| "page-based".to_string());
256
257        // Calculate page size statistics
258        let default_page_size = page_sizes.iter().copied().min().unwrap_or(20);
259        let max_page_size = page_sizes.iter().copied().max().unwrap_or(100);
260        let min_page_size = 1;
261
262        Ok(PaginationRule {
263            default_page_size,
264            max_page_size,
265            min_page_size,
266            parameter_names,
267            format,
268        })
269    }
270
271    /// Analyze CRUD patterns to generate state machines
272    pub async fn analyze_crud_pattern(
273        &self,
274        examples: Vec<CrudExample>,
275    ) -> Result<HashMap<String, StateMachine>> {
276        let mut machines: HashMap<String, StateMachine> = HashMap::new();
277
278        // Group by resource type
279        let mut resource_groups: HashMap<String, Vec<&CrudExample>> = HashMap::new();
280        for example in &examples {
281            resource_groups.entry(example.resource_type.clone()).or_default().push(example);
282        }
283
284        // Generate state machine for each resource type
285        for (resource_type, resource_examples) in resource_groups {
286            let states = self.infer_states_from_crud(&resource_examples)?;
287            let initial_state = states.first().cloned().unwrap_or_else(|| "created".to_string());
288            let transitions = self.infer_transitions_from_crud(&resource_examples, &states)?;
289
290            let machine = StateMachine::new(resource_type.clone(), states, initial_state)
291                .add_transitions(transitions);
292
293            machines.insert(resource_type, machine);
294        }
295
296        Ok(machines)
297    }
298
299    // ===== Private helper methods =====
300
301    /// Group examples by path pattern
302    fn group_by_path_pattern<'a>(
303        &self,
304        examples: &'a [ExamplePair],
305    ) -> HashMap<String, Vec<&'a ExamplePair>> {
306        let mut groups: HashMap<String, Vec<&'a ExamplePair>> = HashMap::new();
307
308        for example in examples {
309            // Extract base path (remove IDs)
310            let base_path = self.normalize_path(&example.path);
311            groups.entry(base_path).or_default().push(example);
312        }
313
314        groups
315    }
316
317    /// Normalize path by replacing IDs with placeholders
318    fn normalize_path(&self, path: &str) -> String {
319        // Simple heuristic: replace UUIDs and numeric IDs with placeholders
320        path.split('/')
321            .map(|segment| {
322                if segment.parse::<u64>().is_ok() || segment.len() == 36 {
323                    // Likely an ID
324                    "{id}"
325                } else {
326                    segment
327                }
328            })
329            .collect::<Vec<_>>()
330            .join("/")
331    }
332
333    /// Infer consistency rules from examples
334    async fn infer_consistency_rules<'a>(
335        &self,
336        examples: &'a [ExamplePair],
337        _path_groups: &HashMap<String, Vec<&'a ExamplePair>>,
338    ) -> Result<Vec<ConsistencyRule>> {
339        let mut rules = Vec::new();
340
341        // Rule 1: POST creates resources (status 201)
342        for example in examples {
343            if example.method == "POST" && example.status == 201 {
344                let path_pattern = self.normalize_path(&example.path);
345                rules.push(ConsistencyRule::new(
346                    format!("create_{}", path_pattern.replace('/', "_")),
347                    format!("method == 'POST' AND path starts_with '{}'", path_pattern),
348                    RuleAction::Transform {
349                        description: format!("Create new resource at {}", path_pattern),
350                    },
351                ));
352            }
353        }
354
355        // Rule 2: GET retrieves resources (status 200)
356        for example in examples {
357            if example.method == "GET" && example.status == 200 {
358                let path_pattern = self.normalize_path(&example.path);
359                rules.push(ConsistencyRule::new(
360                    format!("get_{}", path_pattern.replace('/', "_")),
361                    format!("method == 'GET' AND path starts_with '{}'", path_pattern),
362                    RuleAction::Transform {
363                        description: format!("Retrieve resource from {}", path_pattern),
364                    },
365                ));
366            }
367        }
368
369        // Rule 3: PUT/PATCH updates resources (status 200)
370        for example in examples {
371            if (example.method == "PUT" || example.method == "PATCH") && example.status == 200 {
372                let path_pattern = self.normalize_path(&example.path);
373                rules.push(ConsistencyRule::new(
374                    format!("update_{}", path_pattern.replace('/', "_")),
375                    format!("method IN ['PUT', 'PATCH'] AND path starts_with '{}'", path_pattern),
376                    RuleAction::Transform {
377                        description: format!("Update resource at {}", path_pattern),
378                    },
379                ));
380            }
381        }
382
383        // Rule 4: DELETE removes resources (status 204 or 200)
384        for example in examples {
385            if example.method == "DELETE" && (example.status == 204 || example.status == 200) {
386                let path_pattern = self.normalize_path(&example.path);
387                rules.push(ConsistencyRule::new(
388                    format!("delete_{}", path_pattern.replace('/', "_")),
389                    format!("method == 'DELETE' AND path starts_with '{}'", path_pattern),
390                    RuleAction::Transform {
391                        description: format!("Delete resource from {}", path_pattern),
392                    },
393                ));
394            }
395        }
396
397        // Use LLM to generate additional rules if available
398        if let Some(ref _llm_client) = self.llm_client {
399            let additional_rules = self.generate_rules_with_llm(examples).await?;
400            rules.extend(additional_rules);
401        }
402
403        Ok(rules)
404    }
405
406    /// Extract schemas from example responses
407    async fn extract_schemas_from_examples(
408        &self,
409        examples: &[ExamplePair],
410    ) -> Result<HashMap<String, Value>> {
411        let mut schemas: HashMap<String, Value> = HashMap::new();
412
413        for example in examples {
414            if let Some(ref response) = example.response {
415                // Extract resource name from path
416                let resource_name = self.extract_resource_name(&example.path);
417
418                // Generate JSON Schema from response
419                if let Some(schema) = self.infer_schema_from_value(response) {
420                    schemas.insert(resource_name, schema);
421                }
422            }
423        }
424
425        Ok(schemas)
426    }
427
428    /// Infer JSON Schema from a JSON value
429    #[allow(clippy::only_used_in_recursion)]
430    fn infer_schema_from_value(&self, value: &Value) -> Option<Value> {
431        match value {
432            Value::Object(obj) => {
433                let mut properties = serde_json::Map::new();
434                let mut required = Vec::new();
435
436                for (key, val) in obj {
437                    if let Some(prop_schema) = self.infer_schema_from_value(val) {
438                        properties.insert(key.clone(), prop_schema);
439                        required.push(key.clone());
440                    }
441                }
442
443                Some(serde_json::json!({
444                    "type": "object",
445                    "properties": properties,
446                    "required": required
447                }))
448            }
449            Value::Array(arr) => {
450                if let Some(first) = arr.first() {
451                    if let Some(item_schema) = self.infer_schema_from_value(first) {
452                        Some(serde_json::json!({
453                            "type": "array",
454                            "items": item_schema
455                        }))
456                    } else {
457                        Some(serde_json::json!({"type": "array"}))
458                    }
459                } else {
460                    Some(serde_json::json!({"type": "array"}))
461                }
462            }
463            Value::String(_) => Some(serde_json::json!({"type": "string"})),
464            Value::Number(n) => {
465                if n.is_i64() {
466                    Some(serde_json::json!({"type": "integer"}))
467                } else {
468                    Some(serde_json::json!({"type": "number"}))
469                }
470            }
471            Value::Bool(_) => Some(serde_json::json!({"type": "boolean"})),
472            Value::Null => None,
473        }
474    }
475
476    /// Extract resource name from path
477    fn extract_resource_name(&self, path: &str) -> String {
478        // Extract last meaningful segment, skipping numeric IDs
479        let segments: Vec<&str> =
480            path.split('/').filter(|s| !s.is_empty() && !s.starts_with('{')).collect();
481
482        // Find the last non-numeric segment (resource name, not ID)
483        for segment in segments.iter().rev() {
484            if !segment.chars().all(|c| c.is_ascii_digit()) {
485                return segment.to_string();
486            }
487        }
488
489        // Fallback to last segment if all are numeric
490        segments.last().map(|s| s.to_string()).unwrap_or_else(|| "Resource".to_string())
491    }
492
493    /// Infer state machines from examples
494    async fn infer_state_machines(
495        &self,
496        examples: &[ExamplePair],
497    ) -> Result<HashMap<String, StateMachine>> {
498        // Convert examples to CRUD examples
499        let crud_examples: Vec<CrudExample> = examples
500            .iter()
501            .filter_map(|ex| {
502                let operation = match ex.method.as_str() {
503                    "POST" => Some("create"),
504                    "GET" => Some("read"),
505                    "PUT" | "PATCH" => Some("update"),
506                    "DELETE" => Some("delete"),
507                    _ => None,
508                }?;
509
510                let resource_type = self.extract_resource_name(&ex.path);
511
512                Some(CrudExample {
513                    operation: operation.to_string(),
514                    resource_type,
515                    path: ex.path.clone(),
516                    request: ex.request.clone(),
517                    status: ex.status,
518                    response: ex.response.clone(),
519                    resource_state: None,
520                })
521            })
522            .collect();
523
524        self.analyze_crud_pattern(crud_examples).await
525    }
526
527    /// Infer states from CRUD examples
528    fn infer_states_from_crud(&self, examples: &[&CrudExample]) -> Result<Vec<String>> {
529        // Default states for CRUD operations
530        let mut states = vec!["created".to_string(), "active".to_string()];
531
532        // Check for delete operations (add deleted state)
533        if examples.iter().any(|e| e.operation == "delete") {
534            states.push("deleted".to_string());
535        }
536
537        // Check for update operations (add updated state)
538        if examples.iter().any(|e| e.operation == "update") {
539            states.push("updated".to_string());
540        }
541
542        Ok(states)
543    }
544
545    /// Infer transitions from CRUD examples
546    fn infer_transitions_from_crud(
547        &self,
548        _examples: &[&CrudExample],
549        states: &[String],
550    ) -> Result<Vec<StateTransition>> {
551        let mut transitions = Vec::new();
552
553        // Create -> Active
554        if states.contains(&"created".to_string()) && states.contains(&"active".to_string()) {
555            transitions.push(StateTransition::new("created", "active").with_probability(1.0));
556        }
557
558        // Active -> Updated
559        if states.contains(&"active".to_string()) && states.contains(&"updated".to_string()) {
560            transitions.push(StateTransition::new("active", "updated").with_probability(0.8));
561        }
562
563        // Updated -> Active (can revert)
564        if states.contains(&"updated".to_string()) && states.contains(&"active".to_string()) {
565            transitions.push(StateTransition::new("updated", "active").with_probability(0.5));
566        }
567
568        // Active -> Deleted
569        if states.contains(&"active".to_string()) && states.contains(&"deleted".to_string()) {
570            transitions.push(StateTransition::new("active", "deleted").with_probability(0.3));
571        }
572
573        Ok(transitions)
574    }
575
576    /// Generate system prompt from examples
577    async fn generate_system_prompt(&self, examples: &[ExamplePair]) -> Result<String> {
578        // Analyze examples to understand API domain
579        let mut methods = std::collections::HashSet::new();
580        let mut paths = std::collections::HashSet::new();
581
582        for example in examples {
583            methods.insert(example.method.clone());
584            paths.insert(self.normalize_path(&example.path));
585        }
586
587        let mut prompt = String::from("You are simulating a realistic REST API. ");
588
589        // Add method information
590        if !methods.is_empty() {
591            let methods_vec: Vec<&str> = methods.iter().map(|s| s.as_str()).collect();
592            prompt.push_str(&format!("Supported methods: {}. ", methods_vec.join(", ")));
593        }
594
595        // Add path information
596        if !paths.is_empty() {
597            let paths_vec: Vec<&str> = paths.iter().take(5).map(|s| s.as_str()).collect();
598            prompt.push_str(&format!("Available endpoints: {}. ", paths_vec.join(", ")));
599        }
600
601        prompt.push_str("Maintain consistency across requests and follow REST conventions.");
602
603        // Use LLM to enhance prompt if available
604        if let Some(ref _llm_client) = self.llm_client {
605            let enhanced = self.enhance_prompt_with_llm(&prompt, examples).await?;
606            return Ok(enhanced);
607        }
608
609        Ok(prompt)
610    }
611
612    /// Generate additional rules using LLM
613    async fn generate_rules_with_llm(
614        &self,
615        examples: &[ExamplePair],
616    ) -> Result<Vec<ConsistencyRule>> {
617        let llm_client = self
618            .llm_client
619            .as_ref()
620            .ok_or_else(|| mockforge_foundation::Error::internal("LLM client not available"))?;
621
622        // Build prompt with examples
623        let examples_json = serde_json::to_string(examples)?;
624        let system_prompt = "You are a rule generation system. Analyze API examples and generate consistency rules.";
625        let user_prompt = format!(
626            "Analyze these API examples and suggest additional consistency rules:\n\n{}",
627            examples_json
628        );
629
630        let request = LlmGenerationRequest {
631            system_prompt: system_prompt.to_string(),
632            user_prompt,
633            temperature: 0.3, // Lower temperature for more consistent rules
634            max_tokens: 2000,
635            schema: None,
636        };
637
638        let response = llm_client.generate(&request).await?;
639
640        // Parse rules from LLM response
641        // The LLM returns JSON with a "rules" array of ConsistencyRule objects
642        let rules = if let Some(rules_array) = response.get("rules").and_then(|v| v.as_array()) {
643            rules_array
644                .iter()
645                .filter_map(|rule_value| {
646                    match serde_json::from_value::<ConsistencyRule>(rule_value.clone()) {
647                        Ok(rule) => Some(rule),
648                        Err(e) => {
649                            tracing::warn!(
650                                error = %e,
651                                "Failed to parse LLM-generated rule, skipping"
652                            );
653                            None
654                        }
655                    }
656                })
657                .collect()
658        } else if let Some(text) = response.as_str() {
659            // Try to extract JSON from a text response
660            if let Some(start) = text.find('[') {
661                if let Some(end) = text.rfind(']') {
662                    match serde_json::from_str::<Vec<ConsistencyRule>>(&text[start..=end]) {
663                        Ok(rules) => rules,
664                        Err(e) => {
665                            tracing::warn!(
666                                error = %e,
667                                "Failed to parse LLM text response as rules array"
668                            );
669                            Vec::new()
670                        }
671                    }
672                } else {
673                    Vec::new()
674                }
675            } else {
676                Vec::new()
677            }
678        } else {
679            Vec::new()
680        };
681
682        Ok(rules)
683    }
684
685    /// Enhance system prompt using LLM
686    async fn enhance_prompt_with_llm(
687        &self,
688        base_prompt: &str,
689        examples: &[ExamplePair],
690    ) -> Result<String> {
691        let llm_client = self
692            .llm_client
693            .as_ref()
694            .ok_or_else(|| mockforge_foundation::Error::internal("LLM client not available"))?;
695
696        let examples_summary: Vec<String> = examples
697            .iter()
698            .take(10)
699            .map(|e| format!("{} {} -> {}", e.method, e.path, e.status))
700            .collect();
701
702        let user_prompt = format!(
703            "Based on this base prompt and API examples, generate an enhanced system prompt:\n\nBase: {}\n\nExamples:\n{}\n\nGenerate a comprehensive system prompt that describes the API behavior.",
704            base_prompt,
705            examples_summary.join("\n")
706        );
707
708        let request = LlmGenerationRequest {
709            system_prompt: "You are a system prompt generator for API simulation.".to_string(),
710            user_prompt,
711            temperature: 0.7,
712            max_tokens: 500,
713            schema: None,
714        };
715
716        let response = llm_client.generate(&request).await?;
717
718        // Extract text from response
719        if let Some(text) = response.as_str() {
720            Ok(text.to_string())
721        } else {
722            Ok(base_prompt.to_string())
723        }
724    }
725
726    /// Determine validation type from error examples
727    fn determine_validation_type(&self, errors: &[&ErrorExample]) -> Result<String> {
728        // Analyze error messages and status codes
729        for error in errors {
730            let error_str =
731                serde_json::to_string(&error.error_response).unwrap_or_default().to_lowercase();
732
733            if error_str.contains("required") || error_str.contains("missing") {
734                return Ok("required".to_string());
735            }
736            if error_str.contains("format") || error_str.contains("invalid format") {
737                return Ok("format".to_string());
738            }
739            if error_str.contains("too short") || error_str.contains("minimum") {
740                return Ok("min_length".to_string());
741            }
742            if error_str.contains("too long") || error_str.contains("maximum") {
743                return Ok("max_length".to_string());
744            }
745            if error_str.contains("pattern") || error_str.contains("regex") {
746                return Ok("pattern".to_string());
747            }
748        }
749
750        // Default to required if status is 400
751        if errors[0].status == 400 {
752            Ok("required".to_string())
753        } else {
754            Ok("validation_error".to_string())
755        }
756    }
757
758    /// Extract error message template
759    fn extract_error_message_template(&self, errors: &[&ErrorExample]) -> Result<String> {
760        // Use first error's message as template
761        if let Some(error) = errors.first() {
762            if let Some(message) = error.error_response.get("message").and_then(|m| m.as_str()) {
763                return Ok(message.to_string());
764            }
765            if let Some(error_field) = error.error_response.get("error").and_then(|e| e.as_str()) {
766                return Ok(error_field.to_string());
767            }
768        }
769
770        Ok("Validation error".to_string())
771    }
772
773    /// Infer format from error messages
774    fn infer_format_from_errors(&self, errors: &[&ErrorExample]) -> Option<String> {
775        for error in errors {
776            let error_str =
777                serde_json::to_string(&error.error_response).unwrap_or_default().to_lowercase();
778
779            if error_str.contains("email") {
780                return Some("email".to_string());
781            }
782            if error_str.contains("url") {
783                return Some("uri".to_string());
784            }
785            if error_str.contains("date") {
786                return Some("date-time".to_string());
787            }
788            if error_str.contains("uuid") {
789                return Some("uuid".to_string());
790            }
791        }
792
793        None
794    }
795
796    /// Infer length constraint from errors
797    fn infer_length_constraint(
798        &self,
799        errors: &[&ErrorExample],
800        _validation_type: &str,
801    ) -> Option<serde_json::Number> {
802        for error in errors {
803            let error_str =
804                serde_json::to_string(&error.error_response).unwrap_or_default().to_lowercase();
805
806            // Try to extract number from error message
807            if let Some(num_str) =
808                error_str.split_whitespace().find_map(|word| word.parse::<u64>().ok())
809            {
810                return Some(serde_json::Number::from(num_str));
811            }
812        }
813
814        None
815    }
816}
817
818#[cfg(test)]
819mod tests {
820    use super::*;
821    use serde_json::json;
822
823    #[tokio::test]
824    async fn test_normalize_path() {
825        let config = BehaviorModelConfig::default();
826        let generator = RuleGenerator::new(config);
827
828        assert_eq!(generator.normalize_path("/api/users/123"), "/api/users/{id}");
829        assert_eq!(generator.normalize_path("/api/users"), "/api/users");
830    }
831
832    #[tokio::test]
833    async fn test_infer_schema_from_value() {
834        let config = BehaviorModelConfig::default();
835        let generator = RuleGenerator::new(config);
836
837        let value = json!({
838            "id": "123",
839            "name": "Alice",
840            "age": 30,
841            "active": true
842        });
843
844        let schema = generator.infer_schema_from_value(&value).unwrap();
845        assert_eq!(schema["type"], "object");
846        assert!(schema["properties"].is_object());
847    }
848
849    #[tokio::test]
850    async fn test_extract_resource_name() {
851        let config = BehaviorModelConfig::default();
852        let generator = RuleGenerator::new(config);
853
854        assert_eq!(generator.extract_resource_name("/api/users"), "users");
855        assert_eq!(generator.extract_resource_name("/api/users/123"), "users");
856    }
857
858    #[tokio::test]
859    async fn test_determine_validation_type() {
860        let config = BehaviorModelConfig::default();
861        let generator = RuleGenerator::new(config);
862
863        let errors = [ErrorExample {
864            method: "POST".to_string(),
865            path: "/api/users".to_string(),
866            request: Some(json!({"name": ""})),
867            status: 400,
868            error_response: json!({"message": "Field is required"}),
869            field: Some("email".to_string()),
870        }];
871
872        let validation_type =
873            generator.determine_validation_type(&errors.iter().collect::<Vec<_>>()).unwrap();
874        assert_eq!(validation_type, "required");
875    }
876
877    #[test]
878    fn test_example_pair_creation() {
879        let mut query_params = HashMap::new();
880        query_params.insert("page".to_string(), "1".to_string());
881
882        let mut headers = HashMap::new();
883        headers.insert("Content-Type".to_string(), "application/json".to_string());
884
885        let pair = ExamplePair {
886            method: "GET".to_string(),
887            path: "/api/users".to_string(),
888            request: None,
889            status: 200,
890            response: Some(json!({"users": []})),
891            query_params,
892            headers,
893            metadata: HashMap::new(),
894        };
895
896        assert_eq!(pair.method, "GET");
897        assert_eq!(pair.path, "/api/users");
898        assert_eq!(pair.status, 200);
899    }
900
901    #[test]
902    fn test_example_pair_serialization() {
903        let pair = ExamplePair {
904            method: "POST".to_string(),
905            path: "/api/users".to_string(),
906            request: Some(json!({"name": "Alice"})),
907            status: 201,
908            response: Some(json!({"id": 1, "name": "Alice"})),
909            query_params: HashMap::new(),
910            headers: HashMap::new(),
911            metadata: HashMap::new(),
912        };
913
914        let json = serde_json::to_string(&pair).unwrap();
915        assert!(json.contains("POST"));
916        assert!(json.contains("/api/users"));
917    }
918
919    #[test]
920    fn test_error_example_creation() {
921        let error = ErrorExample {
922            method: "POST".to_string(),
923            path: "/api/users".to_string(),
924            request: Some(json!({"email": "invalid"})),
925            status: 400,
926            error_response: json!({"error": "Invalid email"}),
927            field: Some("email".to_string()),
928        };
929
930        assert_eq!(error.method, "POST");
931        assert_eq!(error.status, 400);
932        assert_eq!(error.field, Some("email".to_string()));
933    }
934
935    #[test]
936    fn test_error_example_serialization() {
937        let error = ErrorExample {
938            method: "PUT".to_string(),
939            path: "/api/users/1".to_string(),
940            request: None,
941            status: 404,
942            error_response: json!({"error": "Not found"}),
943            field: None,
944        };
945
946        let json = serde_json::to_string(&error).unwrap();
947        assert!(json.contains("404"));
948    }
949
950    #[test]
951    fn test_paginated_response_creation() {
952        let mut query_params = HashMap::new();
953        query_params.insert("page".to_string(), "1".to_string());
954        query_params.insert("limit".to_string(), "10".to_string());
955
956        let response = PaginatedResponse {
957            path: "/api/users".to_string(),
958            query_params,
959            response: json!({"data": [], "page": 1, "total": 100}),
960            page: Some(1),
961            page_size: Some(10),
962            total: Some(100),
963        };
964
965        assert_eq!(response.path, "/api/users");
966        assert_eq!(response.page, Some(1));
967        assert_eq!(response.total, Some(100));
968    }
969
970    #[test]
971    fn test_crud_example_creation() {
972        let crud = CrudExample {
973            operation: "create".to_string(),
974            resource_type: "user".to_string(),
975            path: "/api/users".to_string(),
976            request: Some(json!({"name": "Alice"})),
977            status: 201,
978            response: Some(json!({"id": 1, "name": "Alice"})),
979            resource_state: Some("active".to_string()),
980        };
981
982        assert_eq!(crud.operation, "create");
983        assert_eq!(crud.resource_type, "user");
984        assert_eq!(crud.status, 201);
985    }
986
987    #[test]
988    fn test_validation_rule_creation() {
989        let mut parameters = HashMap::new();
990        parameters.insert("min_length".to_string(), json!(3));
991        parameters.insert("max_length".to_string(), json!(50));
992
993        let rule = ValidationRule {
994            field: "username".to_string(),
995            validation_type: "length".to_string(),
996            parameters,
997            error_message: "Username must be between 3 and 50 characters".to_string(),
998            status_code: 400,
999        };
1000
1001        assert_eq!(rule.field, "username");
1002        assert_eq!(rule.validation_type, "length");
1003        assert_eq!(rule.status_code, 400);
1004    }
1005
1006    #[test]
1007    fn test_pagination_rule_creation() {
1008        let mut parameter_names = HashMap::new();
1009        parameter_names.insert("page".to_string(), "page".to_string());
1010        parameter_names.insert("limit".to_string(), "limit".to_string());
1011
1012        let rule = PaginationRule {
1013            default_page_size: 20,
1014            max_page_size: 100,
1015            min_page_size: 1,
1016            parameter_names,
1017            format: "page-based".to_string(),
1018        };
1019
1020        assert_eq!(rule.default_page_size, 20);
1021        assert_eq!(rule.max_page_size, 100);
1022        assert_eq!(rule.format, "page-based");
1023    }
1024
1025    #[test]
1026    fn test_rule_type_serialization() {
1027        let rule_types = vec![
1028            RuleType::Crud,
1029            RuleType::Validation,
1030            RuleType::Pagination,
1031            RuleType::Consistency,
1032            RuleType::StateTransition,
1033            RuleType::Other,
1034        ];
1035
1036        for rule_type in rule_types {
1037            let json = serde_json::to_string(&rule_type).unwrap();
1038            assert!(!json.is_empty());
1039            let deserialized: RuleType = serde_json::from_str(&json).unwrap();
1040            assert_eq!(rule_type, deserialized);
1041        }
1042    }
1043
1044    #[test]
1045    fn test_pattern_match_creation() {
1046        let pattern = PatternMatch {
1047            pattern: "/api/users/*".to_string(),
1048            match_count: 5,
1049            example_ids: vec!["ex1".to_string(), "ex2".to_string()],
1050        };
1051
1052        assert_eq!(pattern.pattern, "/api/users/*");
1053        assert_eq!(pattern.match_count, 5);
1054        assert_eq!(pattern.example_ids.len(), 2);
1055    }
1056
1057    #[test]
1058    fn test_rule_explanation_new() {
1059        let explanation = RuleExplanation::new(
1060            "rule-1".to_string(),
1061            RuleType::Consistency,
1062            0.85,
1063            "Inferred from examples".to_string(),
1064        );
1065
1066        assert_eq!(explanation.rule_id, "rule-1");
1067        assert_eq!(explanation.rule_type, RuleType::Consistency);
1068        assert_eq!(explanation.confidence, 0.85);
1069        assert!(explanation.source_examples.is_empty());
1070    }
1071
1072    #[test]
1073    fn test_rule_explanation_with_source_example() {
1074        let explanation = RuleExplanation::new(
1075            "rule-1".to_string(),
1076            RuleType::Validation,
1077            0.9,
1078            "Test reasoning".to_string(),
1079        )
1080        .with_source_example("example-1".to_string())
1081        .with_source_example("example-2".to_string());
1082
1083        assert_eq!(explanation.source_examples.len(), 2);
1084        assert_eq!(explanation.source_examples[0], "example-1");
1085    }
1086
1087    #[test]
1088    fn test_rule_explanation_with_pattern_match() {
1089        let pattern_match = PatternMatch {
1090            pattern: "/api/*".to_string(),
1091            match_count: 3,
1092            example_ids: vec!["ex1".to_string()],
1093        };
1094
1095        let explanation = RuleExplanation::new(
1096            "rule-1".to_string(),
1097            RuleType::Pagination,
1098            0.75,
1099            "Test".to_string(),
1100        )
1101        .with_pattern_match(pattern_match.clone());
1102
1103        assert_eq!(explanation.pattern_matches.len(), 1);
1104        assert_eq!(explanation.pattern_matches[0].pattern, "/api/*");
1105    }
1106
1107    #[test]
1108    fn test_rule_generator_new() {
1109        let config = BehaviorModelConfig::default();
1110        let generator = RuleGenerator::new(config);
1111        // Just verify it can be created
1112        let _ = generator;
1113    }
1114
1115    #[test]
1116    fn test_rule_generator_new_with_disabled_llm() {
1117        let config = BehaviorModelConfig {
1118            llm_provider: "disabled".to_string(),
1119            ..Default::default()
1120        };
1121        let generator = RuleGenerator::new(config);
1122        // Just verify it can be created
1123        let _ = generator;
1124    }
1125
1126    #[test]
1127    fn test_paginated_response_serialization() {
1128        let mut query_params = HashMap::new();
1129        query_params.insert("page".to_string(), "2".to_string());
1130        let response = PaginatedResponse {
1131            path: "/api/items".to_string(),
1132            query_params: query_params.clone(),
1133            response: json!({"items": []}),
1134            page: Some(2),
1135            page_size: Some(20),
1136            total: Some(50),
1137        };
1138
1139        let json = serde_json::to_string(&response).unwrap();
1140        assert!(json.contains("/api/items"));
1141        assert!(json.contains("2"));
1142    }
1143
1144    #[test]
1145    fn test_crud_example_serialization() {
1146        let crud = CrudExample {
1147            operation: "update".to_string(),
1148            resource_type: "order".to_string(),
1149            path: "/api/orders/123".to_string(),
1150            request: Some(json!({"status": "shipped"})),
1151            status: 200,
1152            response: Some(json!({"id": 123, "status": "shipped"})),
1153            resource_state: Some("shipped".to_string()),
1154        };
1155
1156        let json = serde_json::to_string(&crud).unwrap();
1157        assert!(json.contains("update"));
1158        assert!(json.contains("order"));
1159    }
1160
1161    #[test]
1162    fn test_validation_rule_serialization() {
1163        let mut parameters = HashMap::new();
1164        parameters.insert("pattern".to_string(), json!("^[a-z]+$"));
1165        let rule = ValidationRule {
1166            field: "username".to_string(),
1167            validation_type: "pattern".to_string(),
1168            parameters: parameters.clone(),
1169            error_message: "Invalid format".to_string(),
1170            status_code: 422,
1171        };
1172
1173        let json = serde_json::to_string(&rule).unwrap();
1174        assert!(json.contains("username"));
1175        assert!(json.contains("pattern"));
1176    }
1177
1178    #[test]
1179    fn test_pagination_rule_serialization() {
1180        let mut parameter_names = HashMap::new();
1181        parameter_names.insert("offset".to_string(), "offset".to_string());
1182        parameter_names.insert("limit".to_string(), "limit".to_string());
1183        let rule = PaginationRule {
1184            default_page_size: 25,
1185            max_page_size: 200,
1186            min_page_size: 5,
1187            parameter_names: parameter_names.clone(),
1188            format: "offset-based".to_string(),
1189        };
1190
1191        let json = serde_json::to_string(&rule).unwrap();
1192        assert!(json.contains("offset-based"));
1193        assert!(json.contains("25"));
1194    }
1195
1196    #[test]
1197    fn test_rule_type_variants() {
1198        assert_eq!(RuleType::Crud, RuleType::Crud);
1199        assert_eq!(RuleType::Validation, RuleType::Validation);
1200        assert_eq!(RuleType::Pagination, RuleType::Pagination);
1201        assert_eq!(RuleType::Consistency, RuleType::Consistency);
1202        assert_eq!(RuleType::StateTransition, RuleType::StateTransition);
1203        assert_eq!(RuleType::Other, RuleType::Other);
1204    }
1205
1206    #[test]
1207    fn test_pattern_match_serialization() {
1208        let pattern = PatternMatch {
1209            pattern: "/api/v1/*".to_string(),
1210            match_count: 10,
1211            example_ids: vec!["ex1".to_string(), "ex2".to_string(), "ex3".to_string()],
1212        };
1213
1214        let json = serde_json::to_string(&pattern).unwrap();
1215        assert!(json.contains("/api/v1/*"));
1216        assert!(json.contains("10"));
1217    }
1218
1219    #[test]
1220    fn test_rule_explanation_serialization() {
1221        let explanation = RuleExplanation::new(
1222            "rule-123".to_string(),
1223            RuleType::Consistency,
1224            0.92,
1225            "High confidence rule".to_string(),
1226        )
1227        .with_source_example("ex1".to_string())
1228        .with_pattern_match(PatternMatch {
1229            pattern: "/api/*".to_string(),
1230            match_count: 5,
1231            example_ids: vec!["ex1".to_string()],
1232        });
1233
1234        let json = serde_json::to_string(&explanation).unwrap();
1235        assert!(json.contains("rule-123"));
1236        assert!(json.contains("0.92"));
1237        assert!(json.contains("High confidence"));
1238    }
1239
1240    #[test]
1241    fn test_error_example_with_field() {
1242        let error = ErrorExample {
1243            method: "PATCH".to_string(),
1244            path: "/api/users/1".to_string(),
1245            request: Some(json!({"email": "invalid-email"})),
1246            status: 422,
1247            error_response: json!({"field": "email", "message": "Invalid email format"}),
1248            field: Some("email".to_string()),
1249        };
1250
1251        assert_eq!(error.field, Some("email".to_string()));
1252        assert_eq!(error.status, 422);
1253    }
1254
1255    #[test]
1256    fn test_error_example_without_field() {
1257        let error = ErrorExample {
1258            method: "DELETE".to_string(),
1259            path: "/api/users/999".to_string(),
1260            request: None,
1261            status: 404,
1262            error_response: json!({"error": "Resource not found"}),
1263            field: None,
1264        };
1265
1266        assert!(error.field.is_none());
1267        assert_eq!(error.status, 404);
1268    }
1269
1270    #[test]
1271    fn test_paginated_response_without_pagination_info() {
1272        let response = PaginatedResponse {
1273            path: "/api/data".to_string(),
1274            query_params: HashMap::new(),
1275            response: json!({"data": []}),
1276            page: None,
1277            page_size: None,
1278            total: None,
1279        };
1280
1281        assert!(response.page.is_none());
1282        assert!(response.page_size.is_none());
1283        assert!(response.total.is_none());
1284    }
1285
1286    #[test]
1287    fn test_crud_example_without_state() {
1288        let crud = CrudExample {
1289            operation: "read".to_string(),
1290            resource_type: "product".to_string(),
1291            path: "/api/products/1".to_string(),
1292            request: None,
1293            status: 200,
1294            response: Some(json!({"id": 1, "name": "Product"})),
1295            resource_state: None,
1296        };
1297
1298        assert!(crud.resource_state.is_none());
1299        assert_eq!(crud.operation, "read");
1300    }
1301
1302    #[test]
1303    fn test_validation_rule_without_parameters() {
1304        let rule = ValidationRule {
1305            field: "required_field".to_string(),
1306            validation_type: "required".to_string(),
1307            parameters: HashMap::new(),
1308            error_message: "Field is required".to_string(),
1309            status_code: 400,
1310        };
1311
1312        assert!(rule.parameters.is_empty());
1313        assert_eq!(rule.validation_type, "required");
1314    }
1315
1316    #[test]
1317    fn test_rule_explanation_with_multiple_pattern_matches() {
1318        let explanation = RuleExplanation::new(
1319            "rule-456".to_string(),
1320            RuleType::StateTransition,
1321            0.88,
1322            "Complex rule".to_string(),
1323        )
1324        .with_pattern_match(PatternMatch {
1325            pattern: "/api/v1/*".to_string(),
1326            match_count: 3,
1327            example_ids: vec![],
1328        })
1329        .with_pattern_match(PatternMatch {
1330            pattern: "/api/v2/*".to_string(),
1331            match_count: 2,
1332            example_ids: vec![],
1333        });
1334
1335        assert_eq!(explanation.pattern_matches.len(), 2);
1336    }
1337
1338    #[test]
1339    fn test_example_pair_clone() {
1340        let pair1 = ExamplePair {
1341            method: "GET".to_string(),
1342            path: "/test".to_string(),
1343            request: None,
1344            status: 200,
1345            response: Some(json!({})),
1346            query_params: HashMap::new(),
1347            headers: HashMap::new(),
1348            metadata: HashMap::new(),
1349        };
1350        let pair2 = pair1.clone();
1351        assert_eq!(pair1.method, pair2.method);
1352    }
1353
1354    #[test]
1355    fn test_example_pair_debug() {
1356        let pair = ExamplePair {
1357            method: "POST".to_string(),
1358            path: "/api/test".to_string(),
1359            request: Some(json!({"data": "test"})),
1360            status: 201,
1361            response: Some(json!({"id": 1})),
1362            query_params: HashMap::new(),
1363            headers: HashMap::new(),
1364            metadata: HashMap::new(),
1365        };
1366        let debug_str = format!("{:?}", pair);
1367        assert!(debug_str.contains("ExamplePair"));
1368    }
1369
1370    #[test]
1371    fn test_error_example_clone() {
1372        let error1 = ErrorExample {
1373            method: "PATCH".to_string(),
1374            path: "/test".to_string(),
1375            request: None,
1376            status: 400,
1377            error_response: json!({"error": "Bad request"}),
1378            field: None,
1379        };
1380        let error2 = error1.clone();
1381        assert_eq!(error1.status, error2.status);
1382    }
1383
1384    #[test]
1385    fn test_error_example_debug() {
1386        let error = ErrorExample {
1387            method: "PUT".to_string(),
1388            path: "/api/users/1".to_string(),
1389            request: Some(json!({"email": "invalid"})),
1390            status: 422,
1391            error_response: json!({"field": "email", "message": "Invalid"}),
1392            field: Some("email".to_string()),
1393        };
1394        let debug_str = format!("{:?}", error);
1395        assert!(debug_str.contains("ErrorExample"));
1396    }
1397
1398    #[test]
1399    fn test_paginated_response_clone() {
1400        let response1 = PaginatedResponse {
1401            path: "/api/data".to_string(),
1402            query_params: HashMap::new(),
1403            response: json!({}),
1404            page: Some(1),
1405            page_size: Some(10),
1406            total: Some(100),
1407        };
1408        let response2 = response1.clone();
1409        assert_eq!(response1.page, response2.page);
1410    }
1411
1412    #[test]
1413    fn test_paginated_response_debug() {
1414        let response = PaginatedResponse {
1415            path: "/api/users".to_string(),
1416            query_params: HashMap::from([("page".to_string(), "1".to_string())]),
1417            response: json!({"data": []}),
1418            page: Some(1),
1419            page_size: Some(20),
1420            total: Some(50),
1421        };
1422        let debug_str = format!("{:?}", response);
1423        assert!(debug_str.contains("PaginatedResponse"));
1424    }
1425
1426    #[test]
1427    fn test_crud_example_clone() {
1428        let crud1 = CrudExample {
1429            operation: "create".to_string(),
1430            resource_type: "user".to_string(),
1431            path: "/api/users".to_string(),
1432            request: None,
1433            status: 201,
1434            response: None,
1435            resource_state: None,
1436        };
1437        let crud2 = crud1.clone();
1438        assert_eq!(crud1.operation, crud2.operation);
1439    }
1440
1441    #[test]
1442    fn test_crud_example_debug() {
1443        let crud = CrudExample {
1444            operation: "update".to_string(),
1445            resource_type: "product".to_string(),
1446            path: "/api/products/1".to_string(),
1447            request: Some(json!({"name": "New Name"})),
1448            status: 200,
1449            response: Some(json!({"id": 1, "name": "New Name"})),
1450            resource_state: Some("updated".to_string()),
1451        };
1452        let debug_str = format!("{:?}", crud);
1453        assert!(debug_str.contains("CrudExample"));
1454    }
1455
1456    #[test]
1457    fn test_validation_rule_clone() {
1458        let rule1 = ValidationRule {
1459            field: "email".to_string(),
1460            validation_type: "format".to_string(),
1461            parameters: HashMap::new(),
1462            error_message: "Invalid format".to_string(),
1463            status_code: 400,
1464        };
1465        let rule2 = rule1.clone();
1466        assert_eq!(rule1.field, rule2.field);
1467    }
1468
1469    #[test]
1470    fn test_validation_rule_debug() {
1471        let mut parameters = HashMap::new();
1472        parameters.insert("pattern".to_string(), json!(r"^[a-z]+$"));
1473        let rule = ValidationRule {
1474            field: "username".to_string(),
1475            validation_type: "pattern".to_string(),
1476            parameters,
1477            error_message: "Invalid pattern".to_string(),
1478            status_code: 422,
1479        };
1480        let debug_str = format!("{:?}", rule);
1481        assert!(debug_str.contains("ValidationRule"));
1482    }
1483
1484    #[test]
1485    fn test_pagination_rule_clone() {
1486        let rule1 = PaginationRule {
1487            default_page_size: 20,
1488            max_page_size: 100,
1489            min_page_size: 1,
1490            parameter_names: HashMap::new(),
1491            format: "page-based".to_string(),
1492        };
1493        let rule2 = rule1.clone();
1494        assert_eq!(rule1.default_page_size, rule2.default_page_size);
1495    }
1496
1497    #[test]
1498    fn test_pagination_rule_debug() {
1499        let mut parameter_names = HashMap::new();
1500        parameter_names.insert("page".to_string(), "page".to_string());
1501        parameter_names.insert("size".to_string(), "limit".to_string());
1502        let rule = PaginationRule {
1503            default_page_size: 25,
1504            max_page_size: 200,
1505            min_page_size: 5,
1506            parameter_names,
1507            format: "offset-based".to_string(),
1508        };
1509        let debug_str = format!("{:?}", rule);
1510        assert!(debug_str.contains("PaginationRule"));
1511    }
1512
1513    #[test]
1514    fn test_rule_type_clone() {
1515        let rule_type1 = RuleType::Validation;
1516        let rule_type2 = rule_type1;
1517        assert_eq!(rule_type1, rule_type2);
1518    }
1519
1520    #[test]
1521    fn test_rule_type_debug() {
1522        let rule_type = RuleType::StateTransition;
1523        let debug_str = format!("{:?}", rule_type);
1524        assert!(debug_str.contains("StateTransition") || debug_str.contains("RuleType"));
1525    }
1526
1527    #[test]
1528    fn test_pattern_match_clone() {
1529        let pattern1 = PatternMatch {
1530            pattern: "/api/*".to_string(),
1531            match_count: 10,
1532            example_ids: vec!["ex1".to_string()],
1533        };
1534        let pattern2 = pattern1.clone();
1535        assert_eq!(pattern1.pattern, pattern2.pattern);
1536    }
1537
1538    #[test]
1539    fn test_pattern_match_debug() {
1540        let pattern = PatternMatch {
1541            pattern: "/api/v1/users/*".to_string(),
1542            match_count: 15,
1543            example_ids: vec!["ex1".to_string(), "ex2".to_string(), "ex3".to_string()],
1544        };
1545        let debug_str = format!("{:?}", pattern);
1546        assert!(debug_str.contains("PatternMatch"));
1547    }
1548
1549    #[test]
1550    fn test_rule_explanation_clone() {
1551        let explanation1 = RuleExplanation::new(
1552            "rule-1".to_string(),
1553            RuleType::Consistency,
1554            0.95,
1555            "Test rule".to_string(),
1556        );
1557        let explanation2 = explanation1.clone();
1558        assert_eq!(explanation1.rule_id, explanation2.rule_id);
1559    }
1560
1561    #[test]
1562    fn test_rule_explanation_debug() {
1563        let explanation = RuleExplanation::new(
1564            "rule-123".to_string(),
1565            RuleType::Validation,
1566            0.88,
1567            "Validation rule".to_string(),
1568        )
1569        .with_source_example("ex-1".to_string())
1570        .with_pattern_match(PatternMatch {
1571            pattern: "/api/*".to_string(),
1572            match_count: 5,
1573            example_ids: vec![],
1574        });
1575        let debug_str = format!("{:?}", explanation);
1576        assert!(debug_str.contains("RuleExplanation"));
1577    }
1578}