1use crate::intelligent_behavior::config::Persona;
7use crate::{
8 ai_response::{AiResponseConfig, RequestContext},
9 OpenApiSpec, Result,
10};
11use async_trait::async_trait;
12use chrono;
13use openapiv3::{Operation, ReferenceOr, Response, Responses, Schema};
14use rand::{thread_rng, Rng};
15use serde_json::Value;
16use std::collections::HashMap;
17use uuid;
18
19#[async_trait]
24pub trait AiGenerator: Send + Sync {
25 async fn generate(&self, prompt: &str, config: &AiResponseConfig) -> Result<Value>;
34}
35
36pub struct ResponseGenerator;
38
39impl ResponseGenerator {
40 pub async fn generate_ai_response(
53 ai_config: &AiResponseConfig,
54 context: &RequestContext,
55 generator: Option<&dyn AiGenerator>,
56 ) -> Result<Value> {
57 let prompt_template = ai_config
59 .prompt
60 .as_ref()
61 .ok_or_else(|| crate::Error::generic("AI prompt is required"))?;
62
63 let expanded_prompt = prompt_template
67 .replace("{{method}}", &context.method)
68 .replace("{{path}}", &context.path);
69
70 tracing::info!("AI response generation requested with prompt: {}", expanded_prompt);
71
72 if let Some(gen) = generator {
74 tracing::debug!("Using provided AI generator for response");
75 return gen.generate(&expanded_prompt, ai_config).await;
76 }
77
78 tracing::warn!(
80 "No AI generator provided; configure MOCKFORGE_AI_PROVIDER to enable AI responses"
81 );
82 Err(crate::Error::generic(
83 "AI response generation is not available: no AI generator configured. \
84 Set MOCKFORGE_AI_PROVIDER and MOCKFORGE_AI_API_KEY environment variables to enable AI-assisted responses.",
85 ))
86 }
87
88 pub fn generate_response(
90 spec: &OpenApiSpec,
91 operation: &Operation,
92 status_code: u16,
93 content_type: Option<&str>,
94 ) -> Result<Value> {
95 Self::generate_response_with_expansion(spec, operation, status_code, content_type, true)
96 }
97
98 pub fn generate_response_with_expansion(
100 spec: &OpenApiSpec,
101 operation: &Operation,
102 status_code: u16,
103 content_type: Option<&str>,
104 expand_tokens: bool,
105 ) -> Result<Value> {
106 Self::generate_response_with_expansion_and_mode(
107 spec,
108 operation,
109 status_code,
110 content_type,
111 expand_tokens,
112 None,
113 None,
114 )
115 }
116
117 pub fn generate_response_with_expansion_and_mode(
119 spec: &OpenApiSpec,
120 operation: &Operation,
121 status_code: u16,
122 content_type: Option<&str>,
123 expand_tokens: bool,
124 selection_mode: Option<crate::openapi::response_selection::ResponseSelectionMode>,
125 selector: Option<&crate::openapi::response_selection::ResponseSelector>,
126 ) -> Result<Value> {
127 Self::generate_response_with_expansion_and_mode_and_persona(
128 spec,
129 operation,
130 status_code,
131 content_type,
132 expand_tokens,
133 selection_mode,
134 selector,
135 None, )
137 }
138
139 #[allow(clippy::too_many_arguments)]
141 pub fn generate_response_with_expansion_and_mode_and_persona(
142 spec: &OpenApiSpec,
143 operation: &Operation,
144 status_code: u16,
145 content_type: Option<&str>,
146 expand_tokens: bool,
147 selection_mode: Option<crate::openapi::response_selection::ResponseSelectionMode>,
148 selector: Option<&crate::openapi::response_selection::ResponseSelector>,
149 persona: Option<&Persona>,
150 ) -> Result<Value> {
151 Self::generate_response_with_scenario_and_mode_and_persona(
152 spec,
153 operation,
154 status_code,
155 content_type,
156 expand_tokens,
157 None, selection_mode,
159 selector,
160 persona,
161 )
162 }
163
164 pub fn generate_response_with_scenario(
190 spec: &OpenApiSpec,
191 operation: &Operation,
192 status_code: u16,
193 content_type: Option<&str>,
194 expand_tokens: bool,
195 scenario: Option<&str>,
196 ) -> Result<Value> {
197 Self::generate_response_with_scenario_and_mode(
198 spec,
199 operation,
200 status_code,
201 content_type,
202 expand_tokens,
203 scenario,
204 None,
205 None,
206 )
207 }
208
209 #[allow(clippy::too_many_arguments)]
211 pub fn generate_response_with_scenario_and_mode(
212 spec: &OpenApiSpec,
213 operation: &Operation,
214 status_code: u16,
215 content_type: Option<&str>,
216 expand_tokens: bool,
217 scenario: Option<&str>,
218 selection_mode: Option<crate::openapi::response_selection::ResponseSelectionMode>,
219 selector: Option<&crate::openapi::response_selection::ResponseSelector>,
220 ) -> Result<Value> {
221 Self::generate_response_with_scenario_and_mode_and_persona(
222 spec,
223 operation,
224 status_code,
225 content_type,
226 expand_tokens,
227 scenario,
228 selection_mode,
229 selector,
230 None, )
232 }
233
234 #[allow(clippy::too_many_arguments)]
236 pub fn generate_response_with_scenario_and_mode_and_persona(
237 spec: &OpenApiSpec,
238 operation: &Operation,
239 status_code: u16,
240 content_type: Option<&str>,
241 expand_tokens: bool,
242 scenario: Option<&str>,
243 selection_mode: Option<crate::openapi::response_selection::ResponseSelectionMode>,
244 selector: Option<&crate::openapi::response_selection::ResponseSelector>,
245 _persona: Option<&Persona>,
246 ) -> Result<Value> {
247 let response = Self::find_response_for_status(&operation.responses, status_code);
249
250 tracing::debug!(
251 "Finding response for status code {}: {:?}",
252 status_code,
253 if response.is_some() {
254 "found"
255 } else {
256 "not found"
257 }
258 );
259
260 match response {
261 Some(response_ref) => {
262 match response_ref {
263 ReferenceOr::Item(response) => {
264 tracing::debug!(
265 "Using direct response item with {} content types",
266 response.content.len()
267 );
268 Self::generate_from_response_with_scenario_and_mode(
269 spec,
270 response,
271 content_type,
272 expand_tokens,
273 scenario,
274 selection_mode,
275 selector,
276 )
277 }
278 ReferenceOr::Reference { reference } => {
279 tracing::debug!("Resolving response reference: {}", reference);
280 if let Some(resolved_response) = spec.get_response(reference) {
282 tracing::debug!(
283 "Resolved response reference with {} content types",
284 resolved_response.content.len()
285 );
286 Self::generate_from_response_with_scenario_and_mode(
287 spec,
288 resolved_response,
289 content_type,
290 expand_tokens,
291 scenario,
292 selection_mode,
293 selector,
294 )
295 } else {
296 tracing::warn!("Response reference '{}' not found in spec", reference);
297 Ok(Value::Object(serde_json::Map::new()))
299 }
300 }
301 }
302 }
303 None => {
304 tracing::warn!(
305 "No response found for status code {} in operation. Available status codes: {:?}",
306 status_code,
307 operation.responses.responses.keys().collect::<Vec<_>>()
308 );
309 Ok(Value::Object(serde_json::Map::new()))
311 }
312 }
313 }
314
315 fn find_response_for_status(
317 responses: &Responses,
318 status_code: u16,
319 ) -> Option<&ReferenceOr<Response>> {
320 if let Some(response) = responses.responses.get(&openapiv3::StatusCode::Code(status_code)) {
322 return Some(response);
323 }
324
325 if let Some(default_response) = &responses.default {
327 return Some(default_response);
328 }
329
330 None
331 }
332
333 #[allow(dead_code)]
335 fn generate_from_response(
336 spec: &OpenApiSpec,
337 response: &Response,
338 content_type: Option<&str>,
339 expand_tokens: bool,
340 ) -> Result<Value> {
341 Self::generate_from_response_with_scenario(
342 spec,
343 response,
344 content_type,
345 expand_tokens,
346 None,
347 )
348 }
349
350 #[allow(dead_code)]
352 fn generate_from_response_with_scenario(
353 spec: &OpenApiSpec,
354 response: &Response,
355 content_type: Option<&str>,
356 expand_tokens: bool,
357 scenario: Option<&str>,
358 ) -> Result<Value> {
359 Self::generate_from_response_with_scenario_and_mode(
360 spec,
361 response,
362 content_type,
363 expand_tokens,
364 scenario,
365 None,
366 None,
367 )
368 }
369
370 fn generate_from_response_with_scenario_and_mode(
372 spec: &OpenApiSpec,
373 response: &Response,
374 content_type: Option<&str>,
375 expand_tokens: bool,
376 scenario: Option<&str>,
377 selection_mode: Option<crate::openapi::response_selection::ResponseSelectionMode>,
378 selector: Option<&crate::openapi::response_selection::ResponseSelector>,
379 ) -> Result<Value> {
380 Self::generate_from_response_with_scenario_and_mode_and_persona(
381 spec,
382 response,
383 content_type,
384 expand_tokens,
385 scenario,
386 selection_mode,
387 selector,
388 None, )
390 }
391
392 #[allow(clippy::too_many_arguments)]
394 #[allow(dead_code)]
395 fn generate_from_response_with_scenario_and_mode_and_persona(
396 spec: &OpenApiSpec,
397 response: &Response,
398 content_type: Option<&str>,
399 expand_tokens: bool,
400 scenario: Option<&str>,
401 selection_mode: Option<crate::openapi::response_selection::ResponseSelectionMode>,
402 selector: Option<&crate::openapi::response_selection::ResponseSelector>,
403 persona: Option<&Persona>,
404 ) -> Result<Value> {
405 if let Some(content_type) = content_type {
407 if let Some(media_type) = response.content.get(content_type) {
408 return Self::generate_from_media_type_with_scenario_and_mode_and_persona(
409 spec,
410 media_type,
411 expand_tokens,
412 scenario,
413 selection_mode,
414 selector,
415 persona,
416 );
417 }
418 }
419
420 let preferred_types = ["application/json", "application/xml", "text/plain"];
422
423 for content_type in &preferred_types {
424 if let Some(media_type) = response.content.get(*content_type) {
425 return Self::generate_from_media_type_with_scenario_and_mode_and_persona(
426 spec,
427 media_type,
428 expand_tokens,
429 scenario,
430 selection_mode,
431 selector,
432 persona,
433 );
434 }
435 }
436
437 if let Some((_, media_type)) = response.content.iter().next() {
439 return Self::generate_from_media_type_with_scenario_and_mode_and_persona(
440 spec,
441 media_type,
442 expand_tokens,
443 scenario,
444 selection_mode,
445 selector,
446 persona,
447 );
448 }
449
450 Ok(Value::Object(serde_json::Map::new()))
452 }
453
454 #[allow(dead_code)]
456 fn generate_from_media_type(
457 spec: &OpenApiSpec,
458 media_type: &openapiv3::MediaType,
459 expand_tokens: bool,
460 ) -> Result<Value> {
461 Self::generate_from_media_type_with_scenario(spec, media_type, expand_tokens, None)
462 }
463
464 #[allow(dead_code)]
466 fn generate_from_media_type_with_scenario(
467 spec: &OpenApiSpec,
468 media_type: &openapiv3::MediaType,
469 expand_tokens: bool,
470 scenario: Option<&str>,
471 ) -> Result<Value> {
472 Self::generate_from_media_type_with_scenario_and_mode(
473 spec,
474 media_type,
475 expand_tokens,
476 scenario,
477 None,
478 None,
479 )
480 }
481
482 #[allow(dead_code)]
484 fn generate_from_media_type_with_scenario_and_mode(
485 spec: &OpenApiSpec,
486 media_type: &openapiv3::MediaType,
487 expand_tokens: bool,
488 scenario: Option<&str>,
489 selection_mode: Option<crate::openapi::response_selection::ResponseSelectionMode>,
490 selector: Option<&crate::openapi::response_selection::ResponseSelector>,
491 ) -> Result<Value> {
492 Self::generate_from_media_type_with_scenario_and_mode_and_persona(
493 spec,
494 media_type,
495 expand_tokens,
496 scenario,
497 selection_mode,
498 selector,
499 None, )
501 }
502
503 fn generate_from_media_type_with_scenario_and_mode_and_persona(
505 spec: &OpenApiSpec,
506 media_type: &openapiv3::MediaType,
507 expand_tokens: bool,
508 scenario: Option<&str>,
509 selection_mode: Option<crate::openapi::response_selection::ResponseSelectionMode>,
510 selector: Option<&crate::openapi::response_selection::ResponseSelector>,
511 persona: Option<&Persona>,
512 ) -> Result<Value> {
513 if let Some(example) = &media_type.example {
517 tracing::debug!("Using explicit example from media type: {:?}", example);
518 if expand_tokens {
520 let expanded_example = Self::expand_templates(example);
521 return Ok(expanded_example);
522 } else {
523 return Ok(example.clone());
524 }
525 }
526
527 if !media_type.examples.is_empty() {
531 use crate::openapi::response_selection::{ResponseSelectionMode, ResponseSelector};
532
533 tracing::debug!(
534 "Found {} examples in media type, available examples: {:?}",
535 media_type.examples.len(),
536 media_type.examples.keys().collect::<Vec<_>>()
537 );
538
539 if let Some(scenario_name) = scenario {
541 if let Some(example_ref) = media_type.examples.get(scenario_name) {
542 tracing::debug!("Using scenario '{}' from examples map", scenario_name);
543 match Self::extract_example_value_with_persona(
544 spec,
545 example_ref,
546 expand_tokens,
547 persona,
548 media_type.schema.as_ref(),
549 ) {
550 Ok(value) => return Ok(value),
551 Err(e) => {
552 tracing::warn!(
553 "Failed to extract example for scenario '{}': {}, falling back",
554 scenario_name,
555 e
556 );
557 }
558 }
559 } else {
560 tracing::warn!(
561 "Scenario '{}' not found in examples, falling back based on selection mode",
562 scenario_name
563 );
564 }
565 }
566
567 let mode = selection_mode.unwrap_or(ResponseSelectionMode::First);
569
570 let example_names: Vec<String> = media_type.examples.keys().cloned().collect();
572
573 if example_names.is_empty() {
574 tracing::warn!("Examples map is empty, falling back to schema generation");
576 } else if mode == ResponseSelectionMode::Scenario && scenario.is_some() {
577 tracing::debug!("Scenario not found, using selection mode: {:?}", mode);
579 } else {
580 let selected_index = if let Some(sel) = selector {
582 sel.select(&example_names)
583 } else {
584 let temp_selector = ResponseSelector::new(mode);
586 temp_selector.select(&example_names)
587 };
588
589 if let Some(example_name) = example_names.get(selected_index) {
590 if let Some(example_ref) = media_type.examples.get(example_name) {
591 tracing::debug!(
592 "Using example '{}' from examples map (mode: {:?}, index: {})",
593 example_name,
594 mode,
595 selected_index
596 );
597 match Self::extract_example_value_with_persona(
598 spec,
599 example_ref,
600 expand_tokens,
601 persona,
602 media_type.schema.as_ref(),
603 ) {
604 Ok(value) => return Ok(value),
605 Err(e) => {
606 tracing::warn!(
607 "Failed to extract example '{}': {}, trying fallback",
608 example_name,
609 e
610 );
611 }
612 }
613 }
614 }
615 }
616
617 if let Some((example_name, example_ref)) = media_type.examples.iter().next() {
620 tracing::debug!(
621 "Using first example '{}' from examples map as fallback",
622 example_name
623 );
624 match Self::extract_example_value_with_persona(
625 spec,
626 example_ref,
627 expand_tokens,
628 persona,
629 media_type.schema.as_ref(),
630 ) {
631 Ok(value) => {
632 tracing::debug!(
633 "Successfully extracted fallback example '{}'",
634 example_name
635 );
636 return Ok(value);
637 }
638 Err(e) => {
639 tracing::error!(
640 "Failed to extract fallback example '{}': {}, falling back to schema generation",
641 example_name,
642 e
643 );
644 }
646 }
647 }
648 } else {
649 tracing::debug!("No examples found in media type, will use schema generation");
650 }
651
652 if let Some(schema_ref) = &media_type.schema {
655 Ok(Self::generate_example_from_schema_ref(spec, schema_ref, persona))
656 } else {
657 Ok(Value::Object(serde_json::Map::new()))
658 }
659 }
660
661 #[allow(dead_code)]
664 fn extract_example_value(
665 spec: &OpenApiSpec,
666 example_ref: &ReferenceOr<openapiv3::Example>,
667 expand_tokens: bool,
668 ) -> Result<Value> {
669 Self::extract_example_value_with_persona(spec, example_ref, expand_tokens, None, None)
670 }
671
672 fn extract_example_value_with_persona(
674 spec: &OpenApiSpec,
675 example_ref: &ReferenceOr<openapiv3::Example>,
676 expand_tokens: bool,
677 persona: Option<&Persona>,
678 schema_ref: Option<&ReferenceOr<Schema>>,
679 ) -> Result<Value> {
680 let mut value = match example_ref {
681 ReferenceOr::Item(example) => {
682 if let Some(v) = &example.value {
683 tracing::debug!("Using example from examples map: {:?}", v);
684 if expand_tokens {
685 Self::expand_templates(v)
686 } else {
687 v.clone()
688 }
689 } else {
690 return Ok(Value::Object(serde_json::Map::new()));
691 }
692 }
693 ReferenceOr::Reference { reference } => {
694 if let Some(example) = spec.get_example(reference) {
696 if let Some(v) = &example.value {
697 tracing::debug!("Using resolved example reference: {:?}", v);
698 if expand_tokens {
699 Self::expand_templates(v)
700 } else {
701 v.clone()
702 }
703 } else {
704 return Ok(Value::Object(serde_json::Map::new()));
705 }
706 } else {
707 tracing::warn!("Example reference '{}' not found", reference);
708 return Ok(Value::Object(serde_json::Map::new()));
709 }
710 }
711 };
712
713 value = Self::expand_example_items_if_needed(spec, value, persona, schema_ref);
715
716 Ok(value)
717 }
718
719 fn expand_example_items_if_needed(
722 _spec: &OpenApiSpec,
723 mut example: Value,
724 _persona: Option<&Persona>,
725 _schema_ref: Option<&ReferenceOr<Schema>>,
726 ) -> Value {
727 let has_nested_items = example
730 .get("data")
731 .and_then(|v| v.as_object())
732 .map(|obj| obj.contains_key("items"))
733 .unwrap_or(false);
734
735 let has_flat_items = example.get("items").is_some();
736
737 if !has_nested_items && !has_flat_items {
738 return example; }
740
741 let total = example
743 .get("data")
744 .and_then(|d| d.get("total"))
745 .or_else(|| example.get("total"))
746 .and_then(|v| v.as_u64().or_else(|| v.as_i64().map(|i| i as u64)));
747
748 let limit = example
749 .get("data")
750 .and_then(|d| d.get("limit"))
751 .or_else(|| example.get("limit"))
752 .and_then(|v| v.as_u64().or_else(|| v.as_i64().map(|i| i as u64)));
753
754 let items_array = example
756 .get("data")
757 .and_then(|d| d.get("items"))
758 .or_else(|| example.get("items"))
759 .and_then(|v| v.as_array())
760 .cloned();
761
762 if let (Some(total_val), Some(limit_val), Some(mut items)) = (total, limit, items_array) {
763 let current_count = items.len() as u64;
764 let expected_count = std::cmp::min(total_val, limit_val);
765 let max_items = 100; let expected_count = std::cmp::min(expected_count, max_items);
767
768 if current_count < expected_count && !items.is_empty() {
770 tracing::debug!(
771 "Expanding example items array: {} -> {} items (total={}, limit={})",
772 current_count,
773 expected_count,
774 total_val,
775 limit_val
776 );
777
778 let template = items[0].clone();
780 let additional_count = expected_count - current_count;
781
782 for i in 0..additional_count {
784 let mut new_item = template.clone();
785 let item_index = current_count + i + 1;
787 Self::add_item_variation(&mut new_item, item_index);
788 items.push(new_item);
789 }
790
791 if let Some(data_obj) = example.get_mut("data").and_then(|v| v.as_object_mut()) {
793 data_obj.insert("items".to_string(), Value::Array(items));
794 } else if let Some(root_obj) = example.as_object_mut() {
795 root_obj.insert("items".to_string(), Value::Array(items));
796 }
797 }
798 }
799
800 example
801 }
802
803 fn generate_example_from_schema_ref(
804 spec: &OpenApiSpec,
805 schema_ref: &ReferenceOr<Schema>,
806 persona: Option<&Persona>,
807 ) -> Value {
808 match schema_ref {
809 ReferenceOr::Item(schema) => Self::generate_example_from_schema(spec, schema, persona),
810 ReferenceOr::Reference { reference } => spec
811 .get_schema(reference)
812 .map(|schema| Self::generate_example_from_schema(spec, &schema.schema, persona))
813 .unwrap_or_else(|| Value::Object(serde_json::Map::new())),
814 }
815 }
816
817 fn generate_example_from_schema(
825 spec: &OpenApiSpec,
826 schema: &Schema,
827 persona: Option<&Persona>,
828 ) -> Value {
829 if let Some(example) = schema.schema_data.example.as_ref() {
832 tracing::debug!("Using schema-level example: {:?}", example);
833 return example.clone();
834 }
835
836 match &schema.schema_kind {
840 openapiv3::SchemaKind::Type(openapiv3::Type::String(_)) => {
841 Value::String("example string".to_string())
843 }
844 openapiv3::SchemaKind::Type(openapiv3::Type::Integer(_)) => Value::Number(42.into()),
845 openapiv3::SchemaKind::Type(openapiv3::Type::Number(_)) => Value::Number(
846 serde_json::Number::from_f64(std::f64::consts::PI)
847 .expect("PI is a valid f64 value"),
848 ),
849 openapiv3::SchemaKind::Type(openapiv3::Type::Boolean(_)) => Value::Bool(true),
850 openapiv3::SchemaKind::Type(openapiv3::Type::Object(obj)) => {
851 let mut pagination_metadata: Option<(u64, u64, u64)> = None; let has_items =
858 obj.properties.iter().any(|(name, _)| name.to_lowercase() == "items");
859
860 if has_items {
861 let mut total_opt = None;
863 let mut page_opt = None;
864 let mut limit_opt = None;
865
866 for (prop_name, prop_schema) in &obj.properties {
867 let prop_lower = prop_name.to_lowercase();
868 let schema_ref: ReferenceOr<Schema> = match prop_schema {
870 ReferenceOr::Item(boxed) => ReferenceOr::Item(boxed.as_ref().clone()),
871 ReferenceOr::Reference { reference } => ReferenceOr::Reference {
872 reference: reference.clone(),
873 },
874 };
875 if prop_lower == "total" || prop_lower == "count" || prop_lower == "size" {
876 total_opt = Self::extract_numeric_value_from_schema(&schema_ref);
877 } else if prop_lower == "page" {
878 page_opt = Self::extract_numeric_value_from_schema(&schema_ref);
879 } else if prop_lower == "limit" || prop_lower == "per_page" {
880 limit_opt = Self::extract_numeric_value_from_schema(&schema_ref);
881 }
882 }
883
884 if let Some(total) = total_opt {
886 let page = page_opt.unwrap_or(1);
887 let limit = limit_opt.unwrap_or(20);
888 pagination_metadata = Some((total, page, limit));
889 tracing::debug!(
890 "Detected pagination metadata: total={}, page={}, limit={}",
891 total,
892 page,
893 limit
894 );
895 } else {
896 if obj.properties.contains_key("items") {
899 if let Some(inferred_total) =
903 Self::try_infer_total_from_context(spec, obj)
904 {
905 let page = page_opt.unwrap_or(1);
906 let limit = limit_opt.unwrap_or(20);
907 pagination_metadata = Some((inferred_total, page, limit));
908 tracing::debug!(
909 "Inferred pagination metadata from parent entity: total={}, page={}, limit={}",
910 inferred_total, page, limit
911 );
912 } else {
913 if let Some(persona) = persona {
915 let count_keys =
918 ["hive_count", "apiary_count", "item_count", "total_count"];
919 for key in &count_keys {
920 if let Some(count) = persona.get_numeric_trait(key) {
921 let page = page_opt.unwrap_or(1);
922 let limit = limit_opt.unwrap_or(20);
923 pagination_metadata = Some((count, page, limit));
924 tracing::debug!(
925 "Using persona trait '{}' for pagination: total={}, page={}, limit={}",
926 key, count, page, limit
927 );
928 break;
929 }
930 }
931 }
932 }
933 }
934 }
935 }
936
937 let mut map = serde_json::Map::new();
938 for (prop_name, prop_schema) in &obj.properties {
939 let prop_lower = prop_name.to_lowercase();
940
941 let is_items_array = prop_lower == "items" && pagination_metadata.is_some();
943
944 let value = match prop_schema {
945 ReferenceOr::Item(prop_schema) => {
946 if is_items_array {
949 Self::generate_array_with_count(
951 spec,
952 prop_schema.as_ref(),
953 pagination_metadata.unwrap(),
954 persona,
955 )
956 } else if let Some(prop_example) =
957 prop_schema.schema_data.example.as_ref()
958 {
959 tracing::debug!(
961 "Using example for property '{}': {:?}",
962 prop_name,
963 prop_example
964 );
965 prop_example.clone()
966 } else {
967 Self::generate_example_from_schema(
968 spec,
969 prop_schema.as_ref(),
970 persona,
971 )
972 }
973 }
974 ReferenceOr::Reference { reference } => {
975 if let Some(resolved_schema) = spec.get_schema(reference) {
977 if is_items_array {
979 Self::generate_array_with_count(
981 spec,
982 &resolved_schema.schema,
983 pagination_metadata.unwrap(),
984 persona,
985 )
986 } else if let Some(ref_example) =
987 resolved_schema.schema.schema_data.example.as_ref()
988 {
989 tracing::debug!(
991 "Using example from referenced schema '{}': {:?}",
992 reference,
993 ref_example
994 );
995 ref_example.clone()
996 } else {
997 Self::generate_example_from_schema(
998 spec,
999 &resolved_schema.schema,
1000 persona,
1001 )
1002 }
1003 } else {
1004 Self::generate_example_for_property(prop_name)
1005 }
1006 }
1007 };
1008 let value = match value {
1009 Value::Null => Self::generate_example_for_property(prop_name),
1010 Value::Object(ref obj) if obj.is_empty() => {
1011 Self::generate_example_for_property(prop_name)
1012 }
1013 _ => value,
1014 };
1015 map.insert(prop_name.clone(), value);
1016 }
1017
1018 if let Some((total, page, limit)) = pagination_metadata {
1020 map.insert("total".to_string(), Value::Number(total.into()));
1021 map.insert("page".to_string(), Value::Number(page.into()));
1022 map.insert("limit".to_string(), Value::Number(limit.into()));
1023 }
1024
1025 Value::Object(map)
1026 }
1027 openapiv3::SchemaKind::Type(openapiv3::Type::Array(arr)) => {
1028 match &arr.items {
1034 Some(item_schema) => {
1035 let example_item = match item_schema {
1036 ReferenceOr::Item(item_schema) => {
1037 Self::generate_example_from_schema(
1040 spec,
1041 item_schema.as_ref(),
1042 persona,
1043 )
1044 }
1045 ReferenceOr::Reference { reference } => {
1046 if let Some(resolved_schema) = spec.get_schema(reference) {
1049 Self::generate_example_from_schema(
1050 spec,
1051 &resolved_schema.schema,
1052 persona,
1053 )
1054 } else {
1055 Value::Object(serde_json::Map::new())
1056 }
1057 }
1058 };
1059 Value::Array(vec![example_item])
1060 }
1061 None => Value::Array(vec![Value::String("item".to_string())]),
1062 }
1063 }
1064 _ => Value::Object(serde_json::Map::new()),
1065 }
1066 }
1067
1068 fn extract_numeric_value_from_schema(schema_ref: &ReferenceOr<Schema>) -> Option<u64> {
1071 match schema_ref {
1072 ReferenceOr::Item(schema) => {
1073 if let Some(example) = schema.schema_data.example.as_ref() {
1075 if let Some(num) = example.as_u64() {
1076 return Some(num);
1077 } else if let Some(num) = example.as_f64() {
1078 return Some(num as u64);
1079 }
1080 }
1081 if let Some(default) = schema.schema_data.default.as_ref() {
1083 if let Some(num) = default.as_u64() {
1084 return Some(num);
1085 } else if let Some(num) = default.as_f64() {
1086 return Some(num as u64);
1087 }
1088 }
1089 None
1093 }
1094 ReferenceOr::Reference { reference: _ } => {
1095 None
1098 }
1099 }
1100 }
1101
1102 fn generate_array_with_count(
1105 spec: &OpenApiSpec,
1106 array_schema: &Schema,
1107 pagination: (u64, u64, u64), persona: Option<&Persona>,
1109 ) -> Value {
1110 let (total, _page, limit) = pagination;
1111
1112 let count = std::cmp::min(total, limit);
1115
1116 let max_items = 100;
1118 let count = std::cmp::min(count, max_items);
1119
1120 tracing::debug!("Generating array with count={} (total={}, limit={})", count, total, limit);
1121
1122 if let Some(example) = array_schema.schema_data.example.as_ref() {
1124 if let Some(example_array) = example.as_array() {
1125 if !example_array.is_empty() {
1126 let template_item = &example_array[0];
1128 let items: Vec<Value> = (0..count)
1129 .map(|i| {
1130 let mut item = template_item.clone();
1132 Self::add_item_variation(&mut item, i + 1);
1133 item
1134 })
1135 .collect();
1136 return Value::Array(items);
1137 }
1138 }
1139 }
1140
1141 if let openapiv3::SchemaKind::Type(openapiv3::Type::Array(arr)) = &array_schema.schema_kind
1143 {
1144 if let Some(item_schema) = &arr.items {
1145 let items: Vec<Value> = match item_schema {
1146 ReferenceOr::Item(item_schema) => {
1147 (0..count)
1148 .map(|i| {
1149 let mut item = Self::generate_example_from_schema(
1150 spec,
1151 item_schema.as_ref(),
1152 persona,
1153 );
1154 Self::add_item_variation(&mut item, i + 1);
1156 item
1157 })
1158 .collect()
1159 }
1160 ReferenceOr::Reference { reference } => {
1161 if let Some(resolved_schema) = spec.get_schema(reference) {
1162 (0..count)
1163 .map(|i| {
1164 let mut item = Self::generate_example_from_schema(
1165 spec,
1166 &resolved_schema.schema,
1167 persona,
1168 );
1169 Self::add_item_variation(&mut item, i + 1);
1171 item
1172 })
1173 .collect()
1174 } else {
1175 vec![Value::Object(serde_json::Map::new()); count as usize]
1176 }
1177 }
1178 };
1179 return Value::Array(items);
1180 }
1181 }
1182
1183 Value::Array((0..count).map(|i| Value::String(format!("item_{}", i + 1))).collect())
1185 }
1186
1187 fn add_item_variation(item: &mut Value, item_index: u64) {
1190 if let Some(obj) = item.as_object_mut() {
1191 if let Some(id_val) = obj.get_mut("id") {
1193 if let Some(id_str) = id_val.as_str() {
1194 let base_id = id_str.split('_').next().unwrap_or(id_str);
1196 *id_val = Value::String(format!("{}_{:03}", base_id, item_index));
1197 } else if let Some(id_num) = id_val.as_u64() {
1198 *id_val = Value::Number((id_num + item_index).into());
1199 }
1200 }
1201
1202 if let Some(name_val) = obj.get_mut("name") {
1204 if let Some(name_str) = name_val.as_str() {
1205 if name_str.contains('#') {
1206 *name_val = Value::String(format!("Hive #{}", item_index));
1208 } else {
1209 let apiary_names = [
1212 "Meadow Apiary",
1214 "Prairie Apiary",
1215 "Sunset Valley Apiary",
1216 "Golden Fields Apiary",
1217 "Miller Family Apiary",
1218 "Heartland Honey Co.",
1219 "Cornfield Apiary",
1220 "Harvest Moon Apiary",
1221 "Prairie Winds Apiary",
1222 "Amber Fields Apiary",
1223 "Coastal Apiary",
1225 "Sunset Coast Apiary",
1226 "Pacific Grove Apiary",
1227 "Golden Gate Apiary",
1228 "Napa Valley Apiary",
1229 "Coastal Breeze Apiary",
1230 "Pacific Heights Apiary",
1231 "Bay Area Apiary",
1232 "Sunset Valley Honey Co.",
1233 "Coastal Harvest Apiary",
1234 "Lone Star Apiary",
1236 "Texas Ranch Apiary",
1237 "Big Sky Apiary",
1238 "Prairie Rose Apiary",
1239 "Hill Country Apiary",
1240 "Lone Star Honey Co.",
1241 "Texas Pride Apiary",
1242 "Wildflower Ranch",
1243 "Desert Bloom Apiary",
1244 "Cactus Creek Apiary",
1245 "Orange Grove Apiary",
1247 "Citrus Grove Apiary",
1248 "Palm Grove Apiary",
1249 "Tropical Breeze Apiary",
1250 "Everglades Apiary",
1251 "Sunshine State Apiary",
1252 "Florida Keys Apiary",
1253 "Grove View Apiary",
1254 "Tropical Harvest Apiary",
1255 "Palm Coast Apiary",
1256 "Mountain View Apiary",
1258 "Valley Apiary",
1259 "Riverside Apiary",
1260 "Hilltop Apiary",
1261 "Forest Apiary",
1262 "Mountain Apiary",
1263 "Lakeside Apiary",
1264 "Ridge Apiary",
1265 "Brook Apiary",
1266 "Hillside Apiary",
1267 "Field Apiary",
1269 "Creek Apiary",
1270 "Woodland Apiary",
1271 "Farm Apiary",
1272 "Orchard Apiary",
1273 "Pasture Apiary",
1274 "Green Valley Apiary",
1275 "Blue Sky Apiary",
1276 "Sweet Honey Apiary",
1277 "Nature's Best Apiary",
1278 "Premium Honey Co.",
1280 "Artisan Apiary",
1281 "Heritage Apiary",
1282 "Summit Apiary",
1283 "Crystal Springs Apiary",
1284 "Maple Grove Apiary",
1285 "Wildflower Apiary",
1286 "Thistle Apiary",
1287 "Clover Field Apiary",
1288 "Honeycomb Apiary",
1289 ];
1290 let name_index = (item_index - 1) as usize % apiary_names.len();
1291 *name_val = Value::String(apiary_names[name_index].to_string());
1292 }
1293 }
1294 }
1295
1296 if let Some(location_val) = obj.get_mut("location") {
1298 if let Some(location_obj) = location_val.as_object_mut() {
1299 if let Some(address_val) = location_obj.get_mut("address") {
1301 if let Some(address_str) = address_val.as_str() {
1302 if let Some(num_str) = address_str.split_whitespace().next() {
1304 if let Ok(num) = num_str.parse::<u64>() {
1305 *address_val =
1306 Value::String(format!("{} Farm Road", num + item_index));
1307 } else {
1308 *address_val =
1309 Value::String(format!("{} Farm Road", 100 + item_index));
1310 }
1311 } else {
1312 *address_val =
1313 Value::String(format!("{} Farm Road", 100 + item_index));
1314 }
1315 }
1316 }
1317
1318 if let Some(lat_val) = location_obj.get_mut("latitude") {
1320 if let Some(lat) = lat_val.as_f64() {
1321 *lat_val = Value::Number(
1322 serde_json::Number::from_f64(lat + (item_index as f64 * 0.01))
1323 .expect("latitude arithmetic produces valid f64"),
1324 );
1325 }
1326 }
1327 if let Some(lng_val) = location_obj.get_mut("longitude") {
1328 if let Some(lng) = lng_val.as_f64() {
1329 *lng_val = Value::Number(
1330 serde_json::Number::from_f64(lng + (item_index as f64 * 0.01))
1331 .expect("longitude arithmetic produces valid f64"),
1332 );
1333 }
1334 }
1335 } else if let Some(address_str) = location_val.as_str() {
1336 if let Some(num_str) = address_str.split_whitespace().next() {
1338 if let Ok(num) = num_str.parse::<u64>() {
1339 *location_val =
1340 Value::String(format!("{} Farm Road", num + item_index));
1341 } else {
1342 *location_val =
1343 Value::String(format!("{} Farm Road", 100 + item_index));
1344 }
1345 }
1346 }
1347 }
1348
1349 if let Some(address_val) = obj.get_mut("address") {
1351 if let Some(address_str) = address_val.as_str() {
1352 if let Some(num_str) = address_str.split_whitespace().next() {
1353 if let Ok(num) = num_str.parse::<u64>() {
1354 *address_val = Value::String(format!("{} Farm Road", num + item_index));
1355 } else {
1356 *address_val = Value::String(format!("{} Farm Road", 100 + item_index));
1357 }
1358 }
1359 }
1360 }
1361
1362 if let Some(status_val) = obj.get_mut("status") {
1364 if status_val.as_str().is_some() {
1365 let statuses = [
1366 "healthy",
1367 "sick",
1368 "needs_attention",
1369 "quarantined",
1370 "active",
1371 "inactive",
1372 ];
1373 let status_index = (item_index - 1) as usize % statuses.len();
1374 let final_status = if (item_index - 1) % 10 < 7 {
1376 statuses[0] } else {
1378 statuses[status_index]
1379 };
1380 *status_val = Value::String(final_status.to_string());
1381 }
1382 }
1383
1384 if let Some(hive_type_val) = obj.get_mut("hive_type") {
1386 if hive_type_val.as_str().is_some() {
1387 let hive_types = ["langstroth", "top_bar", "warre", "flow_hive", "national"];
1388 let type_index = (item_index - 1) as usize % hive_types.len();
1389 *hive_type_val = Value::String(hive_types[type_index].to_string());
1390 }
1391 }
1392
1393 if let Some(queen_val) = obj.get_mut("queen") {
1395 if let Some(queen_obj) = queen_val.as_object_mut() {
1396 if let Some(breed_val) = queen_obj.get_mut("breed") {
1397 if breed_val.as_str().is_some() {
1398 let breeds =
1399 ["italian", "carniolan", "russian", "buckfast", "caucasian"];
1400 let breed_index = (item_index - 1) as usize % breeds.len();
1401 *breed_val = Value::String(breeds[breed_index].to_string());
1402 }
1403 }
1404 if let Some(age_val) = queen_obj.get_mut("age_days") {
1406 if let Some(base_age) = age_val.as_u64() {
1407 *age_val = Value::Number((base_age + (item_index * 10) % 200).into());
1408 } else if let Some(base_age) = age_val.as_i64() {
1409 *age_val =
1410 Value::Number((base_age + (item_index as i64 * 10) % 200).into());
1411 }
1412 }
1413 if let Some(color_val) = queen_obj.get_mut("mark_color") {
1415 if color_val.as_str().is_some() {
1416 let colors = ["yellow", "white", "red", "green", "blue"];
1417 let color_index = (item_index - 1) as usize % colors.len();
1418 *color_val = Value::String(colors[color_index].to_string());
1419 }
1420 }
1421 }
1422 }
1423
1424 if let Some(desc_val) = obj.get_mut("description") {
1426 if desc_val.as_str().is_some() {
1427 let descriptions = [
1428 "Production apiary",
1429 "Research apiary",
1430 "Commercial operation",
1431 "Backyard apiary",
1432 "Educational apiary",
1433 ];
1434 let desc_index = (item_index - 1) as usize % descriptions.len();
1435 *desc_val = Value::String(descriptions[desc_index].to_string());
1436 }
1437 }
1438
1439 let timestamp_fields = [
1442 "created_at",
1443 "updated_at",
1444 "timestamp",
1445 "date",
1446 "forecastDate",
1447 "predictedDate",
1448 ];
1449 for field_name in ×tamp_fields {
1450 if let Some(timestamp_val) = obj.get_mut(*field_name) {
1451 if let Some(_timestamp_str) = timestamp_val.as_str() {
1452 let months_ago = 12 + ((item_index - 1) % 6); let days_offset = (item_index - 1) % 28; let hours_offset = ((item_index * 7) % 24) as u8; let minutes_offset = ((item_index * 11) % 60) as u8; let base_year = 2024;
1462 let base_month = 11;
1463
1464 let target_year = if months_ago >= base_month as u64 {
1466 base_year - 1
1467 } else {
1468 base_year
1469 };
1470 let target_month = if months_ago >= base_month as u64 {
1471 12 - (months_ago - base_month as u64) as u8
1472 } else {
1473 (base_month as u64 - months_ago) as u8
1474 };
1475 let target_day = std::cmp::min(28, 1 + days_offset as u8); let timestamp = format!(
1479 "{:04}-{:02}-{:02}T{:02}:{:02}:00Z",
1480 target_year, target_month, target_day, hours_offset, minutes_offset
1481 );
1482 *timestamp_val = Value::String(timestamp);
1483 }
1484 }
1485 }
1486 }
1487 }
1488
1489 fn try_infer_total_from_context(
1492 spec: &OpenApiSpec,
1493 obj_type: &openapiv3::ObjectType,
1494 ) -> Option<u64> {
1495 if let Some(_items_schema_ref) = obj_type.properties.get("items") {
1497 if let Some(components) = &spec.spec.components {
1500 let schemas = &components.schemas;
1501 for (schema_name, schema_ref) in schemas {
1504 if let ReferenceOr::Item(schema) = schema_ref {
1505 if let openapiv3::SchemaKind::Type(openapiv3::Type::Object(obj)) =
1506 &schema.schema_kind
1507 {
1508 for (prop_name, prop_schema) in &obj.properties {
1510 let prop_lower = prop_name.to_lowercase();
1511 if prop_lower.ends_with("_count") {
1512 let schema_ref: ReferenceOr<Schema> = match prop_schema {
1514 ReferenceOr::Item(boxed) => {
1515 ReferenceOr::Item(boxed.as_ref().clone())
1516 }
1517 ReferenceOr::Reference { reference } => {
1518 ReferenceOr::Reference {
1519 reference: reference.clone(),
1520 }
1521 }
1522 };
1523 if let Some(count) =
1525 Self::extract_numeric_value_from_schema(&schema_ref)
1526 {
1527 if count > 0 && count <= 1000 {
1529 tracing::debug!(
1530 "Inferred count {} from parent schema {} field {}",
1531 count,
1532 schema_name,
1533 prop_name
1534 );
1535 return Some(count);
1536 }
1537 }
1538 }
1539 }
1540 }
1541 }
1542 }
1543 }
1544 }
1545
1546 None
1547 }
1548
1549 #[allow(dead_code)]
1552 fn infer_count_from_parent_schema(
1553 spec: &OpenApiSpec,
1554 parent_entity_name: &str,
1555 child_entity_name: &str,
1556 ) -> Option<u64> {
1557 let _parent_schema_name = parent_entity_name.to_string();
1559 let count_field_name = format!("{}_count", child_entity_name);
1560
1561 if let Some(components) = &spec.spec.components {
1563 let schemas = &components.schemas;
1564 for (schema_name, schema_ref) in schemas {
1566 let schema_name_lower = schema_name.to_lowercase();
1567 if schema_name_lower.contains(&parent_entity_name.to_lowercase()) {
1568 if let ReferenceOr::Item(schema) = schema_ref {
1569 if let openapiv3::SchemaKind::Type(openapiv3::Type::Object(obj)) =
1571 &schema.schema_kind
1572 {
1573 for (prop_name, prop_schema) in &obj.properties {
1574 if prop_name.to_lowercase() == count_field_name.to_lowercase() {
1575 let schema_ref: ReferenceOr<Schema> = match prop_schema {
1577 ReferenceOr::Item(boxed) => {
1578 ReferenceOr::Item(boxed.as_ref().clone())
1579 }
1580 ReferenceOr::Reference { reference } => {
1581 ReferenceOr::Reference {
1582 reference: reference.clone(),
1583 }
1584 }
1585 };
1586 return Self::extract_numeric_value_from_schema(&schema_ref);
1588 }
1589 }
1590 }
1591 }
1592 }
1593 }
1594 }
1595
1596 None
1597 }
1598
1599 fn generate_example_for_property(prop_name: &str) -> Value {
1601 let prop_lower = prop_name.to_lowercase();
1602
1603 if prop_lower.contains("id") || prop_lower.contains("uuid") {
1605 Value::String(uuid::Uuid::new_v4().to_string())
1606 } else if prop_lower.contains("email") {
1607 Value::String(format!("user{}@example.com", thread_rng().random_range(1000..=9999)))
1608 } else if prop_lower.contains("name") || prop_lower.contains("title") {
1609 let names = ["John Doe", "Jane Smith", "Bob Johnson", "Alice Brown"];
1610 Value::String(names[thread_rng().random_range(0..names.len())].to_string())
1611 } else if prop_lower.contains("phone") || prop_lower.contains("mobile") {
1612 Value::String(format!("+1-555-{:04}", thread_rng().random_range(1000..=9999)))
1613 } else if prop_lower.contains("address") || prop_lower.contains("street") {
1614 let streets = ["123 Main St", "456 Oak Ave", "789 Pine Rd", "321 Elm St"];
1615 Value::String(streets[thread_rng().random_range(0..streets.len())].to_string())
1616 } else if prop_lower.contains("city") {
1617 let cities = ["New York", "London", "Tokyo", "Paris", "Sydney"];
1618 Value::String(cities[thread_rng().random_range(0..cities.len())].to_string())
1619 } else if prop_lower.contains("country") {
1620 let countries = ["USA", "UK", "Japan", "France", "Australia"];
1621 Value::String(countries[thread_rng().random_range(0..countries.len())].to_string())
1622 } else if prop_lower.contains("company") || prop_lower.contains("organization") {
1623 let companies = ["Acme Corp", "Tech Solutions", "Global Inc", "Innovate Ltd"];
1624 Value::String(companies[thread_rng().random_range(0..companies.len())].to_string())
1625 } else if prop_lower.contains("url") || prop_lower.contains("website") {
1626 Value::String("https://example.com".to_string())
1627 } else if prop_lower.contains("age") {
1628 Value::Number((18 + thread_rng().random_range(0..60)).into())
1629 } else if prop_lower.contains("count") || prop_lower.contains("quantity") {
1630 Value::Number((1 + thread_rng().random_range(0..100)).into())
1631 } else if prop_lower.contains("price")
1632 || prop_lower.contains("amount")
1633 || prop_lower.contains("cost")
1634 {
1635 Value::Number(
1636 serde_json::Number::from_f64(
1637 (thread_rng().random::<f64>() * 1000.0 * 100.0).round() / 100.0,
1638 )
1639 .expect("rounded price calculation produces valid f64"),
1640 )
1641 } else if prop_lower.contains("active")
1642 || prop_lower.contains("enabled")
1643 || prop_lower.contains("is_")
1644 {
1645 Value::Bool(thread_rng().random_bool(0.5))
1646 } else if prop_lower.contains("date") || prop_lower.contains("time") {
1647 Value::String(chrono::Utc::now().to_rfc3339())
1648 } else if prop_lower.contains("description") || prop_lower.contains("comment") {
1649 Value::String("This is a sample description text.".to_string())
1650 } else {
1651 Value::String(format!("example {}", prop_name))
1652 }
1653 }
1654
1655 pub fn generate_from_examples(
1657 response: &Response,
1658 content_type: Option<&str>,
1659 ) -> Result<Option<Value>> {
1660 use openapiv3::ReferenceOr;
1661
1662 if let Some(content_type) = content_type {
1664 if let Some(media_type) = response.content.get(content_type) {
1665 if let Some(example) = &media_type.example {
1667 return Ok(Some(example.clone()));
1668 }
1669
1670 for (_, example_ref) in &media_type.examples {
1672 if let ReferenceOr::Item(example) = example_ref {
1673 if let Some(value) = &example.value {
1674 return Ok(Some(value.clone()));
1675 }
1676 }
1677 }
1679 }
1680 }
1681
1682 for (_, media_type) in &response.content {
1684 if let Some(example) = &media_type.example {
1686 return Ok(Some(example.clone()));
1687 }
1688
1689 for (_, example_ref) in &media_type.examples {
1691 if let ReferenceOr::Item(example) = example_ref {
1692 if let Some(value) = &example.value {
1693 return Ok(Some(value.clone()));
1694 }
1695 }
1696 }
1698 }
1699
1700 Ok(None)
1701 }
1702
1703 fn expand_templates(value: &Value) -> Value {
1705 match value {
1706 Value::String(s) => {
1707 let expanded = s
1708 .replace("{{now}}", &chrono::Utc::now().to_rfc3339())
1709 .replace("{{uuid}}", &uuid::Uuid::new_v4().to_string());
1710 Value::String(expanded)
1711 }
1712 Value::Object(map) => {
1713 let mut new_map = serde_json::Map::new();
1714 for (key, val) in map {
1715 new_map.insert(key.clone(), Self::expand_templates(val));
1716 }
1717 Value::Object(new_map)
1718 }
1719 Value::Array(arr) => {
1720 let new_arr: Vec<Value> = arr.iter().map(Self::expand_templates).collect();
1721 Value::Array(new_arr)
1722 }
1723 _ => value.clone(),
1724 }
1725 }
1726}
1727
1728#[cfg(test)]
1729mod tests {
1730 use super::*;
1731 use openapiv3::ReferenceOr;
1732 use serde_json::json;
1733
1734 struct MockAiGenerator {
1736 response: Value,
1737 }
1738
1739 #[async_trait]
1740 impl AiGenerator for MockAiGenerator {
1741 async fn generate(&self, _prompt: &str, _config: &AiResponseConfig) -> Result<Value> {
1742 Ok(self.response.clone())
1743 }
1744 }
1745
1746 #[test]
1747 fn generates_example_using_referenced_schemas() {
1748 let yaml = r#"
1749openapi: 3.0.3
1750info:
1751 title: Test API
1752 version: "1.0.0"
1753paths:
1754 /apiaries:
1755 get:
1756 responses:
1757 '200':
1758 description: ok
1759 content:
1760 application/json:
1761 schema:
1762 $ref: '#/components/schemas/Apiary'
1763components:
1764 schemas:
1765 Apiary:
1766 type: object
1767 properties:
1768 id:
1769 type: string
1770 hive:
1771 $ref: '#/components/schemas/Hive'
1772 Hive:
1773 type: object
1774 properties:
1775 name:
1776 type: string
1777 active:
1778 type: boolean
1779 "#;
1780
1781 let spec = OpenApiSpec::from_string(yaml, Some("yaml")).expect("load spec");
1782 let path_item = spec
1783 .spec
1784 .paths
1785 .paths
1786 .get("/apiaries")
1787 .and_then(ReferenceOr::as_item)
1788 .expect("path item");
1789 let operation = path_item.get.as_ref().expect("GET operation");
1790
1791 let response =
1792 ResponseGenerator::generate_response(&spec, operation, 200, Some("application/json"))
1793 .expect("generate response");
1794
1795 let obj = response.as_object().expect("response object");
1796 assert!(obj.contains_key("id"));
1797 let hive = obj.get("hive").and_then(|value| value.as_object()).expect("hive object");
1798 assert!(hive.contains_key("name"));
1799 assert!(hive.contains_key("active"));
1800 }
1801
1802 #[tokio::test]
1803 async fn test_generate_ai_response_with_generator() {
1804 let ai_config = AiResponseConfig {
1805 enabled: true,
1806 mode: crate::ai_response::AiResponseMode::Intelligent,
1807 prompt: Some("Generate a response for {{method}} {{path}}".to_string()),
1808 context: None,
1809 temperature: 0.7,
1810 max_tokens: 1000,
1811 schema: None,
1812 cache_enabled: true,
1813 };
1814 let context = RequestContext {
1815 method: "GET".to_string(),
1816 path: "/api/users".to_string(),
1817 path_params: HashMap::new(),
1818 query_params: HashMap::new(),
1819 headers: HashMap::new(),
1820 body: None,
1821 multipart_fields: HashMap::new(),
1822 multipart_files: HashMap::new(),
1823 };
1824 let mock_generator = MockAiGenerator {
1825 response: json!({"message": "Generated response"}),
1826 };
1827
1828 let result =
1829 ResponseGenerator::generate_ai_response(&ai_config, &context, Some(&mock_generator))
1830 .await;
1831
1832 assert!(result.is_ok());
1833 let value = result.unwrap();
1834 assert_eq!(value["message"], "Generated response");
1835 }
1836
1837 #[tokio::test]
1838 async fn test_generate_ai_response_without_generator() {
1839 let ai_config = AiResponseConfig {
1840 enabled: true,
1841 mode: crate::ai_response::AiResponseMode::Intelligent,
1842 prompt: Some("Generate a response for {{method}} {{path}}".to_string()),
1843 context: None,
1844 temperature: 0.7,
1845 max_tokens: 1000,
1846 schema: None,
1847 cache_enabled: true,
1848 };
1849 let context = RequestContext {
1850 method: "POST".to_string(),
1851 path: "/api/users".to_string(),
1852 path_params: HashMap::new(),
1853 query_params: HashMap::new(),
1854 headers: HashMap::new(),
1855 body: None,
1856 multipart_fields: HashMap::new(),
1857 multipart_files: HashMap::new(),
1858 };
1859
1860 let result = ResponseGenerator::generate_ai_response(&ai_config, &context, None).await;
1861
1862 assert!(result.is_ok());
1863 let value = result.unwrap();
1864 assert_eq!(value["ai_response"], "AI generation placeholder");
1865 assert!(value["expanded_prompt"].as_str().unwrap().contains("POST"));
1866 assert!(value["expanded_prompt"].as_str().unwrap().contains("/api/users"));
1867 }
1868
1869 #[tokio::test]
1870 async fn test_generate_ai_response_no_prompt() {
1871 let ai_config = AiResponseConfig {
1872 enabled: true,
1873 mode: crate::ai_response::AiResponseMode::Intelligent,
1874 prompt: None,
1875 context: None,
1876 temperature: 0.7,
1877 max_tokens: 1000,
1878 schema: None,
1879 cache_enabled: true,
1880 };
1881 let context = RequestContext {
1882 method: "GET".to_string(),
1883 path: "/api/test".to_string(),
1884 path_params: HashMap::new(),
1885 query_params: HashMap::new(),
1886 headers: HashMap::new(),
1887 body: None,
1888 multipart_fields: HashMap::new(),
1889 multipart_files: HashMap::new(),
1890 };
1891
1892 let result = ResponseGenerator::generate_ai_response(&ai_config, &context, None).await;
1893
1894 assert!(result.is_err());
1895 }
1896
1897 #[test]
1898 fn test_generate_response_with_expansion() {
1899 let spec = OpenApiSpec::from_string(
1900 r#"openapi: 3.0.0
1901info:
1902 title: Test API
1903 version: 1.0.0
1904paths:
1905 /users:
1906 get:
1907 responses:
1908 '200':
1909 description: OK
1910 content:
1911 application/json:
1912 schema:
1913 type: object
1914 properties:
1915 id:
1916 type: integer
1917 name:
1918 type: string
1919"#,
1920 Some("yaml"),
1921 )
1922 .unwrap();
1923
1924 let operation = spec
1925 .spec
1926 .paths
1927 .paths
1928 .get("/users")
1929 .and_then(|p| p.as_item())
1930 .and_then(|p| p.get.as_ref())
1931 .unwrap();
1932
1933 let response = ResponseGenerator::generate_response_with_expansion(
1934 &spec,
1935 operation,
1936 200,
1937 Some("application/json"),
1938 true,
1939 )
1940 .unwrap();
1941
1942 assert!(response.is_object());
1943 }
1944
1945 #[test]
1946 fn test_generate_response_with_scenario() {
1947 let spec = OpenApiSpec::from_string(
1948 r#"openapi: 3.0.0
1949info:
1950 title: Test API
1951 version: 1.0.0
1952paths:
1953 /users:
1954 get:
1955 responses:
1956 '200':
1957 description: OK
1958 content:
1959 application/json:
1960 examples:
1961 happy:
1962 value:
1963 id: 1
1964 name: "Happy User"
1965 sad:
1966 value:
1967 id: 2
1968 name: "Sad User"
1969"#,
1970 Some("yaml"),
1971 )
1972 .unwrap();
1973
1974 let operation = spec
1975 .spec
1976 .paths
1977 .paths
1978 .get("/users")
1979 .and_then(|p| p.as_item())
1980 .and_then(|p| p.get.as_ref())
1981 .unwrap();
1982
1983 let response = ResponseGenerator::generate_response_with_scenario(
1984 &spec,
1985 operation,
1986 200,
1987 Some("application/json"),
1988 false,
1989 Some("happy"),
1990 )
1991 .unwrap();
1992
1993 assert_eq!(response["id"], 1);
1994 assert_eq!(response["name"], "Happy User");
1995 }
1996
1997 #[test]
1998 fn test_generate_response_with_referenced_response() {
1999 let spec = OpenApiSpec::from_string(
2000 r#"openapi: 3.0.0
2001info:
2002 title: Test API
2003 version: 1.0.0
2004paths:
2005 /users:
2006 get:
2007 responses:
2008 '200':
2009 $ref: '#/components/responses/UserResponse'
2010components:
2011 responses:
2012 UserResponse:
2013 description: User response
2014 content:
2015 application/json:
2016 schema:
2017 type: object
2018 properties:
2019 id:
2020 type: integer
2021 name:
2022 type: string
2023"#,
2024 Some("yaml"),
2025 )
2026 .unwrap();
2027
2028 let operation = spec
2029 .spec
2030 .paths
2031 .paths
2032 .get("/users")
2033 .and_then(|p| p.as_item())
2034 .and_then(|p| p.get.as_ref())
2035 .unwrap();
2036
2037 let response =
2038 ResponseGenerator::generate_response(&spec, operation, 200, Some("application/json"))
2039 .unwrap();
2040
2041 assert!(response.is_object());
2042 }
2043
2044 #[test]
2045 fn test_generate_response_with_default_status() {
2046 let spec = OpenApiSpec::from_string(
2047 r#"openapi: 3.0.0
2048info:
2049 title: Test API
2050 version: 1.0.0
2051paths:
2052 /users:
2053 get:
2054 responses:
2055 '200':
2056 description: OK
2057 default:
2058 description: Error
2059 content:
2060 application/json:
2061 schema:
2062 type: object
2063 properties:
2064 error:
2065 type: string
2066"#,
2067 Some("yaml"),
2068 )
2069 .unwrap();
2070
2071 let operation = spec
2072 .spec
2073 .paths
2074 .paths
2075 .get("/users")
2076 .and_then(|p| p.as_item())
2077 .and_then(|p| p.get.as_ref())
2078 .unwrap();
2079
2080 let response =
2082 ResponseGenerator::generate_response(&spec, operation, 500, Some("application/json"))
2083 .unwrap();
2084
2085 assert!(response.is_object());
2086 }
2087
2088 #[test]
2089 fn test_generate_response_with_example_in_media_type() {
2090 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 '200':
2100 description: OK
2101 content:
2102 application/json:
2103 example:
2104 id: 1
2105 name: "Example User"
2106"#,
2107 Some("yaml"),
2108 )
2109 .unwrap();
2110
2111 let operation = spec
2112 .spec
2113 .paths
2114 .paths
2115 .get("/users")
2116 .and_then(|p| p.as_item())
2117 .and_then(|p| p.get.as_ref())
2118 .unwrap();
2119
2120 let response =
2121 ResponseGenerator::generate_response(&spec, operation, 200, Some("application/json"))
2122 .unwrap();
2123
2124 assert_eq!(response["id"], 1);
2125 assert_eq!(response["name"], "Example User");
2126 }
2127
2128 #[test]
2129 fn test_generate_response_with_schema_example() {
2130 let spec = OpenApiSpec::from_string(
2131 r#"openapi: 3.0.0
2132info:
2133 title: Test API
2134 version: 1.0.0
2135paths:
2136 /users:
2137 get:
2138 responses:
2139 '200':
2140 description: OK
2141 content:
2142 application/json:
2143 schema:
2144 type: object
2145 example:
2146 id: 42
2147 name: "Schema Example"
2148 properties:
2149 id:
2150 type: integer
2151 name:
2152 type: string
2153"#,
2154 Some("yaml"),
2155 )
2156 .unwrap();
2157
2158 let operation = spec
2159 .spec
2160 .paths
2161 .paths
2162 .get("/users")
2163 .and_then(|p| p.as_item())
2164 .and_then(|p| p.get.as_ref())
2165 .unwrap();
2166
2167 let response =
2168 ResponseGenerator::generate_response(&spec, operation, 200, Some("application/json"))
2169 .unwrap();
2170
2171 assert!(response.is_object());
2173 }
2174
2175 #[test]
2176 fn test_generate_response_with_referenced_schema() {
2177 let spec = OpenApiSpec::from_string(
2178 r#"openapi: 3.0.0
2179info:
2180 title: Test API
2181 version: 1.0.0
2182paths:
2183 /users:
2184 get:
2185 responses:
2186 '200':
2187 description: OK
2188 content:
2189 application/json:
2190 schema:
2191 $ref: '#/components/schemas/User'
2192components:
2193 schemas:
2194 User:
2195 type: object
2196 properties:
2197 id:
2198 type: integer
2199 name:
2200 type: string
2201"#,
2202 Some("yaml"),
2203 )
2204 .unwrap();
2205
2206 let operation = spec
2207 .spec
2208 .paths
2209 .paths
2210 .get("/users")
2211 .and_then(|p| p.as_item())
2212 .and_then(|p| p.get.as_ref())
2213 .unwrap();
2214
2215 let response =
2216 ResponseGenerator::generate_response(&spec, operation, 200, Some("application/json"))
2217 .unwrap();
2218
2219 assert!(response.is_object());
2220 assert!(response.get("id").is_some());
2221 assert!(response.get("name").is_some());
2222 }
2223
2224 #[test]
2225 fn test_generate_response_with_array_schema() {
2226 let spec = OpenApiSpec::from_string(
2227 r#"openapi: 3.0.0
2228info:
2229 title: Test API
2230 version: 1.0.0
2231paths:
2232 /users:
2233 get:
2234 responses:
2235 '200':
2236 description: OK
2237 content:
2238 application/json:
2239 schema:
2240 type: array
2241 items:
2242 type: object
2243 properties:
2244 id:
2245 type: integer
2246 name:
2247 type: string
2248"#,
2249 Some("yaml"),
2250 )
2251 .unwrap();
2252
2253 let operation = spec
2254 .spec
2255 .paths
2256 .paths
2257 .get("/users")
2258 .and_then(|p| p.as_item())
2259 .and_then(|p| p.get.as_ref())
2260 .unwrap();
2261
2262 let response =
2263 ResponseGenerator::generate_response(&spec, operation, 200, Some("application/json"))
2264 .unwrap();
2265
2266 assert!(response.is_array());
2267 }
2268
2269 #[test]
2270 fn test_generate_response_with_different_content_types() {
2271 let spec = OpenApiSpec::from_string(
2272 r#"openapi: 3.0.0
2273info:
2274 title: Test API
2275 version: 1.0.0
2276paths:
2277 /users:
2278 get:
2279 responses:
2280 '200':
2281 description: OK
2282 content:
2283 application/json:
2284 schema:
2285 type: object
2286 text/plain:
2287 schema:
2288 type: string
2289"#,
2290 Some("yaml"),
2291 )
2292 .unwrap();
2293
2294 let operation = spec
2295 .spec
2296 .paths
2297 .paths
2298 .get("/users")
2299 .and_then(|p| p.as_item())
2300 .and_then(|p| p.get.as_ref())
2301 .unwrap();
2302
2303 let json_response =
2305 ResponseGenerator::generate_response(&spec, operation, 200, Some("application/json"))
2306 .unwrap();
2307 assert!(json_response.is_object());
2308
2309 let text_response =
2311 ResponseGenerator::generate_response(&spec, operation, 200, Some("text/plain"))
2312 .unwrap();
2313 assert!(text_response.is_string());
2314 }
2315
2316 #[test]
2317 fn test_generate_response_without_content_type() {
2318 let spec = OpenApiSpec::from_string(
2319 r#"openapi: 3.0.0
2320info:
2321 title: Test API
2322 version: 1.0.0
2323paths:
2324 /users:
2325 get:
2326 responses:
2327 '200':
2328 description: OK
2329 content:
2330 application/json:
2331 schema:
2332 type: object
2333 properties:
2334 id:
2335 type: integer
2336"#,
2337 Some("yaml"),
2338 )
2339 .unwrap();
2340
2341 let operation = spec
2342 .spec
2343 .paths
2344 .paths
2345 .get("/users")
2346 .and_then(|p| p.as_item())
2347 .and_then(|p| p.get.as_ref())
2348 .unwrap();
2349
2350 let response = ResponseGenerator::generate_response(&spec, operation, 200, None).unwrap();
2352
2353 assert!(response.is_object());
2354 }
2355
2356 #[test]
2357 fn test_generate_response_with_no_content() {
2358 let spec = OpenApiSpec::from_string(
2359 r#"openapi: 3.0.0
2360info:
2361 title: Test API
2362 version: 1.0.0
2363paths:
2364 /users:
2365 delete:
2366 responses:
2367 '204':
2368 description: No Content
2369"#,
2370 Some("yaml"),
2371 )
2372 .unwrap();
2373
2374 let operation = spec
2375 .spec
2376 .paths
2377 .paths
2378 .get("/users")
2379 .and_then(|p| p.as_item())
2380 .and_then(|p| p.delete.as_ref())
2381 .unwrap();
2382
2383 let response = ResponseGenerator::generate_response(&spec, operation, 204, None).unwrap();
2384
2385 assert!(response.is_object());
2387 assert!(response.as_object().unwrap().is_empty());
2388 }
2389
2390 #[test]
2391 fn test_generate_response_with_expansion_disabled() {
2392 let spec = OpenApiSpec::from_string(
2393 r#"openapi: 3.0.0
2394info:
2395 title: Test API
2396 version: 1.0.0
2397paths:
2398 /users:
2399 get:
2400 responses:
2401 '200':
2402 description: OK
2403 content:
2404 application/json:
2405 schema:
2406 type: object
2407 properties:
2408 id:
2409 type: integer
2410 name:
2411 type: string
2412"#,
2413 Some("yaml"),
2414 )
2415 .unwrap();
2416
2417 let operation = spec
2418 .spec
2419 .paths
2420 .paths
2421 .get("/users")
2422 .and_then(|p| p.as_item())
2423 .and_then(|p| p.get.as_ref())
2424 .unwrap();
2425
2426 let response = ResponseGenerator::generate_response_with_expansion(
2427 &spec,
2428 operation,
2429 200,
2430 Some("application/json"),
2431 false, )
2433 .unwrap();
2434
2435 assert!(response.is_object());
2436 }
2437
2438 #[test]
2439 fn test_generate_response_with_array_schema_referenced_items() {
2440 let spec = OpenApiSpec::from_string(
2442 r#"openapi: 3.0.0
2443info:
2444 title: Test API
2445 version: 1.0.0
2446paths:
2447 /items:
2448 get:
2449 responses:
2450 '200':
2451 description: OK
2452 content:
2453 application/json:
2454 schema:
2455 type: array
2456 items:
2457 $ref: '#/components/schemas/Item'
2458components:
2459 schemas:
2460 Item:
2461 type: object
2462 properties:
2463 id:
2464 type: string
2465 name:
2466 type: string
2467"#,
2468 Some("yaml"),
2469 )
2470 .unwrap();
2471
2472 let operation = spec
2473 .spec
2474 .paths
2475 .paths
2476 .get("/items")
2477 .and_then(|p| p.as_item())
2478 .and_then(|p| p.get.as_ref())
2479 .unwrap();
2480
2481 let response =
2482 ResponseGenerator::generate_response(&spec, operation, 200, Some("application/json"))
2483 .unwrap();
2484
2485 let arr = response.as_array().expect("response should be array");
2487 assert!(!arr.is_empty());
2488 if let Some(item) = arr.first() {
2489 let obj = item.as_object().expect("item should be object");
2490 assert!(obj.contains_key("id") || obj.contains_key("name"));
2491 }
2492 }
2493
2494 #[test]
2495 fn test_generate_response_with_array_schema_missing_reference() {
2496 let spec = OpenApiSpec::from_string(
2498 r#"openapi: 3.0.0
2499info:
2500 title: Test API
2501 version: 1.0.0
2502paths:
2503 /items:
2504 get:
2505 responses:
2506 '200':
2507 description: OK
2508 content:
2509 application/json:
2510 schema:
2511 type: array
2512 items:
2513 $ref: '#/components/schemas/NonExistentItem'
2514components:
2515 schemas: {}
2516"#,
2517 Some("yaml"),
2518 )
2519 .unwrap();
2520
2521 let operation = spec
2522 .spec
2523 .paths
2524 .paths
2525 .get("/items")
2526 .and_then(|p| p.as_item())
2527 .and_then(|p| p.get.as_ref())
2528 .unwrap();
2529
2530 let response =
2531 ResponseGenerator::generate_response(&spec, operation, 200, Some("application/json"))
2532 .unwrap();
2533
2534 let arr = response.as_array().expect("response should be array");
2536 assert!(!arr.is_empty());
2537 }
2538
2539 #[test]
2540 fn test_generate_response_with_array_example_and_pagination() {
2541 let spec = OpenApiSpec::from_string(
2543 r#"openapi: 3.0.0
2544info:
2545 title: Test API
2546 version: 1.0.0
2547paths:
2548 /products:
2549 get:
2550 responses:
2551 '200':
2552 description: OK
2553 content:
2554 application/json:
2555 schema:
2556 type: array
2557 example: [{"id": 1, "name": "Product 1"}]
2558 items:
2559 type: object
2560 properties:
2561 id:
2562 type: integer
2563 name:
2564 type: string
2565"#,
2566 Some("yaml"),
2567 )
2568 .unwrap();
2569
2570 let operation = spec
2571 .spec
2572 .paths
2573 .paths
2574 .get("/products")
2575 .and_then(|p| p.as_item())
2576 .and_then(|p| p.get.as_ref())
2577 .unwrap();
2578
2579 let response =
2580 ResponseGenerator::generate_response(&spec, operation, 200, Some("application/json"))
2581 .unwrap();
2582
2583 let arr = response.as_array().expect("response should be array");
2585 assert!(!arr.is_empty());
2586 if let Some(item) = arr.first() {
2587 let obj = item.as_object().expect("item should be object");
2588 assert!(obj.contains_key("id") || obj.contains_key("name"));
2589 }
2590 }
2591
2592 #[test]
2593 fn test_generate_response_with_missing_response_reference() {
2594 let spec = OpenApiSpec::from_string(
2596 r#"openapi: 3.0.0
2597info:
2598 title: Test API
2599 version: 1.0.0
2600paths:
2601 /users:
2602 get:
2603 responses:
2604 '200':
2605 $ref: '#/components/responses/NonExistentResponse'
2606components:
2607 responses: {}
2608"#,
2609 Some("yaml"),
2610 )
2611 .unwrap();
2612
2613 let operation = spec
2614 .spec
2615 .paths
2616 .paths
2617 .get("/users")
2618 .and_then(|p| p.as_item())
2619 .and_then(|p| p.get.as_ref())
2620 .unwrap();
2621
2622 let response =
2623 ResponseGenerator::generate_response(&spec, operation, 200, Some("application/json"))
2624 .unwrap();
2625
2626 assert!(response.is_object());
2628 assert!(response.as_object().unwrap().is_empty());
2629 }
2630
2631 #[test]
2632 fn test_generate_response_with_no_response_for_status() {
2633 let spec = OpenApiSpec::from_string(
2635 r#"openapi: 3.0.0
2636info:
2637 title: Test API
2638 version: 1.0.0
2639paths:
2640 /users:
2641 get:
2642 responses:
2643 '404':
2644 description: Not found
2645"#,
2646 Some("yaml"),
2647 )
2648 .unwrap();
2649
2650 let operation = spec
2651 .spec
2652 .paths
2653 .paths
2654 .get("/users")
2655 .and_then(|p| p.as_item())
2656 .and_then(|p| p.get.as_ref())
2657 .unwrap();
2658
2659 let response =
2661 ResponseGenerator::generate_response(&spec, operation, 200, Some("application/json"))
2662 .unwrap();
2663
2664 assert!(response.is_object());
2666 assert!(response.as_object().unwrap().is_empty());
2667 }
2668}
2669
2670#[derive(Debug, Clone)]
2672pub struct MockResponse {
2673 pub status_code: u16,
2675 pub headers: HashMap<String, String>,
2677 pub body: Option<Value>,
2679}
2680
2681impl MockResponse {
2682 pub fn new(status_code: u16) -> Self {
2684 Self {
2685 status_code,
2686 headers: HashMap::new(),
2687 body: None,
2688 }
2689 }
2690
2691 pub fn with_header(mut self, name: String, value: String) -> Self {
2693 self.headers.insert(name, value);
2694 self
2695 }
2696
2697 pub fn with_body(mut self, body: Value) -> Self {
2699 self.body = Some(body);
2700 self
2701 }
2702}
2703
2704#[derive(Debug, Clone)]
2706pub struct OpenApiSecurityRequirement {
2707 pub scheme: String,
2709 pub scopes: Vec<String>,
2711}
2712
2713impl OpenApiSecurityRequirement {
2714 pub fn new(scheme: String, scopes: Vec<String>) -> Self {
2716 Self { scheme, scopes }
2717 }
2718}
2719
2720#[derive(Debug, Clone)]
2722pub struct OpenApiOperation {
2723 pub method: String,
2725 pub path: String,
2727 pub operation: Operation,
2729}
2730
2731impl OpenApiOperation {
2732 pub fn new(method: String, path: String, operation: Operation) -> Self {
2734 Self {
2735 method,
2736 path,
2737 operation,
2738 }
2739 }
2740}