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