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_err());
1864 let err = result.unwrap_err().to_string();
1865 assert!(
1866 err.contains("no AI generator configured"),
1867 "Expected 'no AI generator configured' error, got: {}",
1868 err
1869 );
1870 }
1871
1872 #[tokio::test]
1873 async fn test_generate_ai_response_no_prompt() {
1874 let ai_config = AiResponseConfig {
1875 enabled: true,
1876 mode: crate::ai_response::AiResponseMode::Intelligent,
1877 prompt: None,
1878 context: None,
1879 temperature: 0.7,
1880 max_tokens: 1000,
1881 schema: None,
1882 cache_enabled: true,
1883 };
1884 let context = RequestContext {
1885 method: "GET".to_string(),
1886 path: "/api/test".to_string(),
1887 path_params: HashMap::new(),
1888 query_params: HashMap::new(),
1889 headers: HashMap::new(),
1890 body: None,
1891 multipart_fields: HashMap::new(),
1892 multipart_files: HashMap::new(),
1893 };
1894
1895 let result = ResponseGenerator::generate_ai_response(&ai_config, &context, None).await;
1896
1897 assert!(result.is_err());
1898 }
1899
1900 #[test]
1901 fn test_generate_response_with_expansion() {
1902 let spec = OpenApiSpec::from_string(
1903 r#"openapi: 3.0.0
1904info:
1905 title: Test API
1906 version: 1.0.0
1907paths:
1908 /users:
1909 get:
1910 responses:
1911 '200':
1912 description: OK
1913 content:
1914 application/json:
1915 schema:
1916 type: object
1917 properties:
1918 id:
1919 type: integer
1920 name:
1921 type: string
1922"#,
1923 Some("yaml"),
1924 )
1925 .unwrap();
1926
1927 let operation = spec
1928 .spec
1929 .paths
1930 .paths
1931 .get("/users")
1932 .and_then(|p| p.as_item())
1933 .and_then(|p| p.get.as_ref())
1934 .unwrap();
1935
1936 let response = ResponseGenerator::generate_response_with_expansion(
1937 &spec,
1938 operation,
1939 200,
1940 Some("application/json"),
1941 true,
1942 )
1943 .unwrap();
1944
1945 assert!(response.is_object());
1946 }
1947
1948 #[test]
1949 fn test_generate_response_with_scenario() {
1950 let spec = OpenApiSpec::from_string(
1951 r#"openapi: 3.0.0
1952info:
1953 title: Test API
1954 version: 1.0.0
1955paths:
1956 /users:
1957 get:
1958 responses:
1959 '200':
1960 description: OK
1961 content:
1962 application/json:
1963 examples:
1964 happy:
1965 value:
1966 id: 1
1967 name: "Happy User"
1968 sad:
1969 value:
1970 id: 2
1971 name: "Sad User"
1972"#,
1973 Some("yaml"),
1974 )
1975 .unwrap();
1976
1977 let operation = spec
1978 .spec
1979 .paths
1980 .paths
1981 .get("/users")
1982 .and_then(|p| p.as_item())
1983 .and_then(|p| p.get.as_ref())
1984 .unwrap();
1985
1986 let response = ResponseGenerator::generate_response_with_scenario(
1987 &spec,
1988 operation,
1989 200,
1990 Some("application/json"),
1991 false,
1992 Some("happy"),
1993 )
1994 .unwrap();
1995
1996 assert_eq!(response["id"], 1);
1997 assert_eq!(response["name"], "Happy User");
1998 }
1999
2000 #[test]
2001 fn test_generate_response_with_referenced_response() {
2002 let spec = OpenApiSpec::from_string(
2003 r#"openapi: 3.0.0
2004info:
2005 title: Test API
2006 version: 1.0.0
2007paths:
2008 /users:
2009 get:
2010 responses:
2011 '200':
2012 $ref: '#/components/responses/UserResponse'
2013components:
2014 responses:
2015 UserResponse:
2016 description: User response
2017 content:
2018 application/json:
2019 schema:
2020 type: object
2021 properties:
2022 id:
2023 type: integer
2024 name:
2025 type: string
2026"#,
2027 Some("yaml"),
2028 )
2029 .unwrap();
2030
2031 let operation = spec
2032 .spec
2033 .paths
2034 .paths
2035 .get("/users")
2036 .and_then(|p| p.as_item())
2037 .and_then(|p| p.get.as_ref())
2038 .unwrap();
2039
2040 let response =
2041 ResponseGenerator::generate_response(&spec, operation, 200, Some("application/json"))
2042 .unwrap();
2043
2044 assert!(response.is_object());
2045 }
2046
2047 #[test]
2048 fn test_generate_response_with_default_status() {
2049 let spec = OpenApiSpec::from_string(
2050 r#"openapi: 3.0.0
2051info:
2052 title: Test API
2053 version: 1.0.0
2054paths:
2055 /users:
2056 get:
2057 responses:
2058 '200':
2059 description: OK
2060 default:
2061 description: Error
2062 content:
2063 application/json:
2064 schema:
2065 type: object
2066 properties:
2067 error:
2068 type: string
2069"#,
2070 Some("yaml"),
2071 )
2072 .unwrap();
2073
2074 let operation = spec
2075 .spec
2076 .paths
2077 .paths
2078 .get("/users")
2079 .and_then(|p| p.as_item())
2080 .and_then(|p| p.get.as_ref())
2081 .unwrap();
2082
2083 let response =
2085 ResponseGenerator::generate_response(&spec, operation, 500, Some("application/json"))
2086 .unwrap();
2087
2088 assert!(response.is_object());
2089 }
2090
2091 #[test]
2092 fn test_generate_response_with_example_in_media_type() {
2093 let spec = OpenApiSpec::from_string(
2094 r#"openapi: 3.0.0
2095info:
2096 title: Test API
2097 version: 1.0.0
2098paths:
2099 /users:
2100 get:
2101 responses:
2102 '200':
2103 description: OK
2104 content:
2105 application/json:
2106 example:
2107 id: 1
2108 name: "Example User"
2109"#,
2110 Some("yaml"),
2111 )
2112 .unwrap();
2113
2114 let operation = spec
2115 .spec
2116 .paths
2117 .paths
2118 .get("/users")
2119 .and_then(|p| p.as_item())
2120 .and_then(|p| p.get.as_ref())
2121 .unwrap();
2122
2123 let response =
2124 ResponseGenerator::generate_response(&spec, operation, 200, Some("application/json"))
2125 .unwrap();
2126
2127 assert_eq!(response["id"], 1);
2128 assert_eq!(response["name"], "Example User");
2129 }
2130
2131 #[test]
2132 fn test_generate_response_with_schema_example() {
2133 let spec = OpenApiSpec::from_string(
2134 r#"openapi: 3.0.0
2135info:
2136 title: Test API
2137 version: 1.0.0
2138paths:
2139 /users:
2140 get:
2141 responses:
2142 '200':
2143 description: OK
2144 content:
2145 application/json:
2146 schema:
2147 type: object
2148 example:
2149 id: 42
2150 name: "Schema Example"
2151 properties:
2152 id:
2153 type: integer
2154 name:
2155 type: string
2156"#,
2157 Some("yaml"),
2158 )
2159 .unwrap();
2160
2161 let operation = spec
2162 .spec
2163 .paths
2164 .paths
2165 .get("/users")
2166 .and_then(|p| p.as_item())
2167 .and_then(|p| p.get.as_ref())
2168 .unwrap();
2169
2170 let response =
2171 ResponseGenerator::generate_response(&spec, operation, 200, Some("application/json"))
2172 .unwrap();
2173
2174 assert!(response.is_object());
2176 }
2177
2178 #[test]
2179 fn test_generate_response_with_referenced_schema() {
2180 let spec = OpenApiSpec::from_string(
2181 r#"openapi: 3.0.0
2182info:
2183 title: Test API
2184 version: 1.0.0
2185paths:
2186 /users:
2187 get:
2188 responses:
2189 '200':
2190 description: OK
2191 content:
2192 application/json:
2193 schema:
2194 $ref: '#/components/schemas/User'
2195components:
2196 schemas:
2197 User:
2198 type: object
2199 properties:
2200 id:
2201 type: integer
2202 name:
2203 type: string
2204"#,
2205 Some("yaml"),
2206 )
2207 .unwrap();
2208
2209 let operation = spec
2210 .spec
2211 .paths
2212 .paths
2213 .get("/users")
2214 .and_then(|p| p.as_item())
2215 .and_then(|p| p.get.as_ref())
2216 .unwrap();
2217
2218 let response =
2219 ResponseGenerator::generate_response(&spec, operation, 200, Some("application/json"))
2220 .unwrap();
2221
2222 assert!(response.is_object());
2223 assert!(response.get("id").is_some());
2224 assert!(response.get("name").is_some());
2225 }
2226
2227 #[test]
2228 fn test_generate_response_with_array_schema() {
2229 let spec = OpenApiSpec::from_string(
2230 r#"openapi: 3.0.0
2231info:
2232 title: Test API
2233 version: 1.0.0
2234paths:
2235 /users:
2236 get:
2237 responses:
2238 '200':
2239 description: OK
2240 content:
2241 application/json:
2242 schema:
2243 type: array
2244 items:
2245 type: object
2246 properties:
2247 id:
2248 type: integer
2249 name:
2250 type: string
2251"#,
2252 Some("yaml"),
2253 )
2254 .unwrap();
2255
2256 let operation = spec
2257 .spec
2258 .paths
2259 .paths
2260 .get("/users")
2261 .and_then(|p| p.as_item())
2262 .and_then(|p| p.get.as_ref())
2263 .unwrap();
2264
2265 let response =
2266 ResponseGenerator::generate_response(&spec, operation, 200, Some("application/json"))
2267 .unwrap();
2268
2269 assert!(response.is_array());
2270 }
2271
2272 #[test]
2273 fn test_generate_response_with_different_content_types() {
2274 let spec = OpenApiSpec::from_string(
2275 r#"openapi: 3.0.0
2276info:
2277 title: Test API
2278 version: 1.0.0
2279paths:
2280 /users:
2281 get:
2282 responses:
2283 '200':
2284 description: OK
2285 content:
2286 application/json:
2287 schema:
2288 type: object
2289 text/plain:
2290 schema:
2291 type: string
2292"#,
2293 Some("yaml"),
2294 )
2295 .unwrap();
2296
2297 let operation = spec
2298 .spec
2299 .paths
2300 .paths
2301 .get("/users")
2302 .and_then(|p| p.as_item())
2303 .and_then(|p| p.get.as_ref())
2304 .unwrap();
2305
2306 let json_response =
2308 ResponseGenerator::generate_response(&spec, operation, 200, Some("application/json"))
2309 .unwrap();
2310 assert!(json_response.is_object());
2311
2312 let text_response =
2314 ResponseGenerator::generate_response(&spec, operation, 200, Some("text/plain"))
2315 .unwrap();
2316 assert!(text_response.is_string());
2317 }
2318
2319 #[test]
2320 fn test_generate_response_without_content_type() {
2321 let spec = OpenApiSpec::from_string(
2322 r#"openapi: 3.0.0
2323info:
2324 title: Test API
2325 version: 1.0.0
2326paths:
2327 /users:
2328 get:
2329 responses:
2330 '200':
2331 description: OK
2332 content:
2333 application/json:
2334 schema:
2335 type: object
2336 properties:
2337 id:
2338 type: integer
2339"#,
2340 Some("yaml"),
2341 )
2342 .unwrap();
2343
2344 let operation = spec
2345 .spec
2346 .paths
2347 .paths
2348 .get("/users")
2349 .and_then(|p| p.as_item())
2350 .and_then(|p| p.get.as_ref())
2351 .unwrap();
2352
2353 let response = ResponseGenerator::generate_response(&spec, operation, 200, None).unwrap();
2355
2356 assert!(response.is_object());
2357 }
2358
2359 #[test]
2360 fn test_generate_response_with_no_content() {
2361 let spec = OpenApiSpec::from_string(
2362 r#"openapi: 3.0.0
2363info:
2364 title: Test API
2365 version: 1.0.0
2366paths:
2367 /users:
2368 delete:
2369 responses:
2370 '204':
2371 description: No Content
2372"#,
2373 Some("yaml"),
2374 )
2375 .unwrap();
2376
2377 let operation = spec
2378 .spec
2379 .paths
2380 .paths
2381 .get("/users")
2382 .and_then(|p| p.as_item())
2383 .and_then(|p| p.delete.as_ref())
2384 .unwrap();
2385
2386 let response = ResponseGenerator::generate_response(&spec, operation, 204, None).unwrap();
2387
2388 assert!(response.is_object());
2390 assert!(response.as_object().unwrap().is_empty());
2391 }
2392
2393 #[test]
2394 fn test_generate_response_with_expansion_disabled() {
2395 let spec = OpenApiSpec::from_string(
2396 r#"openapi: 3.0.0
2397info:
2398 title: Test API
2399 version: 1.0.0
2400paths:
2401 /users:
2402 get:
2403 responses:
2404 '200':
2405 description: OK
2406 content:
2407 application/json:
2408 schema:
2409 type: object
2410 properties:
2411 id:
2412 type: integer
2413 name:
2414 type: string
2415"#,
2416 Some("yaml"),
2417 )
2418 .unwrap();
2419
2420 let operation = spec
2421 .spec
2422 .paths
2423 .paths
2424 .get("/users")
2425 .and_then(|p| p.as_item())
2426 .and_then(|p| p.get.as_ref())
2427 .unwrap();
2428
2429 let response = ResponseGenerator::generate_response_with_expansion(
2430 &spec,
2431 operation,
2432 200,
2433 Some("application/json"),
2434 false, )
2436 .unwrap();
2437
2438 assert!(response.is_object());
2439 }
2440
2441 #[test]
2442 fn test_generate_response_with_array_schema_referenced_items() {
2443 let spec = OpenApiSpec::from_string(
2445 r#"openapi: 3.0.0
2446info:
2447 title: Test API
2448 version: 1.0.0
2449paths:
2450 /items:
2451 get:
2452 responses:
2453 '200':
2454 description: OK
2455 content:
2456 application/json:
2457 schema:
2458 type: array
2459 items:
2460 $ref: '#/components/schemas/Item'
2461components:
2462 schemas:
2463 Item:
2464 type: object
2465 properties:
2466 id:
2467 type: string
2468 name:
2469 type: string
2470"#,
2471 Some("yaml"),
2472 )
2473 .unwrap();
2474
2475 let operation = spec
2476 .spec
2477 .paths
2478 .paths
2479 .get("/items")
2480 .and_then(|p| p.as_item())
2481 .and_then(|p| p.get.as_ref())
2482 .unwrap();
2483
2484 let response =
2485 ResponseGenerator::generate_response(&spec, operation, 200, Some("application/json"))
2486 .unwrap();
2487
2488 let arr = response.as_array().expect("response should be array");
2490 assert!(!arr.is_empty());
2491 if let Some(item) = arr.first() {
2492 let obj = item.as_object().expect("item should be object");
2493 assert!(obj.contains_key("id") || obj.contains_key("name"));
2494 }
2495 }
2496
2497 #[test]
2498 fn test_generate_response_with_array_schema_missing_reference() {
2499 let spec = OpenApiSpec::from_string(
2501 r#"openapi: 3.0.0
2502info:
2503 title: Test API
2504 version: 1.0.0
2505paths:
2506 /items:
2507 get:
2508 responses:
2509 '200':
2510 description: OK
2511 content:
2512 application/json:
2513 schema:
2514 type: array
2515 items:
2516 $ref: '#/components/schemas/NonExistentItem'
2517components:
2518 schemas: {}
2519"#,
2520 Some("yaml"),
2521 )
2522 .unwrap();
2523
2524 let operation = spec
2525 .spec
2526 .paths
2527 .paths
2528 .get("/items")
2529 .and_then(|p| p.as_item())
2530 .and_then(|p| p.get.as_ref())
2531 .unwrap();
2532
2533 let response =
2534 ResponseGenerator::generate_response(&spec, operation, 200, Some("application/json"))
2535 .unwrap();
2536
2537 let arr = response.as_array().expect("response should be array");
2539 assert!(!arr.is_empty());
2540 }
2541
2542 #[test]
2543 fn test_generate_response_with_array_example_and_pagination() {
2544 let spec = OpenApiSpec::from_string(
2546 r#"openapi: 3.0.0
2547info:
2548 title: Test API
2549 version: 1.0.0
2550paths:
2551 /products:
2552 get:
2553 responses:
2554 '200':
2555 description: OK
2556 content:
2557 application/json:
2558 schema:
2559 type: array
2560 example: [{"id": 1, "name": "Product 1"}]
2561 items:
2562 type: object
2563 properties:
2564 id:
2565 type: integer
2566 name:
2567 type: string
2568"#,
2569 Some("yaml"),
2570 )
2571 .unwrap();
2572
2573 let operation = spec
2574 .spec
2575 .paths
2576 .paths
2577 .get("/products")
2578 .and_then(|p| p.as_item())
2579 .and_then(|p| p.get.as_ref())
2580 .unwrap();
2581
2582 let response =
2583 ResponseGenerator::generate_response(&spec, operation, 200, Some("application/json"))
2584 .unwrap();
2585
2586 let arr = response.as_array().expect("response should be array");
2588 assert!(!arr.is_empty());
2589 if let Some(item) = arr.first() {
2590 let obj = item.as_object().expect("item should be object");
2591 assert!(obj.contains_key("id") || obj.contains_key("name"));
2592 }
2593 }
2594
2595 #[test]
2596 fn test_generate_response_with_missing_response_reference() {
2597 let spec = OpenApiSpec::from_string(
2599 r#"openapi: 3.0.0
2600info:
2601 title: Test API
2602 version: 1.0.0
2603paths:
2604 /users:
2605 get:
2606 responses:
2607 '200':
2608 $ref: '#/components/responses/NonExistentResponse'
2609components:
2610 responses: {}
2611"#,
2612 Some("yaml"),
2613 )
2614 .unwrap();
2615
2616 let operation = spec
2617 .spec
2618 .paths
2619 .paths
2620 .get("/users")
2621 .and_then(|p| p.as_item())
2622 .and_then(|p| p.get.as_ref())
2623 .unwrap();
2624
2625 let response =
2626 ResponseGenerator::generate_response(&spec, operation, 200, Some("application/json"))
2627 .unwrap();
2628
2629 assert!(response.is_object());
2631 assert!(response.as_object().unwrap().is_empty());
2632 }
2633
2634 #[test]
2635 fn test_generate_response_with_no_response_for_status() {
2636 let spec = OpenApiSpec::from_string(
2638 r#"openapi: 3.0.0
2639info:
2640 title: Test API
2641 version: 1.0.0
2642paths:
2643 /users:
2644 get:
2645 responses:
2646 '404':
2647 description: Not found
2648"#,
2649 Some("yaml"),
2650 )
2651 .unwrap();
2652
2653 let operation = spec
2654 .spec
2655 .paths
2656 .paths
2657 .get("/users")
2658 .and_then(|p| p.as_item())
2659 .and_then(|p| p.get.as_ref())
2660 .unwrap();
2661
2662 let response =
2664 ResponseGenerator::generate_response(&spec, operation, 200, Some("application/json"))
2665 .unwrap();
2666
2667 assert!(response.is_object());
2669 assert!(response.as_object().unwrap().is_empty());
2670 }
2671}
2672
2673#[derive(Debug, Clone)]
2675pub struct MockResponse {
2676 pub status_code: u16,
2678 pub headers: HashMap<String, String>,
2680 pub body: Option<Value>,
2682}
2683
2684impl MockResponse {
2685 pub fn new(status_code: u16) -> Self {
2687 Self {
2688 status_code,
2689 headers: HashMap::new(),
2690 body: None,
2691 }
2692 }
2693
2694 pub fn with_header(mut self, name: String, value: String) -> Self {
2696 self.headers.insert(name, value);
2697 self
2698 }
2699
2700 pub fn with_body(mut self, body: Value) -> Self {
2702 self.body = Some(body);
2703 self
2704 }
2705}
2706
2707#[derive(Debug, Clone)]
2709pub struct OpenApiSecurityRequirement {
2710 pub scheme: String,
2712 pub scopes: Vec<String>,
2714}
2715
2716impl OpenApiSecurityRequirement {
2717 pub fn new(scheme: String, scopes: Vec<String>) -> Self {
2719 Self { scheme, scopes }
2720 }
2721}
2722
2723#[derive(Debug, Clone)]
2725pub struct OpenApiOperation {
2726 pub method: String,
2728 pub path: String,
2730 pub operation: Operation,
2732}
2733
2734impl OpenApiOperation {
2735 pub fn new(method: String, path: String, operation: Operation) -> Self {
2737 Self {
2738 method,
2739 path,
2740 operation,
2741 }
2742 }
2743}