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 seed: None,
637 };
638
639 let response = llm_client.generate(&request).await?;
640
641 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 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 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 if let Some(text) = response.as_str() {
722 Ok(text.to_string())
723 } else {
724 Ok(base_prompt.to_string())
725 }
726 }
727
728 fn determine_validation_type(&self, errors: &[&ErrorExample]) -> Result<String> {
730 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 if errors[0].status == 400 {
754 Ok("required".to_string())
755 } else {
756 Ok("validation_error".to_string())
757 }
758 }
759
760 fn extract_error_message_template(&self, errors: &[&ErrorExample]) -> Result<String> {
762 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 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 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 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 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 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}