1mod ai_assisted;
7mod schema_based;
8
9use crate::OpenApiSpec;
10use async_trait::async_trait;
11use chrono;
12use mockforge_foundation::ai_response::{AiResponseConfig, RequestContext};
13use mockforge_foundation::error::Result;
14use mockforge_foundation::intelligent_behavior::Persona;
15use openapiv3::{Operation, ReferenceOr, Response, Responses, Schema};
16use rand::Rng;
17use serde_json::Value;
18use std::collections::HashMap;
19use uuid;
20
21#[async_trait]
26pub trait AiGenerator: Send + Sync {
27 async fn generate(&self, prompt: &str, config: &AiResponseConfig) -> Result<Value>;
36}
37
38pub struct ResponseGenerator;
40
41impl ResponseGenerator {
42 pub fn generate_response(
44 spec: &OpenApiSpec,
45 operation: &Operation,
46 status_code: u16,
47 content_type: Option<&str>,
48 ) -> Result<Value> {
49 Self::generate_response_with_expansion(spec, operation, status_code, content_type, true)
50 }
51
52 pub fn generate_response_with_expansion(
54 spec: &OpenApiSpec,
55 operation: &Operation,
56 status_code: u16,
57 content_type: Option<&str>,
58 expand_tokens: bool,
59 ) -> Result<Value> {
60 Self::generate_response_with_expansion_and_mode(
61 spec,
62 operation,
63 status_code,
64 content_type,
65 expand_tokens,
66 None,
67 None,
68 )
69 }
70
71 pub fn generate_response_with_expansion_and_mode(
73 spec: &OpenApiSpec,
74 operation: &Operation,
75 status_code: u16,
76 content_type: Option<&str>,
77 expand_tokens: bool,
78 selection_mode: Option<crate::response_selection::ResponseSelectionMode>,
79 selector: Option<&crate::response_selection::ResponseSelector>,
80 ) -> Result<Value> {
81 Self::generate_response_with_expansion_and_mode_and_persona(
82 spec,
83 operation,
84 status_code,
85 content_type,
86 expand_tokens,
87 selection_mode,
88 selector,
89 None, )
91 }
92
93 #[allow(clippy::too_many_arguments)]
95 pub fn generate_response_with_expansion_and_mode_and_persona(
96 spec: &OpenApiSpec,
97 operation: &Operation,
98 status_code: u16,
99 content_type: Option<&str>,
100 expand_tokens: bool,
101 selection_mode: Option<crate::response_selection::ResponseSelectionMode>,
102 selector: Option<&crate::response_selection::ResponseSelector>,
103 persona: Option<&Persona>,
104 ) -> Result<Value> {
105 Self::generate_response_with_scenario_and_mode_and_persona(
106 spec,
107 operation,
108 status_code,
109 content_type,
110 expand_tokens,
111 None, selection_mode,
113 selector,
114 persona,
115 )
116 }
117
118 pub fn generate_response_with_scenario(
144 spec: &OpenApiSpec,
145 operation: &Operation,
146 status_code: u16,
147 content_type: Option<&str>,
148 expand_tokens: bool,
149 scenario: Option<&str>,
150 ) -> Result<Value> {
151 Self::generate_response_with_scenario_and_mode(
152 spec,
153 operation,
154 status_code,
155 content_type,
156 expand_tokens,
157 scenario,
158 None,
159 None,
160 )
161 }
162
163 #[allow(clippy::too_many_arguments)]
165 pub fn generate_response_with_scenario_and_mode(
166 spec: &OpenApiSpec,
167 operation: &Operation,
168 status_code: u16,
169 content_type: Option<&str>,
170 expand_tokens: bool,
171 scenario: Option<&str>,
172 selection_mode: Option<crate::response_selection::ResponseSelectionMode>,
173 selector: Option<&crate::response_selection::ResponseSelector>,
174 ) -> Result<Value> {
175 Self::generate_response_with_scenario_and_mode_and_persona(
176 spec,
177 operation,
178 status_code,
179 content_type,
180 expand_tokens,
181 scenario,
182 selection_mode,
183 selector,
184 None, )
186 }
187
188 #[allow(clippy::too_many_arguments)]
190 pub fn generate_response_with_scenario_and_mode_and_persona(
191 spec: &OpenApiSpec,
192 operation: &Operation,
193 status_code: u16,
194 content_type: Option<&str>,
195 expand_tokens: bool,
196 scenario: Option<&str>,
197 selection_mode: Option<crate::response_selection::ResponseSelectionMode>,
198 selector: Option<&crate::response_selection::ResponseSelector>,
199 _persona: Option<&Persona>,
200 ) -> Result<Value> {
201 let response = Self::find_response_for_status(&operation.responses, status_code);
203
204 tracing::debug!(
205 "Finding response for status code {}: {:?}",
206 status_code,
207 if response.is_some() {
208 "found"
209 } else {
210 "not found"
211 }
212 );
213
214 match response {
215 Some(response_ref) => {
216 match response_ref {
217 ReferenceOr::Item(response) => {
218 tracing::debug!(
219 "Using direct response item with {} content types",
220 response.content.len()
221 );
222 Self::generate_from_response_with_scenario_and_mode(
223 spec,
224 response,
225 content_type,
226 expand_tokens,
227 scenario,
228 selection_mode,
229 selector,
230 )
231 }
232 ReferenceOr::Reference { reference } => {
233 tracing::debug!("Resolving response reference: {}", reference);
234 if let Some(resolved_response) = spec.get_response(reference) {
236 tracing::debug!(
237 "Resolved response reference with {} content types",
238 resolved_response.content.len()
239 );
240 Self::generate_from_response_with_scenario_and_mode(
241 spec,
242 resolved_response,
243 content_type,
244 expand_tokens,
245 scenario,
246 selection_mode,
247 selector,
248 )
249 } else {
250 tracing::warn!("Response reference '{}' not found in spec", reference);
251 Ok(Value::Object(serde_json::Map::new()))
253 }
254 }
255 }
256 }
257 None => {
258 tracing::warn!(
259 "No response found for status code {} in operation. Available status codes: {:?}",
260 status_code,
261 operation.responses.responses.keys().collect::<Vec<_>>()
262 );
263 Ok(Value::Object(serde_json::Map::new()))
265 }
266 }
267 }
268
269 fn find_response_for_status(
271 responses: &Responses,
272 status_code: u16,
273 ) -> Option<&ReferenceOr<Response>> {
274 if let Some(response) = responses.responses.get(&openapiv3::StatusCode::Code(status_code)) {
276 return Some(response);
277 }
278
279 if let Some(default_response) = &responses.default {
281 return Some(default_response);
282 }
283
284 None
285 }
286
287 #[allow(dead_code)]
289 fn generate_from_response(
290 spec: &OpenApiSpec,
291 response: &Response,
292 content_type: Option<&str>,
293 expand_tokens: bool,
294 ) -> Result<Value> {
295 Self::generate_from_response_with_scenario(
296 spec,
297 response,
298 content_type,
299 expand_tokens,
300 None,
301 )
302 }
303
304 #[allow(dead_code)]
306 fn generate_from_response_with_scenario(
307 spec: &OpenApiSpec,
308 response: &Response,
309 content_type: Option<&str>,
310 expand_tokens: bool,
311 scenario: Option<&str>,
312 ) -> Result<Value> {
313 Self::generate_from_response_with_scenario_and_mode(
314 spec,
315 response,
316 content_type,
317 expand_tokens,
318 scenario,
319 None,
320 None,
321 )
322 }
323
324 fn generate_from_response_with_scenario_and_mode(
326 spec: &OpenApiSpec,
327 response: &Response,
328 content_type: Option<&str>,
329 expand_tokens: bool,
330 scenario: Option<&str>,
331 selection_mode: Option<crate::response_selection::ResponseSelectionMode>,
332 selector: Option<&crate::response_selection::ResponseSelector>,
333 ) -> Result<Value> {
334 Self::generate_from_response_with_scenario_and_mode_and_persona(
335 spec,
336 response,
337 content_type,
338 expand_tokens,
339 scenario,
340 selection_mode,
341 selector,
342 None, )
344 }
345
346 #[allow(clippy::too_many_arguments)]
348 #[allow(dead_code)]
349 fn generate_from_response_with_scenario_and_mode_and_persona(
350 spec: &OpenApiSpec,
351 response: &Response,
352 content_type: Option<&str>,
353 expand_tokens: bool,
354 scenario: Option<&str>,
355 selection_mode: Option<crate::response_selection::ResponseSelectionMode>,
356 selector: Option<&crate::response_selection::ResponseSelector>,
357 persona: Option<&Persona>,
358 ) -> Result<Value> {
359 if let Some(content_type) = content_type {
361 if let Some(media_type) = response.content.get(content_type) {
362 return Self::generate_from_media_type_with_scenario_and_mode_and_persona(
363 spec,
364 media_type,
365 expand_tokens,
366 scenario,
367 selection_mode,
368 selector,
369 persona,
370 );
371 }
372 }
373
374 let preferred_types = ["application/json", "application/xml", "text/plain"];
376
377 for content_type in &preferred_types {
378 if let Some(media_type) = response.content.get(*content_type) {
379 return Self::generate_from_media_type_with_scenario_and_mode_and_persona(
380 spec,
381 media_type,
382 expand_tokens,
383 scenario,
384 selection_mode,
385 selector,
386 persona,
387 );
388 }
389 }
390
391 if let Some((_, media_type)) = response.content.iter().next() {
393 return Self::generate_from_media_type_with_scenario_and_mode_and_persona(
394 spec,
395 media_type,
396 expand_tokens,
397 scenario,
398 selection_mode,
399 selector,
400 persona,
401 );
402 }
403
404 Ok(Value::Object(serde_json::Map::new()))
406 }
407
408 #[allow(dead_code)]
410 fn generate_from_media_type(
411 spec: &OpenApiSpec,
412 media_type: &openapiv3::MediaType,
413 expand_tokens: bool,
414 ) -> Result<Value> {
415 Self::generate_from_media_type_with_scenario(spec, media_type, expand_tokens, None)
416 }
417
418 #[allow(dead_code)]
420 fn generate_from_media_type_with_scenario(
421 spec: &OpenApiSpec,
422 media_type: &openapiv3::MediaType,
423 expand_tokens: bool,
424 scenario: Option<&str>,
425 ) -> Result<Value> {
426 Self::generate_from_media_type_with_scenario_and_mode(
427 spec,
428 media_type,
429 expand_tokens,
430 scenario,
431 None,
432 None,
433 )
434 }
435
436 #[allow(dead_code)]
438 fn generate_from_media_type_with_scenario_and_mode(
439 spec: &OpenApiSpec,
440 media_type: &openapiv3::MediaType,
441 expand_tokens: bool,
442 scenario: Option<&str>,
443 selection_mode: Option<crate::response_selection::ResponseSelectionMode>,
444 selector: Option<&crate::response_selection::ResponseSelector>,
445 ) -> Result<Value> {
446 Self::generate_from_media_type_with_scenario_and_mode_and_persona(
447 spec,
448 media_type,
449 expand_tokens,
450 scenario,
451 selection_mode,
452 selector,
453 None, )
455 }
456
457 fn generate_from_media_type_with_scenario_and_mode_and_persona(
459 spec: &OpenApiSpec,
460 media_type: &openapiv3::MediaType,
461 expand_tokens: bool,
462 scenario: Option<&str>,
463 selection_mode: Option<crate::response_selection::ResponseSelectionMode>,
464 selector: Option<&crate::response_selection::ResponseSelector>,
465 persona: Option<&Persona>,
466 ) -> Result<Value> {
467 if let Some(example) = &media_type.example {
471 tracing::debug!("Using explicit example from media type: {:?}", example);
472 if expand_tokens {
474 let expanded_example = Self::expand_templates(example);
475 return Ok(expanded_example);
476 } else {
477 return Ok(example.clone());
478 }
479 }
480
481 if !media_type.examples.is_empty() {
485 use crate::response_selection::{ResponseSelectionMode, ResponseSelector};
486
487 tracing::debug!(
488 "Found {} examples in media type, available examples: {:?}",
489 media_type.examples.len(),
490 media_type.examples.keys().collect::<Vec<_>>()
491 );
492
493 if let Some(scenario_name) = scenario {
495 if let Some(example_ref) = media_type.examples.get(scenario_name) {
496 tracing::debug!("Using scenario '{}' from examples map", scenario_name);
497 match Self::extract_example_value_with_persona(
498 spec,
499 example_ref,
500 expand_tokens,
501 persona,
502 media_type.schema.as_ref(),
503 ) {
504 Ok(value) => return Ok(value),
505 Err(e) => {
506 tracing::warn!(
507 "Failed to extract example for scenario '{}': {}, falling back",
508 scenario_name,
509 e
510 );
511 }
512 }
513 } else {
514 tracing::warn!(
515 "Scenario '{}' not found in examples, falling back based on selection mode",
516 scenario_name
517 );
518 }
519 }
520
521 let mode = selection_mode.unwrap_or(ResponseSelectionMode::First);
523
524 let example_names: Vec<String> = media_type.examples.keys().cloned().collect();
526
527 if example_names.is_empty() {
528 tracing::warn!("Examples map is empty, falling back to schema generation");
530 } else if mode == ResponseSelectionMode::Scenario && scenario.is_some() {
531 tracing::debug!("Scenario not found, using selection mode: {:?}", mode);
533 } else {
534 let selected_index = if let Some(sel) = selector {
536 sel.select(&example_names)
537 } else {
538 let temp_selector = ResponseSelector::new(mode);
540 temp_selector.select(&example_names)
541 };
542
543 if let Some(example_name) = example_names.get(selected_index) {
544 if let Some(example_ref) = media_type.examples.get(example_name) {
545 tracing::debug!(
546 "Using example '{}' from examples map (mode: {:?}, index: {})",
547 example_name,
548 mode,
549 selected_index
550 );
551 match Self::extract_example_value_with_persona(
552 spec,
553 example_ref,
554 expand_tokens,
555 persona,
556 media_type.schema.as_ref(),
557 ) {
558 Ok(value) => return Ok(value),
559 Err(e) => {
560 tracing::warn!(
561 "Failed to extract example '{}': {}, trying fallback",
562 example_name,
563 e
564 );
565 }
566 }
567 }
568 }
569 }
570
571 if let Some((example_name, example_ref)) = media_type.examples.iter().next() {
574 tracing::debug!(
575 "Using first example '{}' from examples map as fallback",
576 example_name
577 );
578 match Self::extract_example_value_with_persona(
579 spec,
580 example_ref,
581 expand_tokens,
582 persona,
583 media_type.schema.as_ref(),
584 ) {
585 Ok(value) => {
586 tracing::debug!(
587 "Successfully extracted fallback example '{}'",
588 example_name
589 );
590 return Ok(value);
591 }
592 Err(e) => {
593 tracing::error!(
594 "Failed to extract fallback example '{}': {}, falling back to schema generation",
595 example_name,
596 e
597 );
598 }
600 }
601 }
602 } else {
603 tracing::debug!("No examples found in media type, will use schema generation");
604 }
605
606 if let Some(schema_ref) = &media_type.schema {
609 Ok(Self::generate_example_from_schema_ref(spec, schema_ref, persona))
610 } else {
611 Ok(Value::Object(serde_json::Map::new()))
612 }
613 }
614
615 #[allow(dead_code)]
618 fn extract_example_value(
619 spec: &OpenApiSpec,
620 example_ref: &ReferenceOr<openapiv3::Example>,
621 expand_tokens: bool,
622 ) -> Result<Value> {
623 Self::extract_example_value_with_persona(spec, example_ref, expand_tokens, None, None)
624 }
625
626 fn extract_example_value_with_persona(
628 spec: &OpenApiSpec,
629 example_ref: &ReferenceOr<openapiv3::Example>,
630 expand_tokens: bool,
631 persona: Option<&Persona>,
632 schema_ref: Option<&ReferenceOr<Schema>>,
633 ) -> Result<Value> {
634 let mut value = match example_ref {
635 ReferenceOr::Item(example) => {
636 if let Some(v) = &example.value {
637 tracing::debug!("Using example from examples map: {:?}", v);
638 if expand_tokens {
639 Self::expand_templates(v)
640 } else {
641 v.clone()
642 }
643 } else {
644 return Ok(Value::Object(serde_json::Map::new()));
645 }
646 }
647 ReferenceOr::Reference { reference } => {
648 if let Some(example) = spec.get_example(reference) {
650 if let Some(v) = &example.value {
651 tracing::debug!("Using resolved example reference: {:?}", v);
652 if expand_tokens {
653 Self::expand_templates(v)
654 } else {
655 v.clone()
656 }
657 } else {
658 return Ok(Value::Object(serde_json::Map::new()));
659 }
660 } else {
661 tracing::warn!("Example reference '{}' not found", reference);
662 return Ok(Value::Object(serde_json::Map::new()));
663 }
664 }
665 };
666
667 value = Self::expand_example_items_if_needed(spec, value, persona, schema_ref);
669
670 Ok(value)
671 }
672
673 fn expand_example_items_if_needed(
676 _spec: &OpenApiSpec,
677 mut example: Value,
678 _persona: Option<&Persona>,
679 _schema_ref: Option<&ReferenceOr<Schema>>,
680 ) -> Value {
681 let has_nested_items = example
684 .get("data")
685 .and_then(|v| v.as_object())
686 .map(|obj| obj.contains_key("items"))
687 .unwrap_or(false);
688
689 let has_flat_items = example.get("items").is_some();
690
691 if !has_nested_items && !has_flat_items {
692 return example; }
694
695 let total = example
697 .get("data")
698 .and_then(|d| d.get("total"))
699 .or_else(|| example.get("total"))
700 .and_then(|v| v.as_u64().or_else(|| v.as_i64().map(|i| i as u64)));
701
702 let limit = example
703 .get("data")
704 .and_then(|d| d.get("limit"))
705 .or_else(|| example.get("limit"))
706 .and_then(|v| v.as_u64().or_else(|| v.as_i64().map(|i| i as u64)));
707
708 let items_array = example
710 .get("data")
711 .and_then(|d| d.get("items"))
712 .or_else(|| example.get("items"))
713 .and_then(|v| v.as_array())
714 .cloned();
715
716 if let (Some(total_val), Some(limit_val), Some(mut items)) = (total, limit, items_array) {
717 let current_count = items.len() as u64;
718 let expected_count = std::cmp::min(total_val, limit_val);
719 let max_items = 100; let expected_count = std::cmp::min(expected_count, max_items);
721
722 if current_count < expected_count && !items.is_empty() {
724 tracing::debug!(
725 "Expanding example items array: {} -> {} items (total={}, limit={})",
726 current_count,
727 expected_count,
728 total_val,
729 limit_val
730 );
731
732 let template = items[0].clone();
734 let additional_count = expected_count - current_count;
735
736 for i in 0..additional_count {
738 let mut new_item = template.clone();
739 let item_index = current_count + i + 1;
741 Self::add_item_variation(&mut new_item, item_index);
742 items.push(new_item);
743 }
744
745 if let Some(data_obj) = example.get_mut("data").and_then(|v| v.as_object_mut()) {
747 data_obj.insert("items".to_string(), Value::Array(items));
748 } else if let Some(root_obj) = example.as_object_mut() {
749 root_obj.insert("items".to_string(), Value::Array(items));
750 }
751 }
752 }
753
754 example
755 }
756
757 pub fn generate_from_examples(
759 response: &Response,
760 content_type: Option<&str>,
761 ) -> Result<Option<Value>> {
762 use openapiv3::ReferenceOr;
763
764 if let Some(content_type) = content_type {
766 if let Some(media_type) = response.content.get(content_type) {
767 if let Some(example) = &media_type.example {
769 return Ok(Some(example.clone()));
770 }
771
772 for (_, example_ref) in &media_type.examples {
774 if let ReferenceOr::Item(example) = example_ref {
775 if let Some(value) = &example.value {
776 return Ok(Some(value.clone()));
777 }
778 }
779 }
781 }
782 }
783
784 for (_, media_type) in &response.content {
786 if let Some(example) = &media_type.example {
788 return Ok(Some(example.clone()));
789 }
790
791 for (_, example_ref) in &media_type.examples {
793 if let ReferenceOr::Item(example) = example_ref {
794 if let Some(value) = &example.value {
795 return Ok(Some(value.clone()));
796 }
797 }
798 }
800 }
801
802 Ok(None)
803 }
804
805 fn expand_templates(value: &Value) -> Value {
807 match value {
808 Value::String(s) => {
809 let expanded = s
810 .replace("{{now}}", &chrono::Utc::now().to_rfc3339())
811 .replace("{{uuid}}", &uuid::Uuid::new_v4().to_string());
812 Value::String(expanded)
813 }
814 Value::Object(map) => {
815 let mut new_map = serde_json::Map::new();
816 for (key, val) in map {
817 new_map.insert(key.clone(), Self::expand_templates(val));
818 }
819 Value::Object(new_map)
820 }
821 Value::Array(arr) => {
822 let new_arr: Vec<Value> = arr.iter().map(Self::expand_templates).collect();
823 Value::Array(new_arr)
824 }
825 _ => value.clone(),
826 }
827 }
828}
829
830#[derive(Debug, Clone)]
832pub struct MockResponse {
833 pub status_code: u16,
835 pub headers: HashMap<String, String>,
837 pub body: Option<Value>,
839}
840
841impl MockResponse {
842 pub fn new(status_code: u16) -> Self {
844 Self {
845 status_code,
846 headers: HashMap::new(),
847 body: None,
848 }
849 }
850
851 pub fn with_header(mut self, name: String, value: String) -> Self {
853 self.headers.insert(name, value);
854 self
855 }
856
857 pub fn with_body(mut self, body: Value) -> Self {
859 self.body = Some(body);
860 self
861 }
862}
863
864#[derive(Debug, Clone)]
866pub struct OpenApiSecurityRequirement {
867 pub scheme: String,
869 pub scopes: Vec<String>,
871}
872
873impl OpenApiSecurityRequirement {
874 pub fn new(scheme: String, scopes: Vec<String>) -> Self {
876 Self { scheme, scopes }
877 }
878}
879
880#[derive(Debug, Clone)]
882pub struct OpenApiOperation {
883 pub method: String,
885 pub path: String,
887 pub operation: Operation,
889}
890
891impl OpenApiOperation {
892 pub fn new(method: String, path: String, operation: Operation) -> Self {
894 Self {
895 method,
896 path,
897 operation,
898 }
899 }
900}
901
902#[cfg(test)]
903mod tests {
904 use super::*;
905 use openapiv3::ReferenceOr;
906 use serde_json::json;
907
908 struct MockAiGenerator {
910 response: Value,
911 }
912
913 #[async_trait]
914 impl AiGenerator for MockAiGenerator {
915 async fn generate(&self, _prompt: &str, _config: &AiResponseConfig) -> Result<Value> {
916 Ok(self.response.clone())
917 }
918 }
919
920 #[test]
921 fn generates_example_using_referenced_schemas() {
922 let yaml = r#"
923openapi: 3.0.3
924info:
925 title: Test API
926 version: "1.0.0"
927paths:
928 /apiaries:
929 get:
930 responses:
931 '200':
932 description: ok
933 content:
934 application/json:
935 schema:
936 $ref: '#/components/schemas/Apiary'
937components:
938 schemas:
939 Apiary:
940 type: object
941 properties:
942 id:
943 type: string
944 hive:
945 $ref: '#/components/schemas/Hive'
946 Hive:
947 type: object
948 properties:
949 name:
950 type: string
951 active:
952 type: boolean
953 "#;
954
955 let spec = OpenApiSpec::from_string(yaml, Some("yaml")).expect("load spec");
956 let path_item = spec
957 .spec
958 .paths
959 .paths
960 .get("/apiaries")
961 .and_then(ReferenceOr::as_item)
962 .expect("path item");
963 let operation = path_item.get.as_ref().expect("GET operation");
964
965 let response =
966 ResponseGenerator::generate_response(&spec, operation, 200, Some("application/json"))
967 .expect("generate response");
968
969 let obj = response.as_object().expect("response object");
970 assert!(obj.contains_key("id"));
971 let hive = obj.get("hive").and_then(|value| value.as_object()).expect("hive object");
972 assert!(hive.contains_key("name"));
973 assert!(hive.contains_key("active"));
974 }
975
976 #[tokio::test]
977 async fn test_generate_ai_response_with_generator() {
978 let ai_config = AiResponseConfig {
979 enabled: true,
980 mode: mockforge_foundation::ai_response::AiResponseMode::Intelligent,
981 prompt: Some("Generate a response for {{method}} {{path}}".to_string()),
982 context: None,
983 temperature: 0.7,
984 max_tokens: 1000,
985 schema: None,
986 cache_enabled: true,
987 };
988 let context = RequestContext {
989 method: "GET".to_string(),
990 path: "/api/users".to_string(),
991 path_params: HashMap::new(),
992 query_params: HashMap::new(),
993 headers: HashMap::new(),
994 body: None,
995 multipart_fields: HashMap::new(),
996 multipart_files: HashMap::new(),
997 };
998 let mock_generator = MockAiGenerator {
999 response: json!({"message": "Generated response"}),
1000 };
1001
1002 let result =
1003 ResponseGenerator::generate_ai_response(&ai_config, &context, Some(&mock_generator))
1004 .await;
1005
1006 assert!(result.is_ok());
1007 let value = result.unwrap();
1008 assert_eq!(value["message"], "Generated response");
1009 }
1010
1011 #[tokio::test]
1012 async fn test_generate_ai_response_without_generator() {
1013 let ai_config = AiResponseConfig {
1014 enabled: true,
1015 mode: mockforge_foundation::ai_response::AiResponseMode::Intelligent,
1016 prompt: Some("Generate a response for {{method}} {{path}}".to_string()),
1017 context: None,
1018 temperature: 0.7,
1019 max_tokens: 1000,
1020 schema: None,
1021 cache_enabled: true,
1022 };
1023 let context = RequestContext {
1024 method: "POST".to_string(),
1025 path: "/api/users".to_string(),
1026 path_params: HashMap::new(),
1027 query_params: HashMap::new(),
1028 headers: HashMap::new(),
1029 body: None,
1030 multipart_fields: HashMap::new(),
1031 multipart_files: HashMap::new(),
1032 };
1033
1034 let result = ResponseGenerator::generate_ai_response(&ai_config, &context, None).await;
1035
1036 assert!(result.is_err());
1038 let err = result.unwrap_err().to_string();
1039 assert!(
1040 err.contains("no AI generator configured"),
1041 "Expected 'no AI generator configured' error, got: {}",
1042 err
1043 );
1044 }
1045
1046 #[tokio::test]
1047 async fn test_generate_ai_response_no_prompt() {
1048 let ai_config = AiResponseConfig {
1049 enabled: true,
1050 mode: mockforge_foundation::ai_response::AiResponseMode::Intelligent,
1051 prompt: None,
1052 context: None,
1053 temperature: 0.7,
1054 max_tokens: 1000,
1055 schema: None,
1056 cache_enabled: true,
1057 };
1058 let context = RequestContext {
1059 method: "GET".to_string(),
1060 path: "/api/test".to_string(),
1061 path_params: HashMap::new(),
1062 query_params: HashMap::new(),
1063 headers: HashMap::new(),
1064 body: None,
1065 multipart_fields: HashMap::new(),
1066 multipart_files: HashMap::new(),
1067 };
1068
1069 let result = ResponseGenerator::generate_ai_response(&ai_config, &context, None).await;
1070
1071 assert!(result.is_err());
1072 }
1073
1074 #[test]
1075 fn test_generate_response_with_expansion() {
1076 let spec = OpenApiSpec::from_string(
1077 r#"openapi: 3.0.0
1078info:
1079 title: Test API
1080 version: 1.0.0
1081paths:
1082 /users:
1083 get:
1084 responses:
1085 '200':
1086 description: OK
1087 content:
1088 application/json:
1089 schema:
1090 type: object
1091 properties:
1092 id:
1093 type: integer
1094 name:
1095 type: string
1096"#,
1097 Some("yaml"),
1098 )
1099 .unwrap();
1100
1101 let operation = spec
1102 .spec
1103 .paths
1104 .paths
1105 .get("/users")
1106 .and_then(|p| p.as_item())
1107 .and_then(|p| p.get.as_ref())
1108 .unwrap();
1109
1110 let response = ResponseGenerator::generate_response_with_expansion(
1111 &spec,
1112 operation,
1113 200,
1114 Some("application/json"),
1115 true,
1116 )
1117 .unwrap();
1118
1119 assert!(response.is_object());
1120 }
1121
1122 #[test]
1123 fn test_generate_response_with_scenario() {
1124 let spec = OpenApiSpec::from_string(
1125 r#"openapi: 3.0.0
1126info:
1127 title: Test API
1128 version: 1.0.0
1129paths:
1130 /users:
1131 get:
1132 responses:
1133 '200':
1134 description: OK
1135 content:
1136 application/json:
1137 examples:
1138 happy:
1139 value:
1140 id: 1
1141 name: "Happy User"
1142 sad:
1143 value:
1144 id: 2
1145 name: "Sad User"
1146"#,
1147 Some("yaml"),
1148 )
1149 .unwrap();
1150
1151 let operation = spec
1152 .spec
1153 .paths
1154 .paths
1155 .get("/users")
1156 .and_then(|p| p.as_item())
1157 .and_then(|p| p.get.as_ref())
1158 .unwrap();
1159
1160 let response = ResponseGenerator::generate_response_with_scenario(
1161 &spec,
1162 operation,
1163 200,
1164 Some("application/json"),
1165 false,
1166 Some("happy"),
1167 )
1168 .unwrap();
1169
1170 assert_eq!(response["id"], 1);
1171 assert_eq!(response["name"], "Happy User");
1172 }
1173
1174 #[test]
1175 fn test_generate_response_with_referenced_response() {
1176 let spec = OpenApiSpec::from_string(
1177 r#"openapi: 3.0.0
1178info:
1179 title: Test API
1180 version: 1.0.0
1181paths:
1182 /users:
1183 get:
1184 responses:
1185 '200':
1186 $ref: '#/components/responses/UserResponse'
1187components:
1188 responses:
1189 UserResponse:
1190 description: User response
1191 content:
1192 application/json:
1193 schema:
1194 type: object
1195 properties:
1196 id:
1197 type: integer
1198 name:
1199 type: string
1200"#,
1201 Some("yaml"),
1202 )
1203 .unwrap();
1204
1205 let operation = spec
1206 .spec
1207 .paths
1208 .paths
1209 .get("/users")
1210 .and_then(|p| p.as_item())
1211 .and_then(|p| p.get.as_ref())
1212 .unwrap();
1213
1214 let response =
1215 ResponseGenerator::generate_response(&spec, operation, 200, Some("application/json"))
1216 .unwrap();
1217
1218 assert!(response.is_object());
1219 }
1220
1221 #[test]
1222 fn test_generate_response_with_default_status() {
1223 let spec = OpenApiSpec::from_string(
1224 r#"openapi: 3.0.0
1225info:
1226 title: Test API
1227 version: 1.0.0
1228paths:
1229 /users:
1230 get:
1231 responses:
1232 '200':
1233 description: OK
1234 default:
1235 description: Error
1236 content:
1237 application/json:
1238 schema:
1239 type: object
1240 properties:
1241 error:
1242 type: string
1243"#,
1244 Some("yaml"),
1245 )
1246 .unwrap();
1247
1248 let operation = spec
1249 .spec
1250 .paths
1251 .paths
1252 .get("/users")
1253 .and_then(|p| p.as_item())
1254 .and_then(|p| p.get.as_ref())
1255 .unwrap();
1256
1257 let response =
1259 ResponseGenerator::generate_response(&spec, operation, 500, Some("application/json"))
1260 .unwrap();
1261
1262 assert!(response.is_object());
1263 }
1264
1265 #[test]
1266 fn test_generate_response_with_example_in_media_type() {
1267 let spec = OpenApiSpec::from_string(
1268 r#"openapi: 3.0.0
1269info:
1270 title: Test API
1271 version: 1.0.0
1272paths:
1273 /users:
1274 get:
1275 responses:
1276 '200':
1277 description: OK
1278 content:
1279 application/json:
1280 example:
1281 id: 1
1282 name: "Example User"
1283"#,
1284 Some("yaml"),
1285 )
1286 .unwrap();
1287
1288 let operation = spec
1289 .spec
1290 .paths
1291 .paths
1292 .get("/users")
1293 .and_then(|p| p.as_item())
1294 .and_then(|p| p.get.as_ref())
1295 .unwrap();
1296
1297 let response =
1298 ResponseGenerator::generate_response(&spec, operation, 200, Some("application/json"))
1299 .unwrap();
1300
1301 assert_eq!(response["id"], 1);
1302 assert_eq!(response["name"], "Example User");
1303 }
1304
1305 #[test]
1306 fn test_generate_response_with_schema_example() {
1307 let spec = OpenApiSpec::from_string(
1308 r#"openapi: 3.0.0
1309info:
1310 title: Test API
1311 version: 1.0.0
1312paths:
1313 /users:
1314 get:
1315 responses:
1316 '200':
1317 description: OK
1318 content:
1319 application/json:
1320 schema:
1321 type: object
1322 example:
1323 id: 42
1324 name: "Schema Example"
1325 properties:
1326 id:
1327 type: integer
1328 name:
1329 type: string
1330"#,
1331 Some("yaml"),
1332 )
1333 .unwrap();
1334
1335 let operation = spec
1336 .spec
1337 .paths
1338 .paths
1339 .get("/users")
1340 .and_then(|p| p.as_item())
1341 .and_then(|p| p.get.as_ref())
1342 .unwrap();
1343
1344 let response =
1345 ResponseGenerator::generate_response(&spec, operation, 200, Some("application/json"))
1346 .unwrap();
1347
1348 assert!(response.is_object());
1350 }
1351
1352 #[test]
1353 fn test_generate_response_with_referenced_schema() {
1354 let spec = OpenApiSpec::from_string(
1355 r#"openapi: 3.0.0
1356info:
1357 title: Test API
1358 version: 1.0.0
1359paths:
1360 /users:
1361 get:
1362 responses:
1363 '200':
1364 description: OK
1365 content:
1366 application/json:
1367 schema:
1368 $ref: '#/components/schemas/User'
1369components:
1370 schemas:
1371 User:
1372 type: object
1373 properties:
1374 id:
1375 type: integer
1376 name:
1377 type: string
1378"#,
1379 Some("yaml"),
1380 )
1381 .unwrap();
1382
1383 let operation = spec
1384 .spec
1385 .paths
1386 .paths
1387 .get("/users")
1388 .and_then(|p| p.as_item())
1389 .and_then(|p| p.get.as_ref())
1390 .unwrap();
1391
1392 let response =
1393 ResponseGenerator::generate_response(&spec, operation, 200, Some("application/json"))
1394 .unwrap();
1395
1396 assert!(response.is_object());
1397 assert!(response.get("id").is_some());
1398 assert!(response.get("name").is_some());
1399 }
1400
1401 #[test]
1402 fn test_generate_response_with_array_schema() {
1403 let spec = OpenApiSpec::from_string(
1404 r#"openapi: 3.0.0
1405info:
1406 title: Test API
1407 version: 1.0.0
1408paths:
1409 /users:
1410 get:
1411 responses:
1412 '200':
1413 description: OK
1414 content:
1415 application/json:
1416 schema:
1417 type: array
1418 items:
1419 type: object
1420 properties:
1421 id:
1422 type: integer
1423 name:
1424 type: string
1425"#,
1426 Some("yaml"),
1427 )
1428 .unwrap();
1429
1430 let operation = spec
1431 .spec
1432 .paths
1433 .paths
1434 .get("/users")
1435 .and_then(|p| p.as_item())
1436 .and_then(|p| p.get.as_ref())
1437 .unwrap();
1438
1439 let response =
1440 ResponseGenerator::generate_response(&spec, operation, 200, Some("application/json"))
1441 .unwrap();
1442
1443 assert!(response.is_array());
1444 }
1445
1446 #[test]
1447 fn test_generate_response_with_different_content_types() {
1448 let spec = OpenApiSpec::from_string(
1449 r#"openapi: 3.0.0
1450info:
1451 title: Test API
1452 version: 1.0.0
1453paths:
1454 /users:
1455 get:
1456 responses:
1457 '200':
1458 description: OK
1459 content:
1460 application/json:
1461 schema:
1462 type: object
1463 text/plain:
1464 schema:
1465 type: string
1466"#,
1467 Some("yaml"),
1468 )
1469 .unwrap();
1470
1471 let operation = spec
1472 .spec
1473 .paths
1474 .paths
1475 .get("/users")
1476 .and_then(|p| p.as_item())
1477 .and_then(|p| p.get.as_ref())
1478 .unwrap();
1479
1480 let json_response =
1482 ResponseGenerator::generate_response(&spec, operation, 200, Some("application/json"))
1483 .unwrap();
1484 assert!(json_response.is_object());
1485
1486 let text_response =
1488 ResponseGenerator::generate_response(&spec, operation, 200, Some("text/plain"))
1489 .unwrap();
1490 assert!(text_response.is_string());
1491 }
1492
1493 #[test]
1494 fn test_generate_response_without_content_type() {
1495 let spec = OpenApiSpec::from_string(
1496 r#"openapi: 3.0.0
1497info:
1498 title: Test API
1499 version: 1.0.0
1500paths:
1501 /users:
1502 get:
1503 responses:
1504 '200':
1505 description: OK
1506 content:
1507 application/json:
1508 schema:
1509 type: object
1510 properties:
1511 id:
1512 type: integer
1513"#,
1514 Some("yaml"),
1515 )
1516 .unwrap();
1517
1518 let operation = spec
1519 .spec
1520 .paths
1521 .paths
1522 .get("/users")
1523 .and_then(|p| p.as_item())
1524 .and_then(|p| p.get.as_ref())
1525 .unwrap();
1526
1527 let response = ResponseGenerator::generate_response(&spec, operation, 200, None).unwrap();
1529
1530 assert!(response.is_object());
1531 }
1532
1533 #[test]
1534 fn test_generate_response_with_no_content() {
1535 let spec = OpenApiSpec::from_string(
1536 r#"openapi: 3.0.0
1537info:
1538 title: Test API
1539 version: 1.0.0
1540paths:
1541 /users:
1542 delete:
1543 responses:
1544 '204':
1545 description: No Content
1546"#,
1547 Some("yaml"),
1548 )
1549 .unwrap();
1550
1551 let operation = spec
1552 .spec
1553 .paths
1554 .paths
1555 .get("/users")
1556 .and_then(|p| p.as_item())
1557 .and_then(|p| p.delete.as_ref())
1558 .unwrap();
1559
1560 let response = ResponseGenerator::generate_response(&spec, operation, 204, None).unwrap();
1561
1562 assert!(response.is_object());
1564 assert!(response.as_object().unwrap().is_empty());
1565 }
1566
1567 #[test]
1568 fn test_generate_response_with_expansion_disabled() {
1569 let spec = OpenApiSpec::from_string(
1570 r#"openapi: 3.0.0
1571info:
1572 title: Test API
1573 version: 1.0.0
1574paths:
1575 /users:
1576 get:
1577 responses:
1578 '200':
1579 description: OK
1580 content:
1581 application/json:
1582 schema:
1583 type: object
1584 properties:
1585 id:
1586 type: integer
1587 name:
1588 type: string
1589"#,
1590 Some("yaml"),
1591 )
1592 .unwrap();
1593
1594 let operation = spec
1595 .spec
1596 .paths
1597 .paths
1598 .get("/users")
1599 .and_then(|p| p.as_item())
1600 .and_then(|p| p.get.as_ref())
1601 .unwrap();
1602
1603 let response = ResponseGenerator::generate_response_with_expansion(
1604 &spec,
1605 operation,
1606 200,
1607 Some("application/json"),
1608 false, )
1610 .unwrap();
1611
1612 assert!(response.is_object());
1613 }
1614
1615 #[test]
1616 fn test_generate_response_with_array_schema_referenced_items() {
1617 let spec = OpenApiSpec::from_string(
1619 r#"openapi: 3.0.0
1620info:
1621 title: Test API
1622 version: 1.0.0
1623paths:
1624 /items:
1625 get:
1626 responses:
1627 '200':
1628 description: OK
1629 content:
1630 application/json:
1631 schema:
1632 type: array
1633 items:
1634 $ref: '#/components/schemas/Item'
1635components:
1636 schemas:
1637 Item:
1638 type: object
1639 properties:
1640 id:
1641 type: string
1642 name:
1643 type: string
1644"#,
1645 Some("yaml"),
1646 )
1647 .unwrap();
1648
1649 let operation = spec
1650 .spec
1651 .paths
1652 .paths
1653 .get("/items")
1654 .and_then(|p| p.as_item())
1655 .and_then(|p| p.get.as_ref())
1656 .unwrap();
1657
1658 let response =
1659 ResponseGenerator::generate_response(&spec, operation, 200, Some("application/json"))
1660 .unwrap();
1661
1662 let arr = response.as_array().expect("response should be array");
1664 assert!(!arr.is_empty());
1665 if let Some(item) = arr.first() {
1666 let obj = item.as_object().expect("item should be object");
1667 assert!(obj.contains_key("id") || obj.contains_key("name"));
1668 }
1669 }
1670
1671 #[test]
1672 fn test_generate_response_with_array_schema_missing_reference() {
1673 let spec = OpenApiSpec::from_string(
1675 r#"openapi: 3.0.0
1676info:
1677 title: Test API
1678 version: 1.0.0
1679paths:
1680 /items:
1681 get:
1682 responses:
1683 '200':
1684 description: OK
1685 content:
1686 application/json:
1687 schema:
1688 type: array
1689 items:
1690 $ref: '#/components/schemas/NonExistentItem'
1691components:
1692 schemas: {}
1693"#,
1694 Some("yaml"),
1695 )
1696 .unwrap();
1697
1698 let operation = spec
1699 .spec
1700 .paths
1701 .paths
1702 .get("/items")
1703 .and_then(|p| p.as_item())
1704 .and_then(|p| p.get.as_ref())
1705 .unwrap();
1706
1707 let response =
1708 ResponseGenerator::generate_response(&spec, operation, 200, Some("application/json"))
1709 .unwrap();
1710
1711 let arr = response.as_array().expect("response should be array");
1713 assert!(!arr.is_empty());
1714 }
1715
1716 #[test]
1717 fn test_generate_response_with_array_example_and_pagination() {
1718 let spec = OpenApiSpec::from_string(
1720 r#"openapi: 3.0.0
1721info:
1722 title: Test API
1723 version: 1.0.0
1724paths:
1725 /products:
1726 get:
1727 responses:
1728 '200':
1729 description: OK
1730 content:
1731 application/json:
1732 schema:
1733 type: array
1734 example: [{"id": 1, "name": "Product 1"}]
1735 items:
1736 type: object
1737 properties:
1738 id:
1739 type: integer
1740 name:
1741 type: string
1742"#,
1743 Some("yaml"),
1744 )
1745 .unwrap();
1746
1747 let operation = spec
1748 .spec
1749 .paths
1750 .paths
1751 .get("/products")
1752 .and_then(|p| p.as_item())
1753 .and_then(|p| p.get.as_ref())
1754 .unwrap();
1755
1756 let response =
1757 ResponseGenerator::generate_response(&spec, operation, 200, Some("application/json"))
1758 .unwrap();
1759
1760 let arr = response.as_array().expect("response should be array");
1762 assert!(!arr.is_empty());
1763 if let Some(item) = arr.first() {
1764 let obj = item.as_object().expect("item should be object");
1765 assert!(obj.contains_key("id") || obj.contains_key("name"));
1766 }
1767 }
1768
1769 #[test]
1770 fn test_generate_response_with_missing_response_reference() {
1771 let spec = OpenApiSpec::from_string(
1773 r#"openapi: 3.0.0
1774info:
1775 title: Test API
1776 version: 1.0.0
1777paths:
1778 /users:
1779 get:
1780 responses:
1781 '200':
1782 $ref: '#/components/responses/NonExistentResponse'
1783components:
1784 responses: {}
1785"#,
1786 Some("yaml"),
1787 )
1788 .unwrap();
1789
1790 let operation = spec
1791 .spec
1792 .paths
1793 .paths
1794 .get("/users")
1795 .and_then(|p| p.as_item())
1796 .and_then(|p| p.get.as_ref())
1797 .unwrap();
1798
1799 let response =
1800 ResponseGenerator::generate_response(&spec, operation, 200, Some("application/json"))
1801 .unwrap();
1802
1803 assert!(response.is_object());
1805 assert!(response.as_object().unwrap().is_empty());
1806 }
1807
1808 #[test]
1809 fn test_generate_response_with_no_response_for_status() {
1810 let spec = OpenApiSpec::from_string(
1812 r#"openapi: 3.0.0
1813info:
1814 title: Test API
1815 version: 1.0.0
1816paths:
1817 /users:
1818 get:
1819 responses:
1820 '404':
1821 description: Not found
1822"#,
1823 Some("yaml"),
1824 )
1825 .unwrap();
1826
1827 let operation = spec
1828 .spec
1829 .paths
1830 .paths
1831 .get("/users")
1832 .and_then(|p| p.as_item())
1833 .and_then(|p| p.get.as_ref())
1834 .unwrap();
1835
1836 let response =
1838 ResponseGenerator::generate_response(&spec, operation, 200, Some("application/json"))
1839 .unwrap();
1840
1841 assert!(response.is_object());
1843 assert!(response.as_object().unwrap().is_empty());
1844 }
1845}