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