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
38fn inject_response_violations_enabled() -> bool {
45 std::env::var("MOCKFORGE_INJECT_RESPONSE_VIOLATIONS")
46 .ok()
47 .map(|s| matches!(s.to_ascii_lowercase().as_str(), "1" | "true" | "yes" | "on"))
48 .unwrap_or(false)
49}
50
51pub struct ResponseGenerator;
53
54impl ResponseGenerator {
55 pub fn generate_response(
57 spec: &OpenApiSpec,
58 operation: &Operation,
59 status_code: u16,
60 content_type: Option<&str>,
61 ) -> Result<Value> {
62 Self::generate_response_with_expansion(spec, operation, status_code, content_type, true)
63 }
64
65 pub fn generate_response_with_expansion(
67 spec: &OpenApiSpec,
68 operation: &Operation,
69 status_code: u16,
70 content_type: Option<&str>,
71 expand_tokens: bool,
72 ) -> Result<Value> {
73 Self::generate_response_with_expansion_and_mode(
74 spec,
75 operation,
76 status_code,
77 content_type,
78 expand_tokens,
79 None,
80 None,
81 )
82 }
83
84 pub fn generate_response_with_expansion_and_mode(
86 spec: &OpenApiSpec,
87 operation: &Operation,
88 status_code: u16,
89 content_type: Option<&str>,
90 expand_tokens: bool,
91 selection_mode: Option<crate::response_selection::ResponseSelectionMode>,
92 selector: Option<&crate::response_selection::ResponseSelector>,
93 ) -> Result<Value> {
94 Self::generate_response_with_expansion_and_mode_and_persona(
95 spec,
96 operation,
97 status_code,
98 content_type,
99 expand_tokens,
100 selection_mode,
101 selector,
102 None, )
104 }
105
106 #[allow(clippy::too_many_arguments)]
108 pub fn generate_response_with_expansion_and_mode_and_persona(
109 spec: &OpenApiSpec,
110 operation: &Operation,
111 status_code: u16,
112 content_type: Option<&str>,
113 expand_tokens: bool,
114 selection_mode: Option<crate::response_selection::ResponseSelectionMode>,
115 selector: Option<&crate::response_selection::ResponseSelector>,
116 persona: Option<&Persona>,
117 ) -> Result<Value> {
118 Self::generate_response_with_scenario_and_mode_and_persona(
119 spec,
120 operation,
121 status_code,
122 content_type,
123 expand_tokens,
124 None, selection_mode,
126 selector,
127 persona,
128 )
129 }
130
131 pub fn generate_response_with_scenario(
157 spec: &OpenApiSpec,
158 operation: &Operation,
159 status_code: u16,
160 content_type: Option<&str>,
161 expand_tokens: bool,
162 scenario: Option<&str>,
163 ) -> Result<Value> {
164 Self::generate_response_with_scenario_and_mode(
165 spec,
166 operation,
167 status_code,
168 content_type,
169 expand_tokens,
170 scenario,
171 None,
172 None,
173 )
174 }
175
176 #[allow(clippy::too_many_arguments)]
178 pub fn generate_response_with_scenario_and_mode(
179 spec: &OpenApiSpec,
180 operation: &Operation,
181 status_code: u16,
182 content_type: Option<&str>,
183 expand_tokens: bool,
184 scenario: Option<&str>,
185 selection_mode: Option<crate::response_selection::ResponseSelectionMode>,
186 selector: Option<&crate::response_selection::ResponseSelector>,
187 ) -> Result<Value> {
188 Self::generate_response_with_scenario_and_mode_and_persona(
189 spec,
190 operation,
191 status_code,
192 content_type,
193 expand_tokens,
194 scenario,
195 selection_mode,
196 selector,
197 None, )
199 }
200
201 #[allow(clippy::too_many_arguments)]
203 pub fn generate_response_with_scenario_and_mode_and_persona(
204 spec: &OpenApiSpec,
205 operation: &Operation,
206 status_code: u16,
207 content_type: Option<&str>,
208 expand_tokens: bool,
209 scenario: Option<&str>,
210 selection_mode: Option<crate::response_selection::ResponseSelectionMode>,
211 selector: Option<&crate::response_selection::ResponseSelector>,
212 _persona: Option<&Persona>,
213 ) -> Result<Value> {
214 Self::generate_response_with_policy(
215 spec,
216 operation,
217 status_code,
218 content_type,
219 expand_tokens,
220 scenario,
221 selection_mode,
222 selector,
223 true,
224 )
225 }
226
227 pub(crate) fn generate_hosted_response(
229 spec: &OpenApiSpec,
230 operation: &Operation,
231 status_code: u16,
232 scenario: Option<&str>,
233 selection_mode: crate::response_selection::ResponseSelectionMode,
234 selector: &crate::response_selection::ResponseSelector,
235 ) -> Result<Value> {
236 Self::generate_response_with_policy(
237 spec,
238 operation,
239 status_code,
240 None,
241 false,
242 scenario,
243 Some(selection_mode),
244 Some(selector),
245 false,
246 )
247 }
248
249 #[allow(clippy::too_many_arguments)]
250 fn generate_response_with_policy(
251 spec: &OpenApiSpec,
252 operation: &Operation,
253 status_code: u16,
254 content_type: Option<&str>,
255 expand_tokens: bool,
256 scenario: Option<&str>,
257 selection_mode: Option<crate::response_selection::ResponseSelectionMode>,
258 selector: Option<&crate::response_selection::ResponseSelector>,
259 allow_process_fault_injection: bool,
260 ) -> Result<Value> {
261 let response = Self::find_response_for_status(&operation.responses, status_code);
263
264 tracing::debug!(
265 "Finding response for status code {}: {:?}",
266 status_code,
267 if response.is_some() {
268 "found"
269 } else {
270 "not found"
271 }
272 );
273
274 let body = match response {
275 Some(response_ref) => match response_ref {
276 ReferenceOr::Item(response) => {
277 tracing::debug!(
278 "Using direct response item with {} content types",
279 response.content.len()
280 );
281 Self::generate_from_response_with_scenario_and_mode(
282 spec,
283 response,
284 content_type,
285 expand_tokens,
286 scenario,
287 selection_mode,
288 selector,
289 )
290 }
291 ReferenceOr::Reference { reference } => {
292 tracing::debug!("Resolving response reference: {}", reference);
293 if let Some(resolved_response) = spec.get_response(reference) {
294 tracing::debug!(
295 "Resolved response reference with {} content types",
296 resolved_response.content.len()
297 );
298 Self::generate_from_response_with_scenario_and_mode(
299 spec,
300 resolved_response,
301 content_type,
302 expand_tokens,
303 scenario,
304 selection_mode,
305 selector,
306 )
307 } else {
308 tracing::warn!("Response reference '{}' not found in spec", reference);
309 Ok(Value::Object(serde_json::Map::new()))
310 }
311 }
312 },
313 None => {
314 tracing::warn!(
315 "No response found for status code {} in operation. Available status codes: {:?}",
316 status_code,
317 operation.responses.responses.keys().collect::<Vec<_>>()
318 );
319 Ok(Value::Object(serde_json::Map::new()))
320 }
321 };
322
323 if allow_process_fault_injection {
332 body.map(|b| Self::maybe_inject_response_violation(spec, operation, status_code, b))
333 } else {
334 body
335 }
336 }
337
338 fn maybe_inject_response_violation(
345 spec: &OpenApiSpec,
346 operation: &Operation,
347 status_code: u16,
348 body: Value,
349 ) -> Value {
350 if !inject_response_violations_enabled() {
351 return body;
352 }
353 if !(200..300).contains(&status_code) {
354 return body;
355 }
356 let Some(required_field) =
357 Self::first_required_field_for_status(spec, operation, status_code)
358 else {
359 return body;
360 };
361 match body {
362 Value::Object(mut map) => {
363 if map.remove(&required_field).is_some() {
364 tracing::info!(
365 "MOCKFORGE_INJECT_RESPONSE_VIOLATIONS dropped required field '{required_field}' from {status_code} response"
366 );
367 }
368 Value::Object(map)
369 }
370 other => other,
371 }
372 }
373
374 fn first_required_field_for_status(
380 spec: &OpenApiSpec,
381 operation: &Operation,
382 status_code: u16,
383 ) -> Option<String> {
384 let response_ref = Self::find_response_for_status(&operation.responses, status_code)?;
385 let response: &Response = match response_ref {
386 ReferenceOr::Item(r) => r,
387 ReferenceOr::Reference { reference } => spec.get_response(reference)?,
388 };
389 let media = response.content.get("application/json")?;
390 let schema_ref = media.schema.as_ref()?;
391 let schema = match schema_ref {
392 ReferenceOr::Item(s) => s.clone(),
393 ReferenceOr::Reference { reference } => spec.resolve_schema_ref(reference)?,
394 };
395 if let openapiv3::SchemaKind::Type(openapiv3::Type::Object(obj)) = &schema.schema_kind {
396 obj.required.first().cloned()
397 } else {
398 None
399 }
400 }
401
402 fn find_response_for_status(
404 responses: &Responses,
405 status_code: u16,
406 ) -> Option<&ReferenceOr<Response>> {
407 if let Some(response) = responses.responses.get(&openapiv3::StatusCode::Code(status_code)) {
409 return Some(response);
410 }
411
412 if let Some(default_response) = &responses.default {
414 return Some(default_response);
415 }
416
417 None
418 }
419
420 #[allow(dead_code)]
422 fn generate_from_response(
423 spec: &OpenApiSpec,
424 response: &Response,
425 content_type: Option<&str>,
426 expand_tokens: bool,
427 ) -> Result<Value> {
428 Self::generate_from_response_with_scenario(
429 spec,
430 response,
431 content_type,
432 expand_tokens,
433 None,
434 )
435 }
436
437 #[allow(dead_code)]
439 fn generate_from_response_with_scenario(
440 spec: &OpenApiSpec,
441 response: &Response,
442 content_type: Option<&str>,
443 expand_tokens: bool,
444 scenario: Option<&str>,
445 ) -> Result<Value> {
446 Self::generate_from_response_with_scenario_and_mode(
447 spec,
448 response,
449 content_type,
450 expand_tokens,
451 scenario,
452 None,
453 None,
454 )
455 }
456
457 fn generate_from_response_with_scenario_and_mode(
459 spec: &OpenApiSpec,
460 response: &Response,
461 content_type: Option<&str>,
462 expand_tokens: bool,
463 scenario: Option<&str>,
464 selection_mode: Option<crate::response_selection::ResponseSelectionMode>,
465 selector: Option<&crate::response_selection::ResponseSelector>,
466 ) -> Result<Value> {
467 Self::generate_from_response_with_scenario_and_mode_and_persona(
468 spec,
469 response,
470 content_type,
471 expand_tokens,
472 scenario,
473 selection_mode,
474 selector,
475 None, )
477 }
478
479 #[allow(clippy::too_many_arguments)]
481 #[allow(dead_code)]
482 fn generate_from_response_with_scenario_and_mode_and_persona(
483 spec: &OpenApiSpec,
484 response: &Response,
485 content_type: Option<&str>,
486 expand_tokens: bool,
487 scenario: Option<&str>,
488 selection_mode: Option<crate::response_selection::ResponseSelectionMode>,
489 selector: Option<&crate::response_selection::ResponseSelector>,
490 persona: Option<&Persona>,
491 ) -> Result<Value> {
492 if let Some(content_type) = content_type {
494 if let Some(media_type) = response.content.get(content_type) {
495 return Self::generate_from_media_type_with_scenario_and_mode_and_persona(
496 spec,
497 media_type,
498 expand_tokens,
499 scenario,
500 selection_mode,
501 selector,
502 persona,
503 );
504 }
505 }
506
507 let preferred_types = ["application/json", "application/xml", "text/plain"];
509
510 for content_type in &preferred_types {
511 if let Some(media_type) = response.content.get(*content_type) {
512 return Self::generate_from_media_type_with_scenario_and_mode_and_persona(
513 spec,
514 media_type,
515 expand_tokens,
516 scenario,
517 selection_mode,
518 selector,
519 persona,
520 );
521 }
522 }
523
524 if let Some((_, media_type)) = response.content.iter().next() {
526 return Self::generate_from_media_type_with_scenario_and_mode_and_persona(
527 spec,
528 media_type,
529 expand_tokens,
530 scenario,
531 selection_mode,
532 selector,
533 persona,
534 );
535 }
536
537 Ok(Value::Object(serde_json::Map::new()))
539 }
540
541 #[allow(dead_code)]
543 fn generate_from_media_type(
544 spec: &OpenApiSpec,
545 media_type: &openapiv3::MediaType,
546 expand_tokens: bool,
547 ) -> Result<Value> {
548 Self::generate_from_media_type_with_scenario(spec, media_type, expand_tokens, None)
549 }
550
551 #[allow(dead_code)]
553 fn generate_from_media_type_with_scenario(
554 spec: &OpenApiSpec,
555 media_type: &openapiv3::MediaType,
556 expand_tokens: bool,
557 scenario: Option<&str>,
558 ) -> Result<Value> {
559 Self::generate_from_media_type_with_scenario_and_mode(
560 spec,
561 media_type,
562 expand_tokens,
563 scenario,
564 None,
565 None,
566 )
567 }
568
569 #[allow(dead_code)]
571 fn generate_from_media_type_with_scenario_and_mode(
572 spec: &OpenApiSpec,
573 media_type: &openapiv3::MediaType,
574 expand_tokens: bool,
575 scenario: Option<&str>,
576 selection_mode: Option<crate::response_selection::ResponseSelectionMode>,
577 selector: Option<&crate::response_selection::ResponseSelector>,
578 ) -> Result<Value> {
579 Self::generate_from_media_type_with_scenario_and_mode_and_persona(
580 spec,
581 media_type,
582 expand_tokens,
583 scenario,
584 selection_mode,
585 selector,
586 None, )
588 }
589
590 fn generate_from_media_type_with_scenario_and_mode_and_persona(
592 spec: &OpenApiSpec,
593 media_type: &openapiv3::MediaType,
594 expand_tokens: bool,
595 scenario: Option<&str>,
596 selection_mode: Option<crate::response_selection::ResponseSelectionMode>,
597 selector: Option<&crate::response_selection::ResponseSelector>,
598 persona: Option<&Persona>,
599 ) -> Result<Value> {
600 if let Some(example) = &media_type.example {
604 tracing::debug!("Using explicit example from media type: {:?}", example);
605 if expand_tokens {
607 let expanded_example = Self::expand_templates(example);
608 return Ok(expanded_example);
609 } else {
610 return Ok(example.clone());
611 }
612 }
613
614 if !media_type.examples.is_empty() {
618 use crate::response_selection::{ResponseSelectionMode, ResponseSelector};
619
620 tracing::debug!(
621 "Found {} examples in media type, available examples: {:?}",
622 media_type.examples.len(),
623 media_type.examples.keys().collect::<Vec<_>>()
624 );
625
626 if let Some(scenario_name) = scenario {
628 if let Some(example_ref) = media_type.examples.get(scenario_name) {
629 tracing::debug!("Using scenario '{}' from examples map", scenario_name);
630 match Self::extract_example_value_with_persona(
631 spec,
632 example_ref,
633 expand_tokens,
634 persona,
635 media_type.schema.as_ref(),
636 ) {
637 Ok(value) => return Ok(value),
638 Err(e) => {
639 tracing::warn!(
640 "Failed to extract example for scenario '{}': {}, falling back",
641 scenario_name,
642 e
643 );
644 }
645 }
646 } else {
647 tracing::warn!(
648 "Scenario '{}' not found in examples, falling back based on selection mode",
649 scenario_name
650 );
651 }
652 }
653
654 let mode = selection_mode.unwrap_or(ResponseSelectionMode::First);
656
657 let example_names: Vec<String> = media_type.examples.keys().cloned().collect();
659
660 if example_names.is_empty() {
661 tracing::warn!("Examples map is empty, falling back to schema generation");
663 } else if mode == ResponseSelectionMode::Scenario && scenario.is_some() {
664 tracing::debug!("Scenario not found, using selection mode: {:?}", mode);
666 } else {
667 let selected_index = if let Some(sel) = selector {
669 sel.select(&example_names)
670 } else {
671 let temp_selector = ResponseSelector::new(mode);
673 temp_selector.select(&example_names)
674 };
675
676 if let Some(example_name) = example_names.get(selected_index) {
677 if let Some(example_ref) = media_type.examples.get(example_name) {
678 tracing::debug!(
679 "Using example '{}' from examples map (mode: {:?}, index: {})",
680 example_name,
681 mode,
682 selected_index
683 );
684 match Self::extract_example_value_with_persona(
685 spec,
686 example_ref,
687 expand_tokens,
688 persona,
689 media_type.schema.as_ref(),
690 ) {
691 Ok(value) => return Ok(value),
692 Err(e) => {
693 tracing::warn!(
694 "Failed to extract example '{}': {}, trying fallback",
695 example_name,
696 e
697 );
698 }
699 }
700 }
701 }
702 }
703
704 if let Some((example_name, example_ref)) = media_type.examples.iter().next() {
707 tracing::debug!(
708 "Using first example '{}' from examples map as fallback",
709 example_name
710 );
711 match Self::extract_example_value_with_persona(
712 spec,
713 example_ref,
714 expand_tokens,
715 persona,
716 media_type.schema.as_ref(),
717 ) {
718 Ok(value) => {
719 tracing::debug!(
720 "Successfully extracted fallback example '{}'",
721 example_name
722 );
723 return Ok(value);
724 }
725 Err(e) => {
726 tracing::error!(
727 "Failed to extract fallback example '{}': {}, falling back to schema generation",
728 example_name,
729 e
730 );
731 }
733 }
734 }
735 } else {
736 tracing::debug!("No examples found in media type, will use schema generation");
737 }
738
739 if let Some(schema_ref) = &media_type.schema {
742 Ok(Self::generate_example_from_schema_ref(spec, schema_ref, persona))
743 } else {
744 Ok(Value::Object(serde_json::Map::new()))
745 }
746 }
747
748 #[allow(dead_code)]
751 fn extract_example_value(
752 spec: &OpenApiSpec,
753 example_ref: &ReferenceOr<openapiv3::Example>,
754 expand_tokens: bool,
755 ) -> Result<Value> {
756 Self::extract_example_value_with_persona(spec, example_ref, expand_tokens, None, None)
757 }
758
759 fn extract_example_value_with_persona(
761 spec: &OpenApiSpec,
762 example_ref: &ReferenceOr<openapiv3::Example>,
763 expand_tokens: bool,
764 persona: Option<&Persona>,
765 schema_ref: Option<&ReferenceOr<Schema>>,
766 ) -> Result<Value> {
767 let mut value = match example_ref {
768 ReferenceOr::Item(example) => {
769 if let Some(v) = &example.value {
770 tracing::debug!("Using example from examples map: {:?}", v);
771 if expand_tokens {
772 Self::expand_templates(v)
773 } else {
774 v.clone()
775 }
776 } else {
777 return Ok(Value::Object(serde_json::Map::new()));
778 }
779 }
780 ReferenceOr::Reference { reference } => {
781 if let Some(example) = spec.get_example(reference) {
783 if let Some(v) = &example.value {
784 tracing::debug!("Using resolved example reference: {:?}", v);
785 if expand_tokens {
786 Self::expand_templates(v)
787 } else {
788 v.clone()
789 }
790 } else {
791 return Ok(Value::Object(serde_json::Map::new()));
792 }
793 } else {
794 tracing::warn!("Example reference '{}' not found", reference);
795 return Ok(Value::Object(serde_json::Map::new()));
796 }
797 }
798 };
799
800 value = Self::expand_example_items_if_needed(spec, value, persona, schema_ref);
802
803 Ok(value)
804 }
805
806 fn expand_example_items_if_needed(
809 _spec: &OpenApiSpec,
810 mut example: Value,
811 _persona: Option<&Persona>,
812 _schema_ref: Option<&ReferenceOr<Schema>>,
813 ) -> Value {
814 let has_nested_items = example
817 .get("data")
818 .and_then(|v| v.as_object())
819 .map(|obj| obj.contains_key("items"))
820 .unwrap_or(false);
821
822 let has_flat_items = example.get("items").is_some();
823
824 if !has_nested_items && !has_flat_items {
825 return example; }
827
828 let total = example
830 .get("data")
831 .and_then(|d| d.get("total"))
832 .or_else(|| example.get("total"))
833 .and_then(|v| v.as_u64().or_else(|| v.as_i64().map(|i| i as u64)));
834
835 let limit = example
836 .get("data")
837 .and_then(|d| d.get("limit"))
838 .or_else(|| example.get("limit"))
839 .and_then(|v| v.as_u64().or_else(|| v.as_i64().map(|i| i as u64)));
840
841 let items_array = example
843 .get("data")
844 .and_then(|d| d.get("items"))
845 .or_else(|| example.get("items"))
846 .and_then(|v| v.as_array())
847 .cloned();
848
849 if let (Some(total_val), Some(limit_val), Some(mut items)) = (total, limit, items_array) {
850 let current_count = items.len() as u64;
851 let expected_count = std::cmp::min(total_val, limit_val);
852 let max_items = 100; let expected_count = std::cmp::min(expected_count, max_items);
854
855 if current_count < expected_count && !items.is_empty() {
857 tracing::debug!(
858 "Expanding example items array: {} -> {} items (total={}, limit={})",
859 current_count,
860 expected_count,
861 total_val,
862 limit_val
863 );
864
865 let template = items[0].clone();
867 let additional_count = expected_count - current_count;
868
869 for i in 0..additional_count {
871 let mut new_item = template.clone();
872 let item_index = current_count + i + 1;
874 Self::add_item_variation(&mut new_item, item_index);
875 items.push(new_item);
876 }
877
878 if let Some(data_obj) = example.get_mut("data").and_then(|v| v.as_object_mut()) {
880 data_obj.insert("items".to_string(), Value::Array(items));
881 } else if let Some(root_obj) = example.as_object_mut() {
882 root_obj.insert("items".to_string(), Value::Array(items));
883 }
884 }
885 }
886
887 example
888 }
889
890 pub fn generate_from_examples(
892 response: &Response,
893 content_type: Option<&str>,
894 ) -> Result<Option<Value>> {
895 use openapiv3::ReferenceOr;
896
897 if let Some(content_type) = content_type {
899 if let Some(media_type) = response.content.get(content_type) {
900 if let Some(example) = &media_type.example {
902 return Ok(Some(example.clone()));
903 }
904
905 for (_, example_ref) in &media_type.examples {
907 if let ReferenceOr::Item(example) = example_ref {
908 if let Some(value) = &example.value {
909 return Ok(Some(value.clone()));
910 }
911 }
912 }
914 }
915 }
916
917 for (_, media_type) in &response.content {
919 if let Some(example) = &media_type.example {
921 return Ok(Some(example.clone()));
922 }
923
924 for (_, example_ref) in &media_type.examples {
926 if let ReferenceOr::Item(example) = example_ref {
927 if let Some(value) = &example.value {
928 return Ok(Some(value.clone()));
929 }
930 }
931 }
933 }
934
935 Ok(None)
936 }
937
938 fn expand_templates(value: &Value) -> Value {
940 match value {
941 Value::String(s) => {
942 let expanded = s
943 .replace("{{now}}", &chrono::Utc::now().to_rfc3339())
944 .replace("{{uuid}}", &uuid::Uuid::new_v4().to_string());
945 Value::String(expanded)
946 }
947 Value::Object(map) => {
948 let mut new_map = serde_json::Map::new();
949 for (key, val) in map {
950 new_map.insert(key.clone(), Self::expand_templates(val));
951 }
952 Value::Object(new_map)
953 }
954 Value::Array(arr) => {
955 let new_arr: Vec<Value> = arr.iter().map(Self::expand_templates).collect();
956 Value::Array(new_arr)
957 }
958 _ => value.clone(),
959 }
960 }
961}
962
963#[derive(Debug, Clone)]
965pub struct MockResponse {
966 pub status_code: u16,
968 pub headers: HashMap<String, String>,
970 pub body: Option<Value>,
972}
973
974impl MockResponse {
975 pub fn new(status_code: u16) -> Self {
977 Self {
978 status_code,
979 headers: HashMap::new(),
980 body: None,
981 }
982 }
983
984 pub fn with_header(mut self, name: String, value: String) -> Self {
986 self.headers.insert(name, value);
987 self
988 }
989
990 pub fn with_body(mut self, body: Value) -> Self {
992 self.body = Some(body);
993 self
994 }
995}
996
997#[derive(Debug, Clone)]
999pub struct OpenApiSecurityRequirement {
1000 pub scheme: String,
1002 pub scopes: Vec<String>,
1004}
1005
1006impl OpenApiSecurityRequirement {
1007 pub fn new(scheme: String, scopes: Vec<String>) -> Self {
1009 Self { scheme, scopes }
1010 }
1011}
1012
1013#[derive(Debug, Clone)]
1015pub struct OpenApiOperation {
1016 pub method: String,
1018 pub path: String,
1020 pub operation: Operation,
1022}
1023
1024impl OpenApiOperation {
1025 pub fn new(method: String, path: String, operation: Operation) -> Self {
1027 Self {
1028 method,
1029 path,
1030 operation,
1031 }
1032 }
1033}
1034
1035#[cfg(test)]
1036mod tests {
1037 use super::*;
1038 use openapiv3::ReferenceOr;
1039 use serde_json::json;
1040
1041 struct MockAiGenerator {
1043 response: Value,
1044 }
1045
1046 #[async_trait]
1047 impl AiGenerator for MockAiGenerator {
1048 async fn generate(&self, _prompt: &str, _config: &AiResponseConfig) -> Result<Value> {
1049 Ok(self.response.clone())
1050 }
1051 }
1052
1053 #[test]
1054 fn generates_example_using_referenced_schemas() {
1055 let yaml = r#"
1056openapi: 3.0.3
1057info:
1058 title: Test API
1059 version: "1.0.0"
1060paths:
1061 /apiaries:
1062 get:
1063 responses:
1064 '200':
1065 description: ok
1066 content:
1067 application/json:
1068 schema:
1069 $ref: '#/components/schemas/Apiary'
1070components:
1071 schemas:
1072 Apiary:
1073 type: object
1074 properties:
1075 id:
1076 type: string
1077 hive:
1078 $ref: '#/components/schemas/Hive'
1079 Hive:
1080 type: object
1081 properties:
1082 name:
1083 type: string
1084 active:
1085 type: boolean
1086 "#;
1087
1088 let spec = OpenApiSpec::from_string(yaml, Some("yaml")).expect("load spec");
1089 let path_item = spec
1090 .spec
1091 .paths
1092 .paths
1093 .get("/apiaries")
1094 .and_then(ReferenceOr::as_item)
1095 .expect("path item");
1096 let operation = path_item.get.as_ref().expect("GET operation");
1097
1098 let response =
1099 ResponseGenerator::generate_response(&spec, operation, 200, Some("application/json"))
1100 .expect("generate response");
1101
1102 let obj = response.as_object().expect("response object");
1103 assert!(obj.contains_key("id"));
1104 let hive = obj.get("hive").and_then(|value| value.as_object()).expect("hive object");
1105 assert!(hive.contains_key("name"));
1106 assert!(hive.contains_key("active"));
1107 }
1108
1109 #[test]
1114 fn inject_response_violations_off_by_default_leaves_required_fields() {
1115 let yaml = r#"
1116openapi: 3.0.3
1117info:
1118 title: t
1119 version: "1"
1120paths:
1121 /thing:
1122 get:
1123 responses:
1124 '200':
1125 description: ok
1126 content:
1127 application/json:
1128 schema:
1129 $ref: '#/components/schemas/Thing'
1130components:
1131 schemas:
1132 Thing:
1133 type: object
1134 required: [must_have]
1135 properties:
1136 must_have:
1137 type: string
1138 optional:
1139 type: string
1140 "#;
1141 let spec = OpenApiSpec::from_string(yaml, Some("yaml")).expect("load");
1142 let path_item = spec
1143 .spec
1144 .paths
1145 .paths
1146 .get("/thing")
1147 .and_then(ReferenceOr::as_item)
1148 .expect("path");
1149 let op = path_item.get.as_ref().expect("op");
1150 let body =
1151 ResponseGenerator::generate_response(&spec, op, 200, Some("application/json")).unwrap();
1152 assert!(body.as_object().unwrap().contains_key("must_have"));
1153 }
1154
1155 #[test]
1162 fn first_required_field_for_status_finds_required_in_referenced_schema() {
1163 let yaml = r#"
1164openapi: 3.0.3
1165info:
1166 title: t
1167 version: "1"
1168paths:
1169 /thing:
1170 get:
1171 responses:
1172 '200':
1173 description: ok
1174 content:
1175 application/json:
1176 schema:
1177 $ref: '#/components/schemas/Thing'
1178components:
1179 schemas:
1180 Thing:
1181 type: object
1182 required: [comment, location]
1183 properties:
1184 comment:
1185 type: string
1186 location:
1187 type: string
1188 "#;
1189 let spec = OpenApiSpec::from_string(yaml, Some("yaml")).expect("load");
1190 let path_item = spec
1191 .spec
1192 .paths
1193 .paths
1194 .get("/thing")
1195 .and_then(ReferenceOr::as_item)
1196 .expect("path");
1197 let op = path_item.get.as_ref().expect("op");
1198 let first = ResponseGenerator::first_required_field_for_status(&spec, op, 200);
1199 assert_eq!(first.as_deref(), Some("comment"), "first declared required field is picked");
1200 }
1201
1202 #[test]
1205 fn first_required_field_returns_none_when_no_required_or_non_2xx() {
1206 let yaml = r#"
1207openapi: 3.0.3
1208info:
1209 title: t
1210 version: "1"
1211paths:
1212 /thing:
1213 get:
1214 responses:
1215 '200':
1216 description: ok
1217 content:
1218 application/json:
1219 schema:
1220 type: object
1221 properties:
1222 whatever:
1223 type: string
1224 '500':
1225 description: err
1226 content:
1227 application/json:
1228 schema:
1229 type: object
1230 required: [code]
1231 properties:
1232 code:
1233 type: string
1234 "#;
1235 let spec = OpenApiSpec::from_string(yaml, Some("yaml")).expect("load");
1236 let path_item = spec
1237 .spec
1238 .paths
1239 .paths
1240 .get("/thing")
1241 .and_then(ReferenceOr::as_item)
1242 .expect("path");
1243 let op = path_item.get.as_ref().expect("op");
1244 assert!(ResponseGenerator::first_required_field_for_status(&spec, op, 200).is_none());
1246 assert_eq!(
1250 ResponseGenerator::first_required_field_for_status(&spec, op, 500).as_deref(),
1251 Some("code")
1252 );
1253 }
1254
1255 #[tokio::test]
1256 async fn test_generate_ai_response_with_generator() {
1257 let ai_config = AiResponseConfig {
1258 enabled: true,
1259 mode: mockforge_foundation::ai_response::AiResponseMode::Intelligent,
1260 prompt: Some("Generate a response for {{method}} {{path}}".to_string()),
1261 context: None,
1262 temperature: 0.7,
1263 max_tokens: 1000,
1264 schema: None,
1265 cache_enabled: true,
1266 };
1267 let context = RequestContext {
1268 method: "GET".to_string(),
1269 path: "/api/users".to_string(),
1270 path_params: HashMap::new(),
1271 query_params: HashMap::new(),
1272 headers: HashMap::new(),
1273 body: None,
1274 multipart_fields: HashMap::new(),
1275 multipart_files: HashMap::new(),
1276 };
1277 let mock_generator = MockAiGenerator {
1278 response: json!({"message": "Generated response"}),
1279 };
1280
1281 let result =
1282 ResponseGenerator::generate_ai_response(&ai_config, &context, Some(&mock_generator))
1283 .await;
1284
1285 assert!(result.is_ok());
1286 let value = result.unwrap();
1287 assert_eq!(value["message"], "Generated response");
1288 }
1289
1290 #[tokio::test]
1291 async fn test_generate_ai_response_without_generator() {
1292 let ai_config = AiResponseConfig {
1293 enabled: true,
1294 mode: mockforge_foundation::ai_response::AiResponseMode::Intelligent,
1295 prompt: Some("Generate a response for {{method}} {{path}}".to_string()),
1296 context: None,
1297 temperature: 0.7,
1298 max_tokens: 1000,
1299 schema: None,
1300 cache_enabled: true,
1301 };
1302 let context = RequestContext {
1303 method: "POST".to_string(),
1304 path: "/api/users".to_string(),
1305 path_params: HashMap::new(),
1306 query_params: HashMap::new(),
1307 headers: HashMap::new(),
1308 body: None,
1309 multipart_fields: HashMap::new(),
1310 multipart_files: HashMap::new(),
1311 };
1312
1313 let result = ResponseGenerator::generate_ai_response(&ai_config, &context, None).await;
1314
1315 assert!(result.is_err());
1317 let err = result.unwrap_err().to_string();
1318 assert!(
1319 err.contains("no AI generator configured"),
1320 "Expected 'no AI generator configured' error, got: {}",
1321 err
1322 );
1323 }
1324
1325 #[tokio::test]
1326 async fn test_generate_ai_response_no_prompt() {
1327 let ai_config = AiResponseConfig {
1328 enabled: true,
1329 mode: mockforge_foundation::ai_response::AiResponseMode::Intelligent,
1330 prompt: None,
1331 context: None,
1332 temperature: 0.7,
1333 max_tokens: 1000,
1334 schema: None,
1335 cache_enabled: true,
1336 };
1337 let context = RequestContext {
1338 method: "GET".to_string(),
1339 path: "/api/test".to_string(),
1340 path_params: HashMap::new(),
1341 query_params: HashMap::new(),
1342 headers: HashMap::new(),
1343 body: None,
1344 multipart_fields: HashMap::new(),
1345 multipart_files: HashMap::new(),
1346 };
1347
1348 let result = ResponseGenerator::generate_ai_response(&ai_config, &context, None).await;
1349
1350 assert!(result.is_err());
1351 }
1352
1353 #[test]
1354 fn test_generate_response_with_expansion() {
1355 let spec = OpenApiSpec::from_string(
1356 r#"openapi: 3.0.0
1357info:
1358 title: Test API
1359 version: 1.0.0
1360paths:
1361 /users:
1362 get:
1363 responses:
1364 '200':
1365 description: OK
1366 content:
1367 application/json:
1368 schema:
1369 type: object
1370 properties:
1371 id:
1372 type: integer
1373 name:
1374 type: string
1375"#,
1376 Some("yaml"),
1377 )
1378 .unwrap();
1379
1380 let operation = spec
1381 .spec
1382 .paths
1383 .paths
1384 .get("/users")
1385 .and_then(|p| p.as_item())
1386 .and_then(|p| p.get.as_ref())
1387 .unwrap();
1388
1389 let response = ResponseGenerator::generate_response_with_expansion(
1390 &spec,
1391 operation,
1392 200,
1393 Some("application/json"),
1394 true,
1395 )
1396 .unwrap();
1397
1398 assert!(response.is_object());
1399 }
1400
1401 #[test]
1402 fn test_generate_response_with_scenario() {
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 examples:
1417 happy:
1418 value:
1419 id: 1
1420 name: "Happy User"
1421 sad:
1422 value:
1423 id: 2
1424 name: "Sad User"
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 = ResponseGenerator::generate_response_with_scenario(
1440 &spec,
1441 operation,
1442 200,
1443 Some("application/json"),
1444 false,
1445 Some("happy"),
1446 )
1447 .unwrap();
1448
1449 assert_eq!(response["id"], 1);
1450 assert_eq!(response["name"], "Happy User");
1451 }
1452
1453 #[test]
1454 fn test_generate_response_with_referenced_response() {
1455 let spec = OpenApiSpec::from_string(
1456 r#"openapi: 3.0.0
1457info:
1458 title: Test API
1459 version: 1.0.0
1460paths:
1461 /users:
1462 get:
1463 responses:
1464 '200':
1465 $ref: '#/components/responses/UserResponse'
1466components:
1467 responses:
1468 UserResponse:
1469 description: User response
1470 content:
1471 application/json:
1472 schema:
1473 type: object
1474 properties:
1475 id:
1476 type: integer
1477 name:
1478 type: string
1479"#,
1480 Some("yaml"),
1481 )
1482 .unwrap();
1483
1484 let operation = spec
1485 .spec
1486 .paths
1487 .paths
1488 .get("/users")
1489 .and_then(|p| p.as_item())
1490 .and_then(|p| p.get.as_ref())
1491 .unwrap();
1492
1493 let response =
1494 ResponseGenerator::generate_response(&spec, operation, 200, Some("application/json"))
1495 .unwrap();
1496
1497 assert!(response.is_object());
1498 }
1499
1500 #[test]
1501 fn test_generate_response_with_default_status() {
1502 let spec = OpenApiSpec::from_string(
1503 r#"openapi: 3.0.0
1504info:
1505 title: Test API
1506 version: 1.0.0
1507paths:
1508 /users:
1509 get:
1510 responses:
1511 '200':
1512 description: OK
1513 default:
1514 description: Error
1515 content:
1516 application/json:
1517 schema:
1518 type: object
1519 properties:
1520 error:
1521 type: string
1522"#,
1523 Some("yaml"),
1524 )
1525 .unwrap();
1526
1527 let operation = spec
1528 .spec
1529 .paths
1530 .paths
1531 .get("/users")
1532 .and_then(|p| p.as_item())
1533 .and_then(|p| p.get.as_ref())
1534 .unwrap();
1535
1536 let response =
1538 ResponseGenerator::generate_response(&spec, operation, 500, Some("application/json"))
1539 .unwrap();
1540
1541 assert!(response.is_object());
1542 }
1543
1544 #[test]
1545 fn test_generate_response_with_example_in_media_type() {
1546 let spec = OpenApiSpec::from_string(
1547 r#"openapi: 3.0.0
1548info:
1549 title: Test API
1550 version: 1.0.0
1551paths:
1552 /users:
1553 get:
1554 responses:
1555 '200':
1556 description: OK
1557 content:
1558 application/json:
1559 example:
1560 id: 1
1561 name: "Example User"
1562"#,
1563 Some("yaml"),
1564 )
1565 .unwrap();
1566
1567 let operation = spec
1568 .spec
1569 .paths
1570 .paths
1571 .get("/users")
1572 .and_then(|p| p.as_item())
1573 .and_then(|p| p.get.as_ref())
1574 .unwrap();
1575
1576 let response =
1577 ResponseGenerator::generate_response(&spec, operation, 200, Some("application/json"))
1578 .unwrap();
1579
1580 assert_eq!(response["id"], 1);
1581 assert_eq!(response["name"], "Example User");
1582 }
1583
1584 #[test]
1585 fn test_generate_response_with_schema_example() {
1586 let spec = OpenApiSpec::from_string(
1587 r#"openapi: 3.0.0
1588info:
1589 title: Test API
1590 version: 1.0.0
1591paths:
1592 /users:
1593 get:
1594 responses:
1595 '200':
1596 description: OK
1597 content:
1598 application/json:
1599 schema:
1600 type: object
1601 example:
1602 id: 42
1603 name: "Schema Example"
1604 properties:
1605 id:
1606 type: integer
1607 name:
1608 type: string
1609"#,
1610 Some("yaml"),
1611 )
1612 .unwrap();
1613
1614 let operation = spec
1615 .spec
1616 .paths
1617 .paths
1618 .get("/users")
1619 .and_then(|p| p.as_item())
1620 .and_then(|p| p.get.as_ref())
1621 .unwrap();
1622
1623 let response =
1624 ResponseGenerator::generate_response(&spec, operation, 200, Some("application/json"))
1625 .unwrap();
1626
1627 assert!(response.is_object());
1629 }
1630
1631 #[test]
1632 fn test_generate_response_with_referenced_schema() {
1633 let spec = OpenApiSpec::from_string(
1634 r#"openapi: 3.0.0
1635info:
1636 title: Test API
1637 version: 1.0.0
1638paths:
1639 /users:
1640 get:
1641 responses:
1642 '200':
1643 description: OK
1644 content:
1645 application/json:
1646 schema:
1647 $ref: '#/components/schemas/User'
1648components:
1649 schemas:
1650 User:
1651 type: object
1652 properties:
1653 id:
1654 type: integer
1655 name:
1656 type: string
1657"#,
1658 Some("yaml"),
1659 )
1660 .unwrap();
1661
1662 let operation = spec
1663 .spec
1664 .paths
1665 .paths
1666 .get("/users")
1667 .and_then(|p| p.as_item())
1668 .and_then(|p| p.get.as_ref())
1669 .unwrap();
1670
1671 let response =
1672 ResponseGenerator::generate_response(&spec, operation, 200, Some("application/json"))
1673 .unwrap();
1674
1675 assert!(response.is_object());
1676 assert!(response.get("id").is_some());
1677 assert!(response.get("name").is_some());
1678 }
1679
1680 #[test]
1681 fn test_generate_response_with_array_schema() {
1682 let spec = OpenApiSpec::from_string(
1683 r#"openapi: 3.0.0
1684info:
1685 title: Test API
1686 version: 1.0.0
1687paths:
1688 /users:
1689 get:
1690 responses:
1691 '200':
1692 description: OK
1693 content:
1694 application/json:
1695 schema:
1696 type: array
1697 items:
1698 type: object
1699 properties:
1700 id:
1701 type: integer
1702 name:
1703 type: string
1704"#,
1705 Some("yaml"),
1706 )
1707 .unwrap();
1708
1709 let operation = spec
1710 .spec
1711 .paths
1712 .paths
1713 .get("/users")
1714 .and_then(|p| p.as_item())
1715 .and_then(|p| p.get.as_ref())
1716 .unwrap();
1717
1718 let response =
1719 ResponseGenerator::generate_response(&spec, operation, 200, Some("application/json"))
1720 .unwrap();
1721
1722 assert!(response.is_array());
1723 }
1724
1725 #[test]
1726 fn test_generate_response_with_different_content_types() {
1727 let spec = OpenApiSpec::from_string(
1728 r#"openapi: 3.0.0
1729info:
1730 title: Test API
1731 version: 1.0.0
1732paths:
1733 /users:
1734 get:
1735 responses:
1736 '200':
1737 description: OK
1738 content:
1739 application/json:
1740 schema:
1741 type: object
1742 text/plain:
1743 schema:
1744 type: string
1745"#,
1746 Some("yaml"),
1747 )
1748 .unwrap();
1749
1750 let operation = spec
1751 .spec
1752 .paths
1753 .paths
1754 .get("/users")
1755 .and_then(|p| p.as_item())
1756 .and_then(|p| p.get.as_ref())
1757 .unwrap();
1758
1759 let json_response =
1761 ResponseGenerator::generate_response(&spec, operation, 200, Some("application/json"))
1762 .unwrap();
1763 assert!(json_response.is_object());
1764
1765 let text_response =
1767 ResponseGenerator::generate_response(&spec, operation, 200, Some("text/plain"))
1768 .unwrap();
1769 assert!(text_response.is_string());
1770 }
1771
1772 #[test]
1773 fn test_generate_response_without_content_type() {
1774 let spec = OpenApiSpec::from_string(
1775 r#"openapi: 3.0.0
1776info:
1777 title: Test API
1778 version: 1.0.0
1779paths:
1780 /users:
1781 get:
1782 responses:
1783 '200':
1784 description: OK
1785 content:
1786 application/json:
1787 schema:
1788 type: object
1789 properties:
1790 id:
1791 type: integer
1792"#,
1793 Some("yaml"),
1794 )
1795 .unwrap();
1796
1797 let operation = spec
1798 .spec
1799 .paths
1800 .paths
1801 .get("/users")
1802 .and_then(|p| p.as_item())
1803 .and_then(|p| p.get.as_ref())
1804 .unwrap();
1805
1806 let response = ResponseGenerator::generate_response(&spec, operation, 200, None).unwrap();
1808
1809 assert!(response.is_object());
1810 }
1811
1812 #[test]
1813 fn test_generate_response_with_no_content() {
1814 let spec = OpenApiSpec::from_string(
1815 r#"openapi: 3.0.0
1816info:
1817 title: Test API
1818 version: 1.0.0
1819paths:
1820 /users:
1821 delete:
1822 responses:
1823 '204':
1824 description: No Content
1825"#,
1826 Some("yaml"),
1827 )
1828 .unwrap();
1829
1830 let operation = spec
1831 .spec
1832 .paths
1833 .paths
1834 .get("/users")
1835 .and_then(|p| p.as_item())
1836 .and_then(|p| p.delete.as_ref())
1837 .unwrap();
1838
1839 let response = ResponseGenerator::generate_response(&spec, operation, 204, None).unwrap();
1840
1841 assert!(response.is_object());
1843 assert!(response.as_object().unwrap().is_empty());
1844 }
1845
1846 #[test]
1847 fn test_generate_response_with_expansion_disabled() {
1848 let spec = OpenApiSpec::from_string(
1849 r#"openapi: 3.0.0
1850info:
1851 title: Test API
1852 version: 1.0.0
1853paths:
1854 /users:
1855 get:
1856 responses:
1857 '200':
1858 description: OK
1859 content:
1860 application/json:
1861 schema:
1862 type: object
1863 properties:
1864 id:
1865 type: integer
1866 name:
1867 type: string
1868"#,
1869 Some("yaml"),
1870 )
1871 .unwrap();
1872
1873 let operation = spec
1874 .spec
1875 .paths
1876 .paths
1877 .get("/users")
1878 .and_then(|p| p.as_item())
1879 .and_then(|p| p.get.as_ref())
1880 .unwrap();
1881
1882 let response = ResponseGenerator::generate_response_with_expansion(
1883 &spec,
1884 operation,
1885 200,
1886 Some("application/json"),
1887 false, )
1889 .unwrap();
1890
1891 assert!(response.is_object());
1892 }
1893
1894 #[test]
1895 fn test_generate_response_with_array_schema_referenced_items() {
1896 let spec = OpenApiSpec::from_string(
1898 r#"openapi: 3.0.0
1899info:
1900 title: Test API
1901 version: 1.0.0
1902paths:
1903 /items:
1904 get:
1905 responses:
1906 '200':
1907 description: OK
1908 content:
1909 application/json:
1910 schema:
1911 type: array
1912 items:
1913 $ref: '#/components/schemas/Item'
1914components:
1915 schemas:
1916 Item:
1917 type: object
1918 properties:
1919 id:
1920 type: string
1921 name:
1922 type: string
1923"#,
1924 Some("yaml"),
1925 )
1926 .unwrap();
1927
1928 let operation = spec
1929 .spec
1930 .paths
1931 .paths
1932 .get("/items")
1933 .and_then(|p| p.as_item())
1934 .and_then(|p| p.get.as_ref())
1935 .unwrap();
1936
1937 let response =
1938 ResponseGenerator::generate_response(&spec, operation, 200, Some("application/json"))
1939 .unwrap();
1940
1941 let arr = response.as_array().expect("response should be array");
1943 assert!(!arr.is_empty());
1944 if let Some(item) = arr.first() {
1945 let obj = item.as_object().expect("item should be object");
1946 assert!(obj.contains_key("id") || obj.contains_key("name"));
1947 }
1948 }
1949
1950 #[test]
1951 fn test_generate_response_with_array_schema_missing_reference() {
1952 let spec = OpenApiSpec::from_string(
1954 r#"openapi: 3.0.0
1955info:
1956 title: Test API
1957 version: 1.0.0
1958paths:
1959 /items:
1960 get:
1961 responses:
1962 '200':
1963 description: OK
1964 content:
1965 application/json:
1966 schema:
1967 type: array
1968 items:
1969 $ref: '#/components/schemas/NonExistentItem'
1970components:
1971 schemas: {}
1972"#,
1973 Some("yaml"),
1974 )
1975 .unwrap();
1976
1977 let operation = spec
1978 .spec
1979 .paths
1980 .paths
1981 .get("/items")
1982 .and_then(|p| p.as_item())
1983 .and_then(|p| p.get.as_ref())
1984 .unwrap();
1985
1986 let response =
1987 ResponseGenerator::generate_response(&spec, operation, 200, Some("application/json"))
1988 .unwrap();
1989
1990 let arr = response.as_array().expect("response should be array");
1992 assert!(!arr.is_empty());
1993 }
1994
1995 #[test]
1996 fn test_generate_response_with_array_example_and_pagination() {
1997 let spec = OpenApiSpec::from_string(
1999 r#"openapi: 3.0.0
2000info:
2001 title: Test API
2002 version: 1.0.0
2003paths:
2004 /products:
2005 get:
2006 responses:
2007 '200':
2008 description: OK
2009 content:
2010 application/json:
2011 schema:
2012 type: array
2013 example: [{"id": 1, "name": "Product 1"}]
2014 items:
2015 type: object
2016 properties:
2017 id:
2018 type: integer
2019 name:
2020 type: string
2021"#,
2022 Some("yaml"),
2023 )
2024 .unwrap();
2025
2026 let operation = spec
2027 .spec
2028 .paths
2029 .paths
2030 .get("/products")
2031 .and_then(|p| p.as_item())
2032 .and_then(|p| p.get.as_ref())
2033 .unwrap();
2034
2035 let response =
2036 ResponseGenerator::generate_response(&spec, operation, 200, Some("application/json"))
2037 .unwrap();
2038
2039 let arr = response.as_array().expect("response should be array");
2041 assert!(!arr.is_empty());
2042 if let Some(item) = arr.first() {
2043 let obj = item.as_object().expect("item should be object");
2044 assert!(obj.contains_key("id") || obj.contains_key("name"));
2045 }
2046 }
2047
2048 #[test]
2049 fn test_generate_response_with_missing_response_reference() {
2050 let spec = OpenApiSpec::from_string(
2052 r#"openapi: 3.0.0
2053info:
2054 title: Test API
2055 version: 1.0.0
2056paths:
2057 /users:
2058 get:
2059 responses:
2060 '200':
2061 $ref: '#/components/responses/NonExistentResponse'
2062components:
2063 responses: {}
2064"#,
2065 Some("yaml"),
2066 )
2067 .unwrap();
2068
2069 let operation = spec
2070 .spec
2071 .paths
2072 .paths
2073 .get("/users")
2074 .and_then(|p| p.as_item())
2075 .and_then(|p| p.get.as_ref())
2076 .unwrap();
2077
2078 let response =
2079 ResponseGenerator::generate_response(&spec, operation, 200, Some("application/json"))
2080 .unwrap();
2081
2082 assert!(response.is_object());
2084 assert!(response.as_object().unwrap().is_empty());
2085 }
2086
2087 #[test]
2088 fn test_generate_response_with_no_response_for_status() {
2089 let spec = OpenApiSpec::from_string(
2091 r#"openapi: 3.0.0
2092info:
2093 title: Test API
2094 version: 1.0.0
2095paths:
2096 /users:
2097 get:
2098 responses:
2099 '404':
2100 description: Not found
2101"#,
2102 Some("yaml"),
2103 )
2104 .unwrap();
2105
2106 let operation = spec
2107 .spec
2108 .paths
2109 .paths
2110 .get("/users")
2111 .and_then(|p| p.as_item())
2112 .and_then(|p| p.get.as_ref())
2113 .unwrap();
2114
2115 let response =
2117 ResponseGenerator::generate_response(&spec, operation, 200, Some("application/json"))
2118 .unwrap();
2119
2120 assert!(response.is_object());
2122 assert!(response.as_object().unwrap().is_empty());
2123 }
2124}