1use super::config::BehaviorModelConfig;
8use super::llm_client::LlmClient;
9use super::rules::{ConsistencyRule, RuleAction, StateMachine, StateTransition};
10use super::types::{BehaviorRules, LlmGenerationRequest};
11use mockforge_foundation::Result;
12pub 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
21pub struct RuleGenerator {
23 llm_client: Option<LlmClient>,
25 #[allow(dead_code)]
27 config: BehaviorModelConfig,
28}
29
30impl RuleGenerator {
31 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 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 let path_groups = self.group_by_path_pattern(&examples);
59
60 let consistency_rules = self.infer_consistency_rules(&examples, &path_groups).await?;
62
63 let schemas = self.extract_schemas_from_examples(&examples).await?;
65
66 let state_machines = self.infer_state_machines(&examples).await?;
68
69 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 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 let rules = self.generate_rules_from_examples(examples.clone()).await?;
96
97 let mut explanations = Vec::new();
99
100 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, 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 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, 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 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, format!("Schema for {} resource inferred from response examples", resource_name),
142 );
143 explanations.push(explanation);
144 }
145
146 Ok((rules, explanations))
147 }
148
149 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 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 for (field, errors) in field_errors {
170 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 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 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 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 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 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 let format = formats.first().cloned().unwrap_or_else(|| "page-based".to_string());
256
257 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 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 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 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 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 let base_path = self.normalize_path(&example.path);
311 groups.entry(base_path).or_default().push(example);
312 }
313
314 groups
315 }
316
317 fn normalize_path(&self, path: &str) -> String {
319 path.split('/')
321 .map(|segment| {
322 if segment.parse::<u64>().is_ok() || segment.len() == 36 {
323 "{id}"
325 } else {
326 segment
327 }
328 })
329 .collect::<Vec<_>>()
330 .join("/")
331 }
332
333 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 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 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 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 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 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 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 let resource_name = self.extract_resource_name(&example.path);
417
418 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 #[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 fn extract_resource_name(&self, path: &str) -> String {
478 let segments: Vec<&str> =
480 path.split('/').filter(|s| !s.is_empty() && !s.starts_with('{')).collect();
481
482 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 segments.last().map(|s| s.to_string()).unwrap_or_else(|| "Resource".to_string())
491 }
492
493 async fn infer_state_machines(
495 &self,
496 examples: &[ExamplePair],
497 ) -> Result<HashMap<String, StateMachine>> {
498 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 fn infer_states_from_crud(&self, examples: &[&CrudExample]) -> Result<Vec<String>> {
529 let mut states = vec!["created".to_string(), "active".to_string()];
531
532 if examples.iter().any(|e| e.operation == "delete") {
534 states.push("deleted".to_string());
535 }
536
537 if examples.iter().any(|e| e.operation == "update") {
539 states.push("updated".to_string());
540 }
541
542 Ok(states)
543 }
544
545 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 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 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 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 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 async fn generate_system_prompt(&self, examples: &[ExamplePair]) -> Result<String> {
578 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 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 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 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 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 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, max_tokens: 2000,
635 schema: None,
636 };
637
638 let response = llm_client.generate(&request).await?;
639
640 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 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 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 if let Some(text) = response.as_str() {
720 Ok(text.to_string())
721 } else {
722 Ok(base_prompt.to_string())
723 }
724 }
725
726 fn determine_validation_type(&self, errors: &[&ErrorExample]) -> Result<String> {
728 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 if errors[0].status == 400 {
752 Ok("required".to_string())
753 } else {
754 Ok("validation_error".to_string())
755 }
756 }
757
758 fn extract_error_message_template(&self, errors: &[&ErrorExample]) -> Result<String> {
760 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 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 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 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 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 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}