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            seed: None,
637        };
638
639        let response = llm_client.generate(&request).await?;
640
641        // Parse rules from LLM response
642        // The LLM returns JSON with a "rules" array of ConsistencyRule objects
643        let rules = if let Some(rules_array) = response.get("rules").and_then(|v| v.as_array()) {
644            rules_array
645                .iter()
646                .filter_map(|rule_value| {
647                    match serde_json::from_value::<ConsistencyRule>(rule_value.clone()) {
648                        Ok(rule) => Some(rule),
649                        Err(e) => {
650                            tracing::warn!(
651                                error = %e,
652                                "Failed to parse LLM-generated rule, skipping"
653                            );
654                            None
655                        }
656                    }
657                })
658                .collect()
659        } else if let Some(text) = response.as_str() {
660            // Try to extract JSON from a text response
661            if let Some(start) = text.find('[') {
662                if let Some(end) = text.rfind(']') {
663                    match serde_json::from_str::<Vec<ConsistencyRule>>(&text[start..=end]) {
664                        Ok(rules) => rules,
665                        Err(e) => {
666                            tracing::warn!(
667                                error = %e,
668                                "Failed to parse LLM text response as rules array"
669                            );
670                            Vec::new()
671                        }
672                    }
673                } else {
674                    Vec::new()
675                }
676            } else {
677                Vec::new()
678            }
679        } else {
680            Vec::new()
681        };
682
683        Ok(rules)
684    }
685
686    /// Enhance system prompt using LLM
687    async fn enhance_prompt_with_llm(
688        &self,
689        base_prompt: &str,
690        examples: &[ExamplePair],
691    ) -> Result<String> {
692        let llm_client = self
693            .llm_client
694            .as_ref()
695            .ok_or_else(|| mockforge_foundation::Error::internal("LLM client not available"))?;
696
697        let examples_summary: Vec<String> = examples
698            .iter()
699            .take(10)
700            .map(|e| format!("{} {} -> {}", e.method, e.path, e.status))
701            .collect();
702
703        let user_prompt = format!(
704            "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.",
705            base_prompt,
706            examples_summary.join("\n")
707        );
708
709        let request = LlmGenerationRequest {
710            system_prompt: "You are a system prompt generator for API simulation.".to_string(),
711            user_prompt,
712            temperature: 0.7,
713            max_tokens: 500,
714            schema: None,
715            seed: None,
716        };
717
718        let response = llm_client.generate(&request).await?;
719
720        // Extract text from response
721        if let Some(text) = response.as_str() {
722            Ok(text.to_string())
723        } else {
724            Ok(base_prompt.to_string())
725        }
726    }
727
728    /// Determine validation type from error examples
729    fn determine_validation_type(&self, errors: &[&ErrorExample]) -> Result<String> {
730        // Analyze error messages and status codes
731        for error in errors {
732            let error_str =
733                serde_json::to_string(&error.error_response).unwrap_or_default().to_lowercase();
734
735            if error_str.contains("required") || error_str.contains("missing") {
736                return Ok("required".to_string());
737            }
738            if error_str.contains("format") || error_str.contains("invalid format") {
739                return Ok("format".to_string());
740            }
741            if error_str.contains("too short") || error_str.contains("minimum") {
742                return Ok("min_length".to_string());
743            }
744            if error_str.contains("too long") || error_str.contains("maximum") {
745                return Ok("max_length".to_string());
746            }
747            if error_str.contains("pattern") || error_str.contains("regex") {
748                return Ok("pattern".to_string());
749            }
750        }
751
752        // Default to required if status is 400
753        if errors[0].status == 400 {
754            Ok("required".to_string())
755        } else {
756            Ok("validation_error".to_string())
757        }
758    }
759
760    /// Extract error message template
761    fn extract_error_message_template(&self, errors: &[&ErrorExample]) -> Result<String> {
762        // Use first error's message as template
763        if let Some(error) = errors.first() {
764            if let Some(message) = error.error_response.get("message").and_then(|m| m.as_str()) {
765                return Ok(message.to_string());
766            }
767            if let Some(error_field) = error.error_response.get("error").and_then(|e| e.as_str()) {
768                return Ok(error_field.to_string());
769            }
770        }
771
772        Ok("Validation error".to_string())
773    }
774
775    /// Infer format from error messages
776    fn infer_format_from_errors(&self, errors: &[&ErrorExample]) -> Option<String> {
777        for error in errors {
778            let error_str =
779                serde_json::to_string(&error.error_response).unwrap_or_default().to_lowercase();
780
781            if error_str.contains("email") {
782                return Some("email".to_string());
783            }
784            if error_str.contains("url") {
785                return Some("uri".to_string());
786            }
787            if error_str.contains("date") {
788                return Some("date-time".to_string());
789            }
790            if error_str.contains("uuid") {
791                return Some("uuid".to_string());
792            }
793        }
794
795        None
796    }
797
798    /// Infer length constraint from errors
799    fn infer_length_constraint(
800        &self,
801        errors: &[&ErrorExample],
802        _validation_type: &str,
803    ) -> Option<serde_json::Number> {
804        for error in errors {
805            let error_str =
806                serde_json::to_string(&error.error_response).unwrap_or_default().to_lowercase();
807
808            // Try to extract number from error message
809            if let Some(num_str) =
810                error_str.split_whitespace().find_map(|word| word.parse::<u64>().ok())
811            {
812                return Some(serde_json::Number::from(num_str));
813            }
814        }
815
816        None
817    }
818}
819
820#[cfg(test)]
821mod tests {
822    use super::*;
823    use serde_json::json;
824
825    #[tokio::test]
826    async fn test_normalize_path() {
827        let config = BehaviorModelConfig::default();
828        let generator = RuleGenerator::new(config);
829
830        assert_eq!(generator.normalize_path("/api/users/123"), "/api/users/{id}");
831        assert_eq!(generator.normalize_path("/api/users"), "/api/users");
832    }
833
834    #[tokio::test]
835    async fn test_infer_schema_from_value() {
836        let config = BehaviorModelConfig::default();
837        let generator = RuleGenerator::new(config);
838
839        let value = json!({
840            "id": "123",
841            "name": "Alice",
842            "age": 30,
843            "active": true
844        });
845
846        let schema = generator.infer_schema_from_value(&value).unwrap();
847        assert_eq!(schema["type"], "object");
848        assert!(schema["properties"].is_object());
849    }
850
851    #[tokio::test]
852    async fn test_extract_resource_name() {
853        let config = BehaviorModelConfig::default();
854        let generator = RuleGenerator::new(config);
855
856        assert_eq!(generator.extract_resource_name("/api/users"), "users");
857        assert_eq!(generator.extract_resource_name("/api/users/123"), "users");
858    }
859
860    #[tokio::test]
861    async fn test_determine_validation_type() {
862        let config = BehaviorModelConfig::default();
863        let generator = RuleGenerator::new(config);
864
865        let errors = [ErrorExample {
866            method: "POST".to_string(),
867            path: "/api/users".to_string(),
868            request: Some(json!({"name": ""})),
869            status: 400,
870            error_response: json!({"message": "Field is required"}),
871            field: Some("email".to_string()),
872        }];
873
874        let validation_type =
875            generator.determine_validation_type(&errors.iter().collect::<Vec<_>>()).unwrap();
876        assert_eq!(validation_type, "required");
877    }
878
879    #[test]
880    fn test_example_pair_creation() {
881        let mut query_params = HashMap::new();
882        query_params.insert("page".to_string(), "1".to_string());
883
884        let mut headers = HashMap::new();
885        headers.insert("Content-Type".to_string(), "application/json".to_string());
886
887        let pair = ExamplePair {
888            method: "GET".to_string(),
889            path: "/api/users".to_string(),
890            request: None,
891            status: 200,
892            response: Some(json!({"users": []})),
893            query_params,
894            headers,
895            metadata: HashMap::new(),
896        };
897
898        assert_eq!(pair.method, "GET");
899        assert_eq!(pair.path, "/api/users");
900        assert_eq!(pair.status, 200);
901    }
902
903    #[test]
904    fn test_example_pair_serialization() {
905        let pair = ExamplePair {
906            method: "POST".to_string(),
907            path: "/api/users".to_string(),
908            request: Some(json!({"name": "Alice"})),
909            status: 201,
910            response: Some(json!({"id": 1, "name": "Alice"})),
911            query_params: HashMap::new(),
912            headers: HashMap::new(),
913            metadata: HashMap::new(),
914        };
915
916        let json = serde_json::to_string(&pair).unwrap();
917        assert!(json.contains("POST"));
918        assert!(json.contains("/api/users"));
919    }
920
921    #[test]
922    fn test_error_example_creation() {
923        let error = ErrorExample {
924            method: "POST".to_string(),
925            path: "/api/users".to_string(),
926            request: Some(json!({"email": "invalid"})),
927            status: 400,
928            error_response: json!({"error": "Invalid email"}),
929            field: Some("email".to_string()),
930        };
931
932        assert_eq!(error.method, "POST");
933        assert_eq!(error.status, 400);
934        assert_eq!(error.field, Some("email".to_string()));
935    }
936
937    #[test]
938    fn test_error_example_serialization() {
939        let error = ErrorExample {
940            method: "PUT".to_string(),
941            path: "/api/users/1".to_string(),
942            request: None,
943            status: 404,
944            error_response: json!({"error": "Not found"}),
945            field: None,
946        };
947
948        let json = serde_json::to_string(&error).unwrap();
949        assert!(json.contains("404"));
950    }
951
952    #[test]
953    fn test_paginated_response_creation() {
954        let mut query_params = HashMap::new();
955        query_params.insert("page".to_string(), "1".to_string());
956        query_params.insert("limit".to_string(), "10".to_string());
957
958        let response = PaginatedResponse {
959            path: "/api/users".to_string(),
960            query_params,
961            response: json!({"data": [], "page": 1, "total": 100}),
962            page: Some(1),
963            page_size: Some(10),
964            total: Some(100),
965        };
966
967        assert_eq!(response.path, "/api/users");
968        assert_eq!(response.page, Some(1));
969        assert_eq!(response.total, Some(100));
970    }
971
972    #[test]
973    fn test_crud_example_creation() {
974        let crud = CrudExample {
975            operation: "create".to_string(),
976            resource_type: "user".to_string(),
977            path: "/api/users".to_string(),
978            request: Some(json!({"name": "Alice"})),
979            status: 201,
980            response: Some(json!({"id": 1, "name": "Alice"})),
981            resource_state: Some("active".to_string()),
982        };
983
984        assert_eq!(crud.operation, "create");
985        assert_eq!(crud.resource_type, "user");
986        assert_eq!(crud.status, 201);
987    }
988
989    #[test]
990    fn test_validation_rule_creation() {
991        let mut parameters = HashMap::new();
992        parameters.insert("min_length".to_string(), json!(3));
993        parameters.insert("max_length".to_string(), json!(50));
994
995        let rule = ValidationRule {
996            field: "username".to_string(),
997            validation_type: "length".to_string(),
998            parameters,
999            error_message: "Username must be between 3 and 50 characters".to_string(),
1000            status_code: 400,
1001        };
1002
1003        assert_eq!(rule.field, "username");
1004        assert_eq!(rule.validation_type, "length");
1005        assert_eq!(rule.status_code, 400);
1006    }
1007
1008    #[test]
1009    fn test_pagination_rule_creation() {
1010        let mut parameter_names = HashMap::new();
1011        parameter_names.insert("page".to_string(), "page".to_string());
1012        parameter_names.insert("limit".to_string(), "limit".to_string());
1013
1014        let rule = PaginationRule {
1015            default_page_size: 20,
1016            max_page_size: 100,
1017            min_page_size: 1,
1018            parameter_names,
1019            format: "page-based".to_string(),
1020        };
1021
1022        assert_eq!(rule.default_page_size, 20);
1023        assert_eq!(rule.max_page_size, 100);
1024        assert_eq!(rule.format, "page-based");
1025    }
1026
1027    #[test]
1028    fn test_rule_type_serialization() {
1029        let rule_types = vec![
1030            RuleType::Crud,
1031            RuleType::Validation,
1032            RuleType::Pagination,
1033            RuleType::Consistency,
1034            RuleType::StateTransition,
1035            RuleType::Other,
1036        ];
1037
1038        for rule_type in rule_types {
1039            let json = serde_json::to_string(&rule_type).unwrap();
1040            assert!(!json.is_empty());
1041            let deserialized: RuleType = serde_json::from_str(&json).unwrap();
1042            assert_eq!(rule_type, deserialized);
1043        }
1044    }
1045
1046    #[test]
1047    fn test_pattern_match_creation() {
1048        let pattern = PatternMatch {
1049            pattern: "/api/users/*".to_string(),
1050            match_count: 5,
1051            example_ids: vec!["ex1".to_string(), "ex2".to_string()],
1052        };
1053
1054        assert_eq!(pattern.pattern, "/api/users/*");
1055        assert_eq!(pattern.match_count, 5);
1056        assert_eq!(pattern.example_ids.len(), 2);
1057    }
1058
1059    #[test]
1060    fn test_rule_explanation_new() {
1061        let explanation = RuleExplanation::new(
1062            "rule-1".to_string(),
1063            RuleType::Consistency,
1064            0.85,
1065            "Inferred from examples".to_string(),
1066        );
1067
1068        assert_eq!(explanation.rule_id, "rule-1");
1069        assert_eq!(explanation.rule_type, RuleType::Consistency);
1070        assert_eq!(explanation.confidence, 0.85);
1071        assert!(explanation.source_examples.is_empty());
1072    }
1073
1074    #[test]
1075    fn test_rule_explanation_with_source_example() {
1076        let explanation = RuleExplanation::new(
1077            "rule-1".to_string(),
1078            RuleType::Validation,
1079            0.9,
1080            "Test reasoning".to_string(),
1081        )
1082        .with_source_example("example-1".to_string())
1083        .with_source_example("example-2".to_string());
1084
1085        assert_eq!(explanation.source_examples.len(), 2);
1086        assert_eq!(explanation.source_examples[0], "example-1");
1087    }
1088
1089    #[test]
1090    fn test_rule_explanation_with_pattern_match() {
1091        let pattern_match = PatternMatch {
1092            pattern: "/api/*".to_string(),
1093            match_count: 3,
1094            example_ids: vec!["ex1".to_string()],
1095        };
1096
1097        let explanation = RuleExplanation::new(
1098            "rule-1".to_string(),
1099            RuleType::Pagination,
1100            0.75,
1101            "Test".to_string(),
1102        )
1103        .with_pattern_match(pattern_match.clone());
1104
1105        assert_eq!(explanation.pattern_matches.len(), 1);
1106        assert_eq!(explanation.pattern_matches[0].pattern, "/api/*");
1107    }
1108
1109    #[test]
1110    fn test_rule_generator_new() {
1111        let config = BehaviorModelConfig::default();
1112        let generator = RuleGenerator::new(config);
1113        // Just verify it can be created
1114        let _ = generator;
1115    }
1116
1117    #[test]
1118    fn test_rule_generator_new_with_disabled_llm() {
1119        let config = BehaviorModelConfig {
1120            llm_provider: "disabled".to_string(),
1121            ..Default::default()
1122        };
1123        let generator = RuleGenerator::new(config);
1124        // Just verify it can be created
1125        let _ = generator;
1126    }
1127
1128    #[test]
1129    fn test_paginated_response_serialization() {
1130        let mut query_params = HashMap::new();
1131        query_params.insert("page".to_string(), "2".to_string());
1132        let response = PaginatedResponse {
1133            path: "/api/items".to_string(),
1134            query_params: query_params.clone(),
1135            response: json!({"items": []}),
1136            page: Some(2),
1137            page_size: Some(20),
1138            total: Some(50),
1139        };
1140
1141        let json = serde_json::to_string(&response).unwrap();
1142        assert!(json.contains("/api/items"));
1143        assert!(json.contains("2"));
1144    }
1145
1146    #[test]
1147    fn test_crud_example_serialization() {
1148        let crud = CrudExample {
1149            operation: "update".to_string(),
1150            resource_type: "order".to_string(),
1151            path: "/api/orders/123".to_string(),
1152            request: Some(json!({"status": "shipped"})),
1153            status: 200,
1154            response: Some(json!({"id": 123, "status": "shipped"})),
1155            resource_state: Some("shipped".to_string()),
1156        };
1157
1158        let json = serde_json::to_string(&crud).unwrap();
1159        assert!(json.contains("update"));
1160        assert!(json.contains("order"));
1161    }
1162
1163    #[test]
1164    fn test_validation_rule_serialization() {
1165        let mut parameters = HashMap::new();
1166        parameters.insert("pattern".to_string(), json!("^[a-z]+$"));
1167        let rule = ValidationRule {
1168            field: "username".to_string(),
1169            validation_type: "pattern".to_string(),
1170            parameters: parameters.clone(),
1171            error_message: "Invalid format".to_string(),
1172            status_code: 422,
1173        };
1174
1175        let json = serde_json::to_string(&rule).unwrap();
1176        assert!(json.contains("username"));
1177        assert!(json.contains("pattern"));
1178    }
1179
1180    #[test]
1181    fn test_pagination_rule_serialization() {
1182        let mut parameter_names = HashMap::new();
1183        parameter_names.insert("offset".to_string(), "offset".to_string());
1184        parameter_names.insert("limit".to_string(), "limit".to_string());
1185        let rule = PaginationRule {
1186            default_page_size: 25,
1187            max_page_size: 200,
1188            min_page_size: 5,
1189            parameter_names: parameter_names.clone(),
1190            format: "offset-based".to_string(),
1191        };
1192
1193        let json = serde_json::to_string(&rule).unwrap();
1194        assert!(json.contains("offset-based"));
1195        assert!(json.contains("25"));
1196    }
1197
1198    #[test]
1199    fn test_rule_type_variants() {
1200        assert_eq!(RuleType::Crud, RuleType::Crud);
1201        assert_eq!(RuleType::Validation, RuleType::Validation);
1202        assert_eq!(RuleType::Pagination, RuleType::Pagination);
1203        assert_eq!(RuleType::Consistency, RuleType::Consistency);
1204        assert_eq!(RuleType::StateTransition, RuleType::StateTransition);
1205        assert_eq!(RuleType::Other, RuleType::Other);
1206    }
1207
1208    #[test]
1209    fn test_pattern_match_serialization() {
1210        let pattern = PatternMatch {
1211            pattern: "/api/v1/*".to_string(),
1212            match_count: 10,
1213            example_ids: vec!["ex1".to_string(), "ex2".to_string(), "ex3".to_string()],
1214        };
1215
1216        let json = serde_json::to_string(&pattern).unwrap();
1217        assert!(json.contains("/api/v1/*"));
1218        assert!(json.contains("10"));
1219    }
1220
1221    #[test]
1222    fn test_rule_explanation_serialization() {
1223        let explanation = RuleExplanation::new(
1224            "rule-123".to_string(),
1225            RuleType::Consistency,
1226            0.92,
1227            "High confidence rule".to_string(),
1228        )
1229        .with_source_example("ex1".to_string())
1230        .with_pattern_match(PatternMatch {
1231            pattern: "/api/*".to_string(),
1232            match_count: 5,
1233            example_ids: vec!["ex1".to_string()],
1234        });
1235
1236        let json = serde_json::to_string(&explanation).unwrap();
1237        assert!(json.contains("rule-123"));
1238        assert!(json.contains("0.92"));
1239        assert!(json.contains("High confidence"));
1240    }
1241
1242    #[test]
1243    fn test_error_example_with_field() {
1244        let error = ErrorExample {
1245            method: "PATCH".to_string(),
1246            path: "/api/users/1".to_string(),
1247            request: Some(json!({"email": "invalid-email"})),
1248            status: 422,
1249            error_response: json!({"field": "email", "message": "Invalid email format"}),
1250            field: Some("email".to_string()),
1251        };
1252
1253        assert_eq!(error.field, Some("email".to_string()));
1254        assert_eq!(error.status, 422);
1255    }
1256
1257    #[test]
1258    fn test_error_example_without_field() {
1259        let error = ErrorExample {
1260            method: "DELETE".to_string(),
1261            path: "/api/users/999".to_string(),
1262            request: None,
1263            status: 404,
1264            error_response: json!({"error": "Resource not found"}),
1265            field: None,
1266        };
1267
1268        assert!(error.field.is_none());
1269        assert_eq!(error.status, 404);
1270    }
1271
1272    #[test]
1273    fn test_paginated_response_without_pagination_info() {
1274        let response = PaginatedResponse {
1275            path: "/api/data".to_string(),
1276            query_params: HashMap::new(),
1277            response: json!({"data": []}),
1278            page: None,
1279            page_size: None,
1280            total: None,
1281        };
1282
1283        assert!(response.page.is_none());
1284        assert!(response.page_size.is_none());
1285        assert!(response.total.is_none());
1286    }
1287
1288    #[test]
1289    fn test_crud_example_without_state() {
1290        let crud = CrudExample {
1291            operation: "read".to_string(),
1292            resource_type: "product".to_string(),
1293            path: "/api/products/1".to_string(),
1294            request: None,
1295            status: 200,
1296            response: Some(json!({"id": 1, "name": "Product"})),
1297            resource_state: None,
1298        };
1299
1300        assert!(crud.resource_state.is_none());
1301        assert_eq!(crud.operation, "read");
1302    }
1303
1304    #[test]
1305    fn test_validation_rule_without_parameters() {
1306        let rule = ValidationRule {
1307            field: "required_field".to_string(),
1308            validation_type: "required".to_string(),
1309            parameters: HashMap::new(),
1310            error_message: "Field is required".to_string(),
1311            status_code: 400,
1312        };
1313
1314        assert!(rule.parameters.is_empty());
1315        assert_eq!(rule.validation_type, "required");
1316    }
1317
1318    #[test]
1319    fn test_rule_explanation_with_multiple_pattern_matches() {
1320        let explanation = RuleExplanation::new(
1321            "rule-456".to_string(),
1322            RuleType::StateTransition,
1323            0.88,
1324            "Complex rule".to_string(),
1325        )
1326        .with_pattern_match(PatternMatch {
1327            pattern: "/api/v1/*".to_string(),
1328            match_count: 3,
1329            example_ids: vec![],
1330        })
1331        .with_pattern_match(PatternMatch {
1332            pattern: "/api/v2/*".to_string(),
1333            match_count: 2,
1334            example_ids: vec![],
1335        });
1336
1337        assert_eq!(explanation.pattern_matches.len(), 2);
1338    }
1339
1340    #[test]
1341    fn test_example_pair_clone() {
1342        let pair1 = ExamplePair {
1343            method: "GET".to_string(),
1344            path: "/test".to_string(),
1345            request: None,
1346            status: 200,
1347            response: Some(json!({})),
1348            query_params: HashMap::new(),
1349            headers: HashMap::new(),
1350            metadata: HashMap::new(),
1351        };
1352        let pair2 = pair1.clone();
1353        assert_eq!(pair1.method, pair2.method);
1354    }
1355
1356    #[test]
1357    fn test_example_pair_debug() {
1358        let pair = ExamplePair {
1359            method: "POST".to_string(),
1360            path: "/api/test".to_string(),
1361            request: Some(json!({"data": "test"})),
1362            status: 201,
1363            response: Some(json!({"id": 1})),
1364            query_params: HashMap::new(),
1365            headers: HashMap::new(),
1366            metadata: HashMap::new(),
1367        };
1368        let debug_str = format!("{:?}", pair);
1369        assert!(debug_str.contains("ExamplePair"));
1370    }
1371
1372    #[test]
1373    fn test_error_example_clone() {
1374        let error1 = ErrorExample {
1375            method: "PATCH".to_string(),
1376            path: "/test".to_string(),
1377            request: None,
1378            status: 400,
1379            error_response: json!({"error": "Bad request"}),
1380            field: None,
1381        };
1382        let error2 = error1.clone();
1383        assert_eq!(error1.status, error2.status);
1384    }
1385
1386    #[test]
1387    fn test_error_example_debug() {
1388        let error = ErrorExample {
1389            method: "PUT".to_string(),
1390            path: "/api/users/1".to_string(),
1391            request: Some(json!({"email": "invalid"})),
1392            status: 422,
1393            error_response: json!({"field": "email", "message": "Invalid"}),
1394            field: Some("email".to_string()),
1395        };
1396        let debug_str = format!("{:?}", error);
1397        assert!(debug_str.contains("ErrorExample"));
1398    }
1399
1400    #[test]
1401    fn test_paginated_response_clone() {
1402        let response1 = PaginatedResponse {
1403            path: "/api/data".to_string(),
1404            query_params: HashMap::new(),
1405            response: json!({}),
1406            page: Some(1),
1407            page_size: Some(10),
1408            total: Some(100),
1409        };
1410        let response2 = response1.clone();
1411        assert_eq!(response1.page, response2.page);
1412    }
1413
1414    #[test]
1415    fn test_paginated_response_debug() {
1416        let response = PaginatedResponse {
1417            path: "/api/users".to_string(),
1418            query_params: HashMap::from([("page".to_string(), "1".to_string())]),
1419            response: json!({"data": []}),
1420            page: Some(1),
1421            page_size: Some(20),
1422            total: Some(50),
1423        };
1424        let debug_str = format!("{:?}", response);
1425        assert!(debug_str.contains("PaginatedResponse"));
1426    }
1427
1428    #[test]
1429    fn test_crud_example_clone() {
1430        let crud1 = CrudExample {
1431            operation: "create".to_string(),
1432            resource_type: "user".to_string(),
1433            path: "/api/users".to_string(),
1434            request: None,
1435            status: 201,
1436            response: None,
1437            resource_state: None,
1438        };
1439        let crud2 = crud1.clone();
1440        assert_eq!(crud1.operation, crud2.operation);
1441    }
1442
1443    #[test]
1444    fn test_crud_example_debug() {
1445        let crud = CrudExample {
1446            operation: "update".to_string(),
1447            resource_type: "product".to_string(),
1448            path: "/api/products/1".to_string(),
1449            request: Some(json!({"name": "New Name"})),
1450            status: 200,
1451            response: Some(json!({"id": 1, "name": "New Name"})),
1452            resource_state: Some("updated".to_string()),
1453        };
1454        let debug_str = format!("{:?}", crud);
1455        assert!(debug_str.contains("CrudExample"));
1456    }
1457
1458    #[test]
1459    fn test_validation_rule_clone() {
1460        let rule1 = ValidationRule {
1461            field: "email".to_string(),
1462            validation_type: "format".to_string(),
1463            parameters: HashMap::new(),
1464            error_message: "Invalid format".to_string(),
1465            status_code: 400,
1466        };
1467        let rule2 = rule1.clone();
1468        assert_eq!(rule1.field, rule2.field);
1469    }
1470
1471    #[test]
1472    fn test_validation_rule_debug() {
1473        let mut parameters = HashMap::new();
1474        parameters.insert("pattern".to_string(), json!(r"^[a-z]+$"));
1475        let rule = ValidationRule {
1476            field: "username".to_string(),
1477            validation_type: "pattern".to_string(),
1478            parameters,
1479            error_message: "Invalid pattern".to_string(),
1480            status_code: 422,
1481        };
1482        let debug_str = format!("{:?}", rule);
1483        assert!(debug_str.contains("ValidationRule"));
1484    }
1485
1486    #[test]
1487    fn test_pagination_rule_clone() {
1488        let rule1 = PaginationRule {
1489            default_page_size: 20,
1490            max_page_size: 100,
1491            min_page_size: 1,
1492            parameter_names: HashMap::new(),
1493            format: "page-based".to_string(),
1494        };
1495        let rule2 = rule1.clone();
1496        assert_eq!(rule1.default_page_size, rule2.default_page_size);
1497    }
1498
1499    #[test]
1500    fn test_pagination_rule_debug() {
1501        let mut parameter_names = HashMap::new();
1502        parameter_names.insert("page".to_string(), "page".to_string());
1503        parameter_names.insert("size".to_string(), "limit".to_string());
1504        let rule = PaginationRule {
1505            default_page_size: 25,
1506            max_page_size: 200,
1507            min_page_size: 5,
1508            parameter_names,
1509            format: "offset-based".to_string(),
1510        };
1511        let debug_str = format!("{:?}", rule);
1512        assert!(debug_str.contains("PaginationRule"));
1513    }
1514
1515    #[test]
1516    fn test_rule_type_clone() {
1517        let rule_type1 = RuleType::Validation;
1518        let rule_type2 = rule_type1;
1519        assert_eq!(rule_type1, rule_type2);
1520    }
1521
1522    #[test]
1523    fn test_rule_type_debug() {
1524        let rule_type = RuleType::StateTransition;
1525        let debug_str = format!("{:?}", rule_type);
1526        assert!(debug_str.contains("StateTransition") || debug_str.contains("RuleType"));
1527    }
1528
1529    #[test]
1530    fn test_pattern_match_clone() {
1531        let pattern1 = PatternMatch {
1532            pattern: "/api/*".to_string(),
1533            match_count: 10,
1534            example_ids: vec!["ex1".to_string()],
1535        };
1536        let pattern2 = pattern1.clone();
1537        assert_eq!(pattern1.pattern, pattern2.pattern);
1538    }
1539
1540    #[test]
1541    fn test_pattern_match_debug() {
1542        let pattern = PatternMatch {
1543            pattern: "/api/v1/users/*".to_string(),
1544            match_count: 15,
1545            example_ids: vec!["ex1".to_string(), "ex2".to_string(), "ex3".to_string()],
1546        };
1547        let debug_str = format!("{:?}", pattern);
1548        assert!(debug_str.contains("PatternMatch"));
1549    }
1550
1551    #[test]
1552    fn test_rule_explanation_clone() {
1553        let explanation1 = RuleExplanation::new(
1554            "rule-1".to_string(),
1555            RuleType::Consistency,
1556            0.95,
1557            "Test rule".to_string(),
1558        );
1559        let explanation2 = explanation1.clone();
1560        assert_eq!(explanation1.rule_id, explanation2.rule_id);
1561    }
1562
1563    #[test]
1564    fn test_rule_explanation_debug() {
1565        let explanation = RuleExplanation::new(
1566            "rule-123".to_string(),
1567            RuleType::Validation,
1568            0.88,
1569            "Validation rule".to_string(),
1570        )
1571        .with_source_example("ex-1".to_string())
1572        .with_pattern_match(PatternMatch {
1573            pattern: "/api/*".to_string(),
1574            match_count: 5,
1575            example_ids: vec![],
1576        });
1577        let debug_str = format!("{:?}", explanation);
1578        assert!(debug_str.contains("RuleExplanation"));
1579    }
1580}