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(_)) => Value::Number(
837 serde_json::Number::from_f64(std::f64::consts::PI)
838 .expect("PI is a valid f64 value"),
839 ),
840 openapiv3::SchemaKind::Type(openapiv3::Type::Boolean(_)) => Value::Bool(true),
841 openapiv3::SchemaKind::Type(openapiv3::Type::Object(obj)) => {
842 let mut pagination_metadata: Option<(u64, u64, u64)> = None; let has_items =
849 obj.properties.iter().any(|(name, _)| name.to_lowercase() == "items");
850
851 if has_items {
852 let mut total_opt = None;
854 let mut page_opt = None;
855 let mut limit_opt = None;
856
857 for (prop_name, prop_schema) in &obj.properties {
858 let prop_lower = prop_name.to_lowercase();
859 let schema_ref: ReferenceOr<Schema> = match prop_schema {
861 ReferenceOr::Item(boxed) => ReferenceOr::Item(boxed.as_ref().clone()),
862 ReferenceOr::Reference { reference } => ReferenceOr::Reference {
863 reference: reference.clone(),
864 },
865 };
866 if prop_lower == "total" || prop_lower == "count" || prop_lower == "size" {
867 total_opt = Self::extract_numeric_value_from_schema(&schema_ref);
868 } else if prop_lower == "page" {
869 page_opt = Self::extract_numeric_value_from_schema(&schema_ref);
870 } else if prop_lower == "limit" || prop_lower == "per_page" {
871 limit_opt = Self::extract_numeric_value_from_schema(&schema_ref);
872 }
873 }
874
875 if let Some(total) = total_opt {
877 let page = page_opt.unwrap_or(1);
878 let limit = limit_opt.unwrap_or(20);
879 pagination_metadata = Some((total, page, limit));
880 tracing::debug!(
881 "Detected pagination metadata: total={}, page={}, limit={}",
882 total,
883 page,
884 limit
885 );
886 } else {
887 if obj.properties.contains_key("items") {
890 if let Some(inferred_total) =
894 Self::try_infer_total_from_context(spec, obj)
895 {
896 let page = page_opt.unwrap_or(1);
897 let limit = limit_opt.unwrap_or(20);
898 pagination_metadata = Some((inferred_total, page, limit));
899 tracing::debug!(
900 "Inferred pagination metadata from parent entity: total={}, page={}, limit={}",
901 inferred_total, page, limit
902 );
903 } else {
904 if let Some(persona) = persona {
906 let count_keys =
909 ["hive_count", "apiary_count", "item_count", "total_count"];
910 for key in &count_keys {
911 if let Some(count) = persona.get_numeric_trait(key) {
912 let page = page_opt.unwrap_or(1);
913 let limit = limit_opt.unwrap_or(20);
914 pagination_metadata = Some((count, page, limit));
915 tracing::debug!(
916 "Using persona trait '{}' for pagination: total={}, page={}, limit={}",
917 key, count, page, limit
918 );
919 break;
920 }
921 }
922 }
923 }
924 }
925 }
926 }
927
928 let mut map = serde_json::Map::new();
929 for (prop_name, prop_schema) in &obj.properties {
930 let prop_lower = prop_name.to_lowercase();
931
932 let is_items_array = prop_lower == "items" && pagination_metadata.is_some();
934
935 let value = match prop_schema {
936 ReferenceOr::Item(prop_schema) => {
937 if is_items_array {
940 Self::generate_array_with_count(
942 spec,
943 prop_schema.as_ref(),
944 pagination_metadata.unwrap(),
945 persona,
946 )
947 } else if let Some(prop_example) =
948 prop_schema.schema_data.example.as_ref()
949 {
950 tracing::debug!(
952 "Using example for property '{}': {:?}",
953 prop_name,
954 prop_example
955 );
956 prop_example.clone()
957 } else {
958 Self::generate_example_from_schema(
959 spec,
960 prop_schema.as_ref(),
961 persona,
962 )
963 }
964 }
965 ReferenceOr::Reference { reference } => {
966 if let Some(resolved_schema) = spec.get_schema(reference) {
968 if is_items_array {
970 Self::generate_array_with_count(
972 spec,
973 &resolved_schema.schema,
974 pagination_metadata.unwrap(),
975 persona,
976 )
977 } else if let Some(ref_example) =
978 resolved_schema.schema.schema_data.example.as_ref()
979 {
980 tracing::debug!(
982 "Using example from referenced schema '{}': {:?}",
983 reference,
984 ref_example
985 );
986 ref_example.clone()
987 } else {
988 Self::generate_example_from_schema(
989 spec,
990 &resolved_schema.schema,
991 persona,
992 )
993 }
994 } else {
995 Self::generate_example_for_property(prop_name)
996 }
997 }
998 };
999 let value = match value {
1000 Value::Null => Self::generate_example_for_property(prop_name),
1001 Value::Object(ref obj) if obj.is_empty() => {
1002 Self::generate_example_for_property(prop_name)
1003 }
1004 _ => value,
1005 };
1006 map.insert(prop_name.clone(), value);
1007 }
1008
1009 if let Some((total, page, limit)) = pagination_metadata {
1011 map.insert("total".to_string(), Value::Number(total.into()));
1012 map.insert("page".to_string(), Value::Number(page.into()));
1013 map.insert("limit".to_string(), Value::Number(limit.into()));
1014 }
1015
1016 Value::Object(map)
1017 }
1018 openapiv3::SchemaKind::Type(openapiv3::Type::Array(arr)) => {
1019 match &arr.items {
1025 Some(item_schema) => {
1026 let example_item = match item_schema {
1027 ReferenceOr::Item(item_schema) => {
1028 Self::generate_example_from_schema(
1031 spec,
1032 item_schema.as_ref(),
1033 persona,
1034 )
1035 }
1036 ReferenceOr::Reference { reference } => {
1037 if let Some(resolved_schema) = spec.get_schema(reference) {
1040 Self::generate_example_from_schema(
1041 spec,
1042 &resolved_schema.schema,
1043 persona,
1044 )
1045 } else {
1046 Value::Object(serde_json::Map::new())
1047 }
1048 }
1049 };
1050 Value::Array(vec![example_item])
1051 }
1052 None => Value::Array(vec![Value::String("item".to_string())]),
1053 }
1054 }
1055 _ => Value::Object(serde_json::Map::new()),
1056 }
1057 }
1058
1059 fn extract_numeric_value_from_schema(schema_ref: &ReferenceOr<Schema>) -> Option<u64> {
1062 match schema_ref {
1063 ReferenceOr::Item(schema) => {
1064 if let Some(example) = schema.schema_data.example.as_ref() {
1066 if let Some(num) = example.as_u64() {
1067 return Some(num);
1068 } else if let Some(num) = example.as_f64() {
1069 return Some(num as u64);
1070 }
1071 }
1072 if let Some(default) = schema.schema_data.default.as_ref() {
1074 if let Some(num) = default.as_u64() {
1075 return Some(num);
1076 } else if let Some(num) = default.as_f64() {
1077 return Some(num as u64);
1078 }
1079 }
1080 None
1084 }
1085 ReferenceOr::Reference { reference: _ } => {
1086 None
1089 }
1090 }
1091 }
1092
1093 fn generate_array_with_count(
1096 spec: &OpenApiSpec,
1097 array_schema: &Schema,
1098 pagination: (u64, u64, u64), persona: Option<&Persona>,
1100 ) -> Value {
1101 let (total, _page, limit) = pagination;
1102
1103 let count = std::cmp::min(total, limit);
1106
1107 let max_items = 100;
1109 let count = std::cmp::min(count, max_items);
1110
1111 tracing::debug!("Generating array with count={} (total={}, limit={})", count, total, limit);
1112
1113 if let Some(example) = array_schema.schema_data.example.as_ref() {
1115 if let Some(example_array) = example.as_array() {
1116 if !example_array.is_empty() {
1117 let template_item = &example_array[0];
1119 let items: Vec<Value> = (0..count)
1120 .map(|i| {
1121 let mut item = template_item.clone();
1123 Self::add_item_variation(&mut item, i + 1);
1124 item
1125 })
1126 .collect();
1127 return Value::Array(items);
1128 }
1129 }
1130 }
1131
1132 if let openapiv3::SchemaKind::Type(openapiv3::Type::Array(arr)) = &array_schema.schema_kind
1134 {
1135 if let Some(item_schema) = &arr.items {
1136 let items: Vec<Value> = match item_schema {
1137 ReferenceOr::Item(item_schema) => {
1138 (0..count)
1139 .map(|i| {
1140 let mut item = Self::generate_example_from_schema(
1141 spec,
1142 item_schema.as_ref(),
1143 persona,
1144 );
1145 Self::add_item_variation(&mut item, i + 1);
1147 item
1148 })
1149 .collect()
1150 }
1151 ReferenceOr::Reference { reference } => {
1152 if let Some(resolved_schema) = spec.get_schema(reference) {
1153 (0..count)
1154 .map(|i| {
1155 let mut item = Self::generate_example_from_schema(
1156 spec,
1157 &resolved_schema.schema,
1158 persona,
1159 );
1160 Self::add_item_variation(&mut item, i + 1);
1162 item
1163 })
1164 .collect()
1165 } else {
1166 vec![Value::Object(serde_json::Map::new()); count as usize]
1167 }
1168 }
1169 };
1170 return Value::Array(items);
1171 }
1172 }
1173
1174 Value::Array((0..count).map(|i| Value::String(format!("item_{}", i + 1))).collect())
1176 }
1177
1178 fn add_item_variation(item: &mut Value, item_index: u64) {
1181 if let Some(obj) = item.as_object_mut() {
1182 if let Some(id_val) = obj.get_mut("id") {
1184 if let Some(id_str) = id_val.as_str() {
1185 let base_id = id_str.split('_').next().unwrap_or(id_str);
1187 *id_val = Value::String(format!("{}_{:03}", base_id, item_index));
1188 } else if let Some(id_num) = id_val.as_u64() {
1189 *id_val = Value::Number((id_num + item_index).into());
1190 }
1191 }
1192
1193 if let Some(name_val) = obj.get_mut("name") {
1195 if let Some(name_str) = name_val.as_str() {
1196 if name_str.contains('#') {
1197 *name_val = Value::String(format!("Hive #{}", item_index));
1199 } else {
1200 let apiary_names = [
1203 "Meadow Apiary",
1205 "Prairie Apiary",
1206 "Sunset Valley Apiary",
1207 "Golden Fields Apiary",
1208 "Miller Family Apiary",
1209 "Heartland Honey Co.",
1210 "Cornfield Apiary",
1211 "Harvest Moon Apiary",
1212 "Prairie Winds Apiary",
1213 "Amber Fields Apiary",
1214 "Coastal Apiary",
1216 "Sunset Coast Apiary",
1217 "Pacific Grove Apiary",
1218 "Golden Gate Apiary",
1219 "Napa Valley Apiary",
1220 "Coastal Breeze Apiary",
1221 "Pacific Heights Apiary",
1222 "Bay Area Apiary",
1223 "Sunset Valley Honey Co.",
1224 "Coastal Harvest Apiary",
1225 "Lone Star Apiary",
1227 "Texas Ranch Apiary",
1228 "Big Sky Apiary",
1229 "Prairie Rose Apiary",
1230 "Hill Country Apiary",
1231 "Lone Star Honey Co.",
1232 "Texas Pride Apiary",
1233 "Wildflower Ranch",
1234 "Desert Bloom Apiary",
1235 "Cactus Creek Apiary",
1236 "Orange Grove Apiary",
1238 "Citrus Grove Apiary",
1239 "Palm Grove Apiary",
1240 "Tropical Breeze Apiary",
1241 "Everglades Apiary",
1242 "Sunshine State Apiary",
1243 "Florida Keys Apiary",
1244 "Grove View Apiary",
1245 "Tropical Harvest Apiary",
1246 "Palm Coast Apiary",
1247 "Mountain View Apiary",
1249 "Valley Apiary",
1250 "Riverside Apiary",
1251 "Hilltop Apiary",
1252 "Forest Apiary",
1253 "Mountain Apiary",
1254 "Lakeside Apiary",
1255 "Ridge Apiary",
1256 "Brook Apiary",
1257 "Hillside Apiary",
1258 "Field Apiary",
1260 "Creek Apiary",
1261 "Woodland Apiary",
1262 "Farm Apiary",
1263 "Orchard Apiary",
1264 "Pasture Apiary",
1265 "Green Valley Apiary",
1266 "Blue Sky Apiary",
1267 "Sweet Honey Apiary",
1268 "Nature's Best Apiary",
1269 "Premium Honey Co.",
1271 "Artisan Apiary",
1272 "Heritage Apiary",
1273 "Summit Apiary",
1274 "Crystal Springs Apiary",
1275 "Maple Grove Apiary",
1276 "Wildflower Apiary",
1277 "Thistle Apiary",
1278 "Clover Field Apiary",
1279 "Honeycomb Apiary",
1280 ];
1281 let name_index = (item_index - 1) as usize % apiary_names.len();
1282 *name_val = Value::String(apiary_names[name_index].to_string());
1283 }
1284 }
1285 }
1286
1287 if let Some(location_val) = obj.get_mut("location") {
1289 if let Some(location_obj) = location_val.as_object_mut() {
1290 if let Some(address_val) = location_obj.get_mut("address") {
1292 if let Some(address_str) = address_val.as_str() {
1293 if let Some(num_str) = address_str.split_whitespace().next() {
1295 if let Ok(num) = num_str.parse::<u64>() {
1296 *address_val =
1297 Value::String(format!("{} Farm Road", num + item_index));
1298 } else {
1299 *address_val =
1300 Value::String(format!("{} Farm Road", 100 + item_index));
1301 }
1302 } else {
1303 *address_val =
1304 Value::String(format!("{} Farm Road", 100 + item_index));
1305 }
1306 }
1307 }
1308
1309 if let Some(lat_val) = location_obj.get_mut("latitude") {
1311 if let Some(lat) = lat_val.as_f64() {
1312 *lat_val = Value::Number(
1313 serde_json::Number::from_f64(lat + (item_index as f64 * 0.01))
1314 .expect("latitude arithmetic produces valid f64"),
1315 );
1316 }
1317 }
1318 if let Some(lng_val) = location_obj.get_mut("longitude") {
1319 if let Some(lng) = lng_val.as_f64() {
1320 *lng_val = Value::Number(
1321 serde_json::Number::from_f64(lng + (item_index as f64 * 0.01))
1322 .expect("longitude arithmetic produces valid f64"),
1323 );
1324 }
1325 }
1326 } else if let Some(address_str) = location_val.as_str() {
1327 if let Some(num_str) = address_str.split_whitespace().next() {
1329 if let Ok(num) = num_str.parse::<u64>() {
1330 *location_val =
1331 Value::String(format!("{} Farm Road", num + item_index));
1332 } else {
1333 *location_val =
1334 Value::String(format!("{} Farm Road", 100 + item_index));
1335 }
1336 }
1337 }
1338 }
1339
1340 if let Some(address_val) = obj.get_mut("address") {
1342 if let Some(address_str) = address_val.as_str() {
1343 if let Some(num_str) = address_str.split_whitespace().next() {
1344 if let Ok(num) = num_str.parse::<u64>() {
1345 *address_val = Value::String(format!("{} Farm Road", num + item_index));
1346 } else {
1347 *address_val = Value::String(format!("{} Farm Road", 100 + item_index));
1348 }
1349 }
1350 }
1351 }
1352
1353 if let Some(status_val) = obj.get_mut("status") {
1355 if let Some(status_str) = status_val.as_str() {
1356 let statuses = [
1357 "healthy",
1358 "sick",
1359 "needs_attention",
1360 "quarantined",
1361 "active",
1362 "inactive",
1363 ];
1364 let status_index = (item_index - 1) as usize % statuses.len();
1365 let final_status = if (item_index - 1) % 10 < 7 {
1367 statuses[0] } else {
1369 statuses[status_index]
1370 };
1371 *status_val = Value::String(final_status.to_string());
1372 }
1373 }
1374
1375 if let Some(hive_type_val) = obj.get_mut("hive_type") {
1377 if hive_type_val.as_str().is_some() {
1378 let hive_types = ["langstroth", "top_bar", "warre", "flow_hive", "national"];
1379 let type_index = (item_index - 1) as usize % hive_types.len();
1380 *hive_type_val = Value::String(hive_types[type_index].to_string());
1381 }
1382 }
1383
1384 if let Some(queen_val) = obj.get_mut("queen") {
1386 if let Some(queen_obj) = queen_val.as_object_mut() {
1387 if let Some(breed_val) = queen_obj.get_mut("breed") {
1388 if breed_val.as_str().is_some() {
1389 let breeds =
1390 ["italian", "carniolan", "russian", "buckfast", "caucasian"];
1391 let breed_index = (item_index - 1) as usize % breeds.len();
1392 *breed_val = Value::String(breeds[breed_index].to_string());
1393 }
1394 }
1395 if let Some(age_val) = queen_obj.get_mut("age_days") {
1397 if let Some(base_age) = age_val.as_u64() {
1398 *age_val = Value::Number((base_age + (item_index * 10) % 200).into());
1399 } else if let Some(base_age) = age_val.as_i64() {
1400 *age_val =
1401 Value::Number((base_age + (item_index as i64 * 10) % 200).into());
1402 }
1403 }
1404 if let Some(color_val) = queen_obj.get_mut("mark_color") {
1406 if color_val.as_str().is_some() {
1407 let colors = ["yellow", "white", "red", "green", "blue"];
1408 let color_index = (item_index - 1) as usize % colors.len();
1409 *color_val = Value::String(colors[color_index].to_string());
1410 }
1411 }
1412 }
1413 }
1414
1415 if let Some(desc_val) = obj.get_mut("description") {
1417 if let Some(desc_str) = desc_val.as_str() {
1418 let descriptions = [
1419 "Production apiary",
1420 "Research apiary",
1421 "Commercial operation",
1422 "Backyard apiary",
1423 "Educational apiary",
1424 ];
1425 let desc_index = (item_index - 1) as usize % descriptions.len();
1426 *desc_val = Value::String(descriptions[desc_index].to_string());
1427 }
1428 }
1429
1430 let timestamp_fields = [
1433 "created_at",
1434 "updated_at",
1435 "timestamp",
1436 "date",
1437 "forecastDate",
1438 "predictedDate",
1439 ];
1440 for field_name in ×tamp_fields {
1441 if let Some(timestamp_val) = obj.get_mut(*field_name) {
1442 if let Some(_timestamp_str) = timestamp_val.as_str() {
1443 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;
1453 let base_month = 11;
1454
1455 let target_year = if months_ago >= base_month as u64 {
1457 base_year - 1
1458 } else {
1459 base_year
1460 };
1461 let target_month = if months_ago >= base_month as u64 {
1462 12 - (months_ago - base_month as u64) as u8
1463 } else {
1464 (base_month as u64 - months_ago) as u8
1465 };
1466 let target_day = std::cmp::min(28, 1 + days_offset as u8); let timestamp = format!(
1470 "{:04}-{:02}-{:02}T{:02}:{:02}:00Z",
1471 target_year, target_month, target_day, hours_offset, minutes_offset
1472 );
1473 *timestamp_val = Value::String(timestamp);
1474 }
1475 }
1476 }
1477 }
1478 }
1479
1480 fn try_infer_total_from_context(
1483 spec: &OpenApiSpec,
1484 obj_type: &openapiv3::ObjectType,
1485 ) -> Option<u64> {
1486 if let Some(items_schema_ref) = obj_type.properties.get("items") {
1488 if let Some(components) = &spec.spec.components {
1491 let schemas = &components.schemas;
1492 for (schema_name, schema_ref) in schemas {
1495 if let ReferenceOr::Item(schema) = schema_ref {
1496 if let openapiv3::SchemaKind::Type(openapiv3::Type::Object(obj)) =
1497 &schema.schema_kind
1498 {
1499 for (prop_name, prop_schema) in &obj.properties {
1501 let prop_lower = prop_name.to_lowercase();
1502 if prop_lower.ends_with("_count") {
1503 let schema_ref: ReferenceOr<Schema> = match prop_schema {
1505 ReferenceOr::Item(boxed) => {
1506 ReferenceOr::Item(boxed.as_ref().clone())
1507 }
1508 ReferenceOr::Reference { reference } => {
1509 ReferenceOr::Reference {
1510 reference: reference.clone(),
1511 }
1512 }
1513 };
1514 if let Some(count) =
1516 Self::extract_numeric_value_from_schema(&schema_ref)
1517 {
1518 if count > 0 && count <= 1000 {
1520 tracing::debug!(
1521 "Inferred count {} from parent schema {} field {}",
1522 count,
1523 schema_name,
1524 prop_name
1525 );
1526 return Some(count);
1527 }
1528 }
1529 }
1530 }
1531 }
1532 }
1533 }
1534 }
1535 }
1536
1537 None
1538 }
1539
1540 fn infer_count_from_parent_schema(
1543 spec: &OpenApiSpec,
1544 parent_entity_name: &str,
1545 child_entity_name: &str,
1546 ) -> Option<u64> {
1547 let parent_schema_name = parent_entity_name.to_string();
1549 let count_field_name = format!("{}_count", child_entity_name);
1550
1551 if let Some(components) = &spec.spec.components {
1553 let schemas = &components.schemas;
1554 for (schema_name, schema_ref) in schemas {
1556 let schema_name_lower = schema_name.to_lowercase();
1557 if schema_name_lower.contains(&parent_entity_name.to_lowercase()) {
1558 if let ReferenceOr::Item(schema) = schema_ref {
1559 if let openapiv3::SchemaKind::Type(openapiv3::Type::Object(obj)) =
1561 &schema.schema_kind
1562 {
1563 for (prop_name, prop_schema) in &obj.properties {
1564 if prop_name.to_lowercase() == count_field_name.to_lowercase() {
1565 let schema_ref: ReferenceOr<Schema> = match prop_schema {
1567 ReferenceOr::Item(boxed) => {
1568 ReferenceOr::Item(boxed.as_ref().clone())
1569 }
1570 ReferenceOr::Reference { reference } => {
1571 ReferenceOr::Reference {
1572 reference: reference.clone(),
1573 }
1574 }
1575 };
1576 return Self::extract_numeric_value_from_schema(&schema_ref);
1578 }
1579 }
1580 }
1581 }
1582 }
1583 }
1584 }
1585
1586 None
1587 }
1588
1589 fn generate_example_for_property(prop_name: &str) -> Value {
1591 let prop_lower = prop_name.to_lowercase();
1592
1593 if prop_lower.contains("id") || prop_lower.contains("uuid") {
1595 Value::String(uuid::Uuid::new_v4().to_string())
1596 } else if prop_lower.contains("email") {
1597 Value::String(format!("user{}@example.com", thread_rng().random_range(1000..=9999)))
1598 } else if prop_lower.contains("name") || prop_lower.contains("title") {
1599 let names = ["John Doe", "Jane Smith", "Bob Johnson", "Alice Brown"];
1600 Value::String(names[thread_rng().random_range(0..names.len())].to_string())
1601 } else if prop_lower.contains("phone") || prop_lower.contains("mobile") {
1602 Value::String(format!("+1-555-{:04}", thread_rng().random_range(1000..=9999)))
1603 } else if prop_lower.contains("address") || prop_lower.contains("street") {
1604 let streets = ["123 Main St", "456 Oak Ave", "789 Pine Rd", "321 Elm St"];
1605 Value::String(streets[thread_rng().random_range(0..streets.len())].to_string())
1606 } else if prop_lower.contains("city") {
1607 let cities = ["New York", "London", "Tokyo", "Paris", "Sydney"];
1608 Value::String(cities[thread_rng().random_range(0..cities.len())].to_string())
1609 } else if prop_lower.contains("country") {
1610 let countries = ["USA", "UK", "Japan", "France", "Australia"];
1611 Value::String(countries[thread_rng().random_range(0..countries.len())].to_string())
1612 } else if prop_lower.contains("company") || prop_lower.contains("organization") {
1613 let companies = ["Acme Corp", "Tech Solutions", "Global Inc", "Innovate Ltd"];
1614 Value::String(companies[thread_rng().random_range(0..companies.len())].to_string())
1615 } else if prop_lower.contains("url") || prop_lower.contains("website") {
1616 Value::String("https://example.com".to_string())
1617 } else if prop_lower.contains("age") {
1618 Value::Number((18 + thread_rng().random_range(0..60)).into())
1619 } else if prop_lower.contains("count") || prop_lower.contains("quantity") {
1620 Value::Number((1 + thread_rng().random_range(0..100)).into())
1621 } else if prop_lower.contains("price")
1622 || prop_lower.contains("amount")
1623 || prop_lower.contains("cost")
1624 {
1625 Value::Number(
1626 serde_json::Number::from_f64(
1627 (thread_rng().random::<f64>() * 1000.0 * 100.0).round() / 100.0,
1628 )
1629 .expect("rounded price calculation produces valid f64"),
1630 )
1631 } else if prop_lower.contains("active")
1632 || prop_lower.contains("enabled")
1633 || prop_lower.contains("is_")
1634 {
1635 Value::Bool(thread_rng().random_bool(0.5))
1636 } else if prop_lower.contains("date") || prop_lower.contains("time") {
1637 Value::String(chrono::Utc::now().to_rfc3339())
1638 } else if prop_lower.contains("description") || prop_lower.contains("comment") {
1639 Value::String("This is a sample description text.".to_string())
1640 } else {
1641 Value::String(format!("example {}", prop_name))
1642 }
1643 }
1644
1645 pub fn generate_from_examples(
1647 response: &Response,
1648 content_type: Option<&str>,
1649 ) -> Result<Option<Value>> {
1650 use openapiv3::ReferenceOr;
1651
1652 if let Some(content_type) = content_type {
1654 if let Some(media_type) = response.content.get(content_type) {
1655 if let Some(example) = &media_type.example {
1657 return Ok(Some(example.clone()));
1658 }
1659
1660 for (_, example_ref) in &media_type.examples {
1662 if let ReferenceOr::Item(example) = example_ref {
1663 if let Some(value) = &example.value {
1664 return Ok(Some(value.clone()));
1665 }
1666 }
1667 }
1669 }
1670 }
1671
1672 for (_, media_type) in &response.content {
1674 if let Some(example) = &media_type.example {
1676 return Ok(Some(example.clone()));
1677 }
1678
1679 for (_, example_ref) in &media_type.examples {
1681 if let ReferenceOr::Item(example) = example_ref {
1682 if let Some(value) = &example.value {
1683 return Ok(Some(value.clone()));
1684 }
1685 }
1686 }
1688 }
1689
1690 Ok(None)
1691 }
1692
1693 fn expand_templates(value: &Value) -> Value {
1695 match value {
1696 Value::String(s) => {
1697 let expanded = s
1698 .replace("{{now}}", &chrono::Utc::now().to_rfc3339())
1699 .replace("{{uuid}}", &uuid::Uuid::new_v4().to_string());
1700 Value::String(expanded)
1701 }
1702 Value::Object(map) => {
1703 let mut new_map = serde_json::Map::new();
1704 for (key, val) in map {
1705 new_map.insert(key.clone(), Self::expand_templates(val));
1706 }
1707 Value::Object(new_map)
1708 }
1709 Value::Array(arr) => {
1710 let new_arr: Vec<Value> = arr.iter().map(Self::expand_templates).collect();
1711 Value::Array(new_arr)
1712 }
1713 _ => value.clone(),
1714 }
1715 }
1716}
1717
1718#[cfg(test)]
1719mod tests {
1720 use super::*;
1721 use openapiv3::ReferenceOr;
1722 use serde_json::json;
1723
1724 struct MockAiGenerator {
1726 response: Value,
1727 }
1728
1729 #[async_trait]
1730 impl AiGenerator for MockAiGenerator {
1731 async fn generate(&self, _prompt: &str, _config: &AiResponseConfig) -> Result<Value> {
1732 Ok(self.response.clone())
1733 }
1734 }
1735
1736 #[test]
1737 fn generates_example_using_referenced_schemas() {
1738 let yaml = r#"
1739openapi: 3.0.3
1740info:
1741 title: Test API
1742 version: "1.0.0"
1743paths:
1744 /apiaries:
1745 get:
1746 responses:
1747 '200':
1748 description: ok
1749 content:
1750 application/json:
1751 schema:
1752 $ref: '#/components/schemas/Apiary'
1753components:
1754 schemas:
1755 Apiary:
1756 type: object
1757 properties:
1758 id:
1759 type: string
1760 hive:
1761 $ref: '#/components/schemas/Hive'
1762 Hive:
1763 type: object
1764 properties:
1765 name:
1766 type: string
1767 active:
1768 type: boolean
1769 "#;
1770
1771 let spec = OpenApiSpec::from_string(yaml, Some("yaml")).expect("load spec");
1772 let path_item = spec
1773 .spec
1774 .paths
1775 .paths
1776 .get("/apiaries")
1777 .and_then(ReferenceOr::as_item)
1778 .expect("path item");
1779 let operation = path_item.get.as_ref().expect("GET operation");
1780
1781 let response =
1782 ResponseGenerator::generate_response(&spec, operation, 200, Some("application/json"))
1783 .expect("generate response");
1784
1785 let obj = response.as_object().expect("response object");
1786 assert!(obj.contains_key("id"));
1787 let hive = obj.get("hive").and_then(|value| value.as_object()).expect("hive object");
1788 assert!(hive.contains_key("name"));
1789 assert!(hive.contains_key("active"));
1790 }
1791
1792 #[tokio::test]
1793 async fn test_generate_ai_response_with_generator() {
1794 let ai_config = AiResponseConfig {
1795 enabled: true,
1796 mode: crate::ai_response::AiResponseMode::Intelligent,
1797 prompt: Some("Generate a response for {{method}} {{path}}".to_string()),
1798 context: None,
1799 temperature: 0.7,
1800 max_tokens: 1000,
1801 schema: None,
1802 cache_enabled: true,
1803 };
1804 let context = RequestContext {
1805 method: "GET".to_string(),
1806 path: "/api/users".to_string(),
1807 path_params: HashMap::new(),
1808 query_params: HashMap::new(),
1809 headers: HashMap::new(),
1810 body: None,
1811 multipart_fields: HashMap::new(),
1812 multipart_files: HashMap::new(),
1813 };
1814 let mock_generator = MockAiGenerator {
1815 response: json!({"message": "Generated response"}),
1816 };
1817
1818 let result =
1819 ResponseGenerator::generate_ai_response(&ai_config, &context, Some(&mock_generator))
1820 .await;
1821
1822 assert!(result.is_ok());
1823 let value = result.unwrap();
1824 assert_eq!(value["message"], "Generated response");
1825 }
1826
1827 #[tokio::test]
1828 async fn test_generate_ai_response_without_generator() {
1829 let ai_config = AiResponseConfig {
1830 enabled: true,
1831 mode: crate::ai_response::AiResponseMode::Intelligent,
1832 prompt: Some("Generate a response for {{method}} {{path}}".to_string()),
1833 context: None,
1834 temperature: 0.7,
1835 max_tokens: 1000,
1836 schema: None,
1837 cache_enabled: true,
1838 };
1839 let context = RequestContext {
1840 method: "POST".to_string(),
1841 path: "/api/users".to_string(),
1842 path_params: HashMap::new(),
1843 query_params: HashMap::new(),
1844 headers: HashMap::new(),
1845 body: None,
1846 multipart_fields: HashMap::new(),
1847 multipart_files: HashMap::new(),
1848 };
1849
1850 let result = ResponseGenerator::generate_ai_response(&ai_config, &context, None).await;
1851
1852 assert!(result.is_ok());
1853 let value = result.unwrap();
1854 assert_eq!(value["ai_response"], "AI generation placeholder");
1855 assert!(value["expanded_prompt"].as_str().unwrap().contains("POST"));
1856 assert!(value["expanded_prompt"].as_str().unwrap().contains("/api/users"));
1857 }
1858
1859 #[tokio::test]
1860 async fn test_generate_ai_response_no_prompt() {
1861 let ai_config = AiResponseConfig {
1862 enabled: true,
1863 mode: crate::ai_response::AiResponseMode::Intelligent,
1864 prompt: None,
1865 context: None,
1866 temperature: 0.7,
1867 max_tokens: 1000,
1868 schema: None,
1869 cache_enabled: true,
1870 };
1871 let context = RequestContext {
1872 method: "GET".to_string(),
1873 path: "/api/test".to_string(),
1874 path_params: HashMap::new(),
1875 query_params: HashMap::new(),
1876 headers: HashMap::new(),
1877 body: None,
1878 multipart_fields: HashMap::new(),
1879 multipart_files: HashMap::new(),
1880 };
1881
1882 let result = ResponseGenerator::generate_ai_response(&ai_config, &context, None).await;
1883
1884 assert!(result.is_err());
1885 }
1886
1887 #[test]
1888 fn test_generate_response_with_expansion() {
1889 let spec = OpenApiSpec::from_string(
1890 r#"openapi: 3.0.0
1891info:
1892 title: Test API
1893 version: 1.0.0
1894paths:
1895 /users:
1896 get:
1897 responses:
1898 '200':
1899 description: OK
1900 content:
1901 application/json:
1902 schema:
1903 type: object
1904 properties:
1905 id:
1906 type: integer
1907 name:
1908 type: string
1909"#,
1910 Some("yaml"),
1911 )
1912 .unwrap();
1913
1914 let operation = spec
1915 .spec
1916 .paths
1917 .paths
1918 .get("/users")
1919 .and_then(|p| p.as_item())
1920 .and_then(|p| p.get.as_ref())
1921 .unwrap();
1922
1923 let response = ResponseGenerator::generate_response_with_expansion(
1924 &spec,
1925 operation,
1926 200,
1927 Some("application/json"),
1928 true,
1929 )
1930 .unwrap();
1931
1932 assert!(response.is_object());
1933 }
1934
1935 #[test]
1936 fn test_generate_response_with_scenario() {
1937 let spec = OpenApiSpec::from_string(
1938 r#"openapi: 3.0.0
1939info:
1940 title: Test API
1941 version: 1.0.0
1942paths:
1943 /users:
1944 get:
1945 responses:
1946 '200':
1947 description: OK
1948 content:
1949 application/json:
1950 examples:
1951 happy:
1952 value:
1953 id: 1
1954 name: "Happy User"
1955 sad:
1956 value:
1957 id: 2
1958 name: "Sad User"
1959"#,
1960 Some("yaml"),
1961 )
1962 .unwrap();
1963
1964 let operation = spec
1965 .spec
1966 .paths
1967 .paths
1968 .get("/users")
1969 .and_then(|p| p.as_item())
1970 .and_then(|p| p.get.as_ref())
1971 .unwrap();
1972
1973 let response = ResponseGenerator::generate_response_with_scenario(
1974 &spec,
1975 operation,
1976 200,
1977 Some("application/json"),
1978 false,
1979 Some("happy"),
1980 )
1981 .unwrap();
1982
1983 assert_eq!(response["id"], 1);
1984 assert_eq!(response["name"], "Happy User");
1985 }
1986
1987 #[test]
1988 fn test_generate_response_with_referenced_response() {
1989 let spec = OpenApiSpec::from_string(
1990 r#"openapi: 3.0.0
1991info:
1992 title: Test API
1993 version: 1.0.0
1994paths:
1995 /users:
1996 get:
1997 responses:
1998 '200':
1999 $ref: '#/components/responses/UserResponse'
2000components:
2001 responses:
2002 UserResponse:
2003 description: User response
2004 content:
2005 application/json:
2006 schema:
2007 type: object
2008 properties:
2009 id:
2010 type: integer
2011 name:
2012 type: string
2013"#,
2014 Some("yaml"),
2015 )
2016 .unwrap();
2017
2018 let operation = spec
2019 .spec
2020 .paths
2021 .paths
2022 .get("/users")
2023 .and_then(|p| p.as_item())
2024 .and_then(|p| p.get.as_ref())
2025 .unwrap();
2026
2027 let response =
2028 ResponseGenerator::generate_response(&spec, operation, 200, Some("application/json"))
2029 .unwrap();
2030
2031 assert!(response.is_object());
2032 }
2033
2034 #[test]
2035 fn test_generate_response_with_default_status() {
2036 let spec = OpenApiSpec::from_string(
2037 r#"openapi: 3.0.0
2038info:
2039 title: Test API
2040 version: 1.0.0
2041paths:
2042 /users:
2043 get:
2044 responses:
2045 '200':
2046 description: OK
2047 default:
2048 description: Error
2049 content:
2050 application/json:
2051 schema:
2052 type: object
2053 properties:
2054 error:
2055 type: string
2056"#,
2057 Some("yaml"),
2058 )
2059 .unwrap();
2060
2061 let operation = spec
2062 .spec
2063 .paths
2064 .paths
2065 .get("/users")
2066 .and_then(|p| p.as_item())
2067 .and_then(|p| p.get.as_ref())
2068 .unwrap();
2069
2070 let response =
2072 ResponseGenerator::generate_response(&spec, operation, 500, Some("application/json"))
2073 .unwrap();
2074
2075 assert!(response.is_object());
2076 }
2077
2078 #[test]
2079 fn test_generate_response_with_example_in_media_type() {
2080 let spec = OpenApiSpec::from_string(
2081 r#"openapi: 3.0.0
2082info:
2083 title: Test API
2084 version: 1.0.0
2085paths:
2086 /users:
2087 get:
2088 responses:
2089 '200':
2090 description: OK
2091 content:
2092 application/json:
2093 example:
2094 id: 1
2095 name: "Example User"
2096"#,
2097 Some("yaml"),
2098 )
2099 .unwrap();
2100
2101 let operation = spec
2102 .spec
2103 .paths
2104 .paths
2105 .get("/users")
2106 .and_then(|p| p.as_item())
2107 .and_then(|p| p.get.as_ref())
2108 .unwrap();
2109
2110 let response =
2111 ResponseGenerator::generate_response(&spec, operation, 200, Some("application/json"))
2112 .unwrap();
2113
2114 assert_eq!(response["id"], 1);
2115 assert_eq!(response["name"], "Example User");
2116 }
2117
2118 #[test]
2119 fn test_generate_response_with_schema_example() {
2120 let spec = OpenApiSpec::from_string(
2121 r#"openapi: 3.0.0
2122info:
2123 title: Test API
2124 version: 1.0.0
2125paths:
2126 /users:
2127 get:
2128 responses:
2129 '200':
2130 description: OK
2131 content:
2132 application/json:
2133 schema:
2134 type: object
2135 example:
2136 id: 42
2137 name: "Schema Example"
2138 properties:
2139 id:
2140 type: integer
2141 name:
2142 type: string
2143"#,
2144 Some("yaml"),
2145 )
2146 .unwrap();
2147
2148 let operation = spec
2149 .spec
2150 .paths
2151 .paths
2152 .get("/users")
2153 .and_then(|p| p.as_item())
2154 .and_then(|p| p.get.as_ref())
2155 .unwrap();
2156
2157 let response =
2158 ResponseGenerator::generate_response(&spec, operation, 200, Some("application/json"))
2159 .unwrap();
2160
2161 assert!(response.is_object());
2163 }
2164
2165 #[test]
2166 fn test_generate_response_with_referenced_schema() {
2167 let spec = OpenApiSpec::from_string(
2168 r#"openapi: 3.0.0
2169info:
2170 title: Test API
2171 version: 1.0.0
2172paths:
2173 /users:
2174 get:
2175 responses:
2176 '200':
2177 description: OK
2178 content:
2179 application/json:
2180 schema:
2181 $ref: '#/components/schemas/User'
2182components:
2183 schemas:
2184 User:
2185 type: object
2186 properties:
2187 id:
2188 type: integer
2189 name:
2190 type: string
2191"#,
2192 Some("yaml"),
2193 )
2194 .unwrap();
2195
2196 let operation = spec
2197 .spec
2198 .paths
2199 .paths
2200 .get("/users")
2201 .and_then(|p| p.as_item())
2202 .and_then(|p| p.get.as_ref())
2203 .unwrap();
2204
2205 let response =
2206 ResponseGenerator::generate_response(&spec, operation, 200, Some("application/json"))
2207 .unwrap();
2208
2209 assert!(response.is_object());
2210 assert!(response.get("id").is_some());
2211 assert!(response.get("name").is_some());
2212 }
2213
2214 #[test]
2215 fn test_generate_response_with_array_schema() {
2216 let spec = OpenApiSpec::from_string(
2217 r#"openapi: 3.0.0
2218info:
2219 title: Test API
2220 version: 1.0.0
2221paths:
2222 /users:
2223 get:
2224 responses:
2225 '200':
2226 description: OK
2227 content:
2228 application/json:
2229 schema:
2230 type: array
2231 items:
2232 type: object
2233 properties:
2234 id:
2235 type: integer
2236 name:
2237 type: string
2238"#,
2239 Some("yaml"),
2240 )
2241 .unwrap();
2242
2243 let operation = spec
2244 .spec
2245 .paths
2246 .paths
2247 .get("/users")
2248 .and_then(|p| p.as_item())
2249 .and_then(|p| p.get.as_ref())
2250 .unwrap();
2251
2252 let response =
2253 ResponseGenerator::generate_response(&spec, operation, 200, Some("application/json"))
2254 .unwrap();
2255
2256 assert!(response.is_array());
2257 }
2258
2259 #[test]
2260 fn test_generate_response_with_different_content_types() {
2261 let spec = OpenApiSpec::from_string(
2262 r#"openapi: 3.0.0
2263info:
2264 title: Test API
2265 version: 1.0.0
2266paths:
2267 /users:
2268 get:
2269 responses:
2270 '200':
2271 description: OK
2272 content:
2273 application/json:
2274 schema:
2275 type: object
2276 text/plain:
2277 schema:
2278 type: string
2279"#,
2280 Some("yaml"),
2281 )
2282 .unwrap();
2283
2284 let operation = spec
2285 .spec
2286 .paths
2287 .paths
2288 .get("/users")
2289 .and_then(|p| p.as_item())
2290 .and_then(|p| p.get.as_ref())
2291 .unwrap();
2292
2293 let json_response =
2295 ResponseGenerator::generate_response(&spec, operation, 200, Some("application/json"))
2296 .unwrap();
2297 assert!(json_response.is_object());
2298
2299 let text_response =
2301 ResponseGenerator::generate_response(&spec, operation, 200, Some("text/plain"))
2302 .unwrap();
2303 assert!(text_response.is_string());
2304 }
2305
2306 #[test]
2307 fn test_generate_response_without_content_type() {
2308 let spec = OpenApiSpec::from_string(
2309 r#"openapi: 3.0.0
2310info:
2311 title: Test API
2312 version: 1.0.0
2313paths:
2314 /users:
2315 get:
2316 responses:
2317 '200':
2318 description: OK
2319 content:
2320 application/json:
2321 schema:
2322 type: object
2323 properties:
2324 id:
2325 type: integer
2326"#,
2327 Some("yaml"),
2328 )
2329 .unwrap();
2330
2331 let operation = spec
2332 .spec
2333 .paths
2334 .paths
2335 .get("/users")
2336 .and_then(|p| p.as_item())
2337 .and_then(|p| p.get.as_ref())
2338 .unwrap();
2339
2340 let response = ResponseGenerator::generate_response(&spec, operation, 200, None).unwrap();
2342
2343 assert!(response.is_object());
2344 }
2345
2346 #[test]
2347 fn test_generate_response_with_no_content() {
2348 let spec = OpenApiSpec::from_string(
2349 r#"openapi: 3.0.0
2350info:
2351 title: Test API
2352 version: 1.0.0
2353paths:
2354 /users:
2355 delete:
2356 responses:
2357 '204':
2358 description: No Content
2359"#,
2360 Some("yaml"),
2361 )
2362 .unwrap();
2363
2364 let operation = spec
2365 .spec
2366 .paths
2367 .paths
2368 .get("/users")
2369 .and_then(|p| p.as_item())
2370 .and_then(|p| p.delete.as_ref())
2371 .unwrap();
2372
2373 let response = ResponseGenerator::generate_response(&spec, operation, 204, None).unwrap();
2374
2375 assert!(response.is_object());
2377 assert!(response.as_object().unwrap().is_empty());
2378 }
2379
2380 #[test]
2381 fn test_generate_response_with_expansion_disabled() {
2382 let spec = OpenApiSpec::from_string(
2383 r#"openapi: 3.0.0
2384info:
2385 title: Test API
2386 version: 1.0.0
2387paths:
2388 /users:
2389 get:
2390 responses:
2391 '200':
2392 description: OK
2393 content:
2394 application/json:
2395 schema:
2396 type: object
2397 properties:
2398 id:
2399 type: integer
2400 name:
2401 type: string
2402"#,
2403 Some("yaml"),
2404 )
2405 .unwrap();
2406
2407 let operation = spec
2408 .spec
2409 .paths
2410 .paths
2411 .get("/users")
2412 .and_then(|p| p.as_item())
2413 .and_then(|p| p.get.as_ref())
2414 .unwrap();
2415
2416 let response = ResponseGenerator::generate_response_with_expansion(
2417 &spec,
2418 operation,
2419 200,
2420 Some("application/json"),
2421 false, )
2423 .unwrap();
2424
2425 assert!(response.is_object());
2426 }
2427
2428 #[test]
2429 fn test_generate_response_with_array_schema_referenced_items() {
2430 let spec = OpenApiSpec::from_string(
2432 r#"openapi: 3.0.0
2433info:
2434 title: Test API
2435 version: 1.0.0
2436paths:
2437 /items:
2438 get:
2439 responses:
2440 '200':
2441 description: OK
2442 content:
2443 application/json:
2444 schema:
2445 type: array
2446 items:
2447 $ref: '#/components/schemas/Item'
2448components:
2449 schemas:
2450 Item:
2451 type: object
2452 properties:
2453 id:
2454 type: string
2455 name:
2456 type: string
2457"#,
2458 Some("yaml"),
2459 )
2460 .unwrap();
2461
2462 let operation = spec
2463 .spec
2464 .paths
2465 .paths
2466 .get("/items")
2467 .and_then(|p| p.as_item())
2468 .and_then(|p| p.get.as_ref())
2469 .unwrap();
2470
2471 let response =
2472 ResponseGenerator::generate_response(&spec, operation, 200, Some("application/json"))
2473 .unwrap();
2474
2475 let arr = response.as_array().expect("response should be array");
2477 assert!(!arr.is_empty());
2478 if let Some(item) = arr.first() {
2479 let obj = item.as_object().expect("item should be object");
2480 assert!(obj.contains_key("id") || obj.contains_key("name"));
2481 }
2482 }
2483
2484 #[test]
2485 fn test_generate_response_with_array_schema_missing_reference() {
2486 let spec = OpenApiSpec::from_string(
2488 r#"openapi: 3.0.0
2489info:
2490 title: Test API
2491 version: 1.0.0
2492paths:
2493 /items:
2494 get:
2495 responses:
2496 '200':
2497 description: OK
2498 content:
2499 application/json:
2500 schema:
2501 type: array
2502 items:
2503 $ref: '#/components/schemas/NonExistentItem'
2504components:
2505 schemas: {}
2506"#,
2507 Some("yaml"),
2508 )
2509 .unwrap();
2510
2511 let operation = spec
2512 .spec
2513 .paths
2514 .paths
2515 .get("/items")
2516 .and_then(|p| p.as_item())
2517 .and_then(|p| p.get.as_ref())
2518 .unwrap();
2519
2520 let response =
2521 ResponseGenerator::generate_response(&spec, operation, 200, Some("application/json"))
2522 .unwrap();
2523
2524 let arr = response.as_array().expect("response should be array");
2526 assert!(!arr.is_empty());
2527 }
2528
2529 #[test]
2530 fn test_generate_response_with_array_example_and_pagination() {
2531 let spec = OpenApiSpec::from_string(
2533 r#"openapi: 3.0.0
2534info:
2535 title: Test API
2536 version: 1.0.0
2537paths:
2538 /products:
2539 get:
2540 responses:
2541 '200':
2542 description: OK
2543 content:
2544 application/json:
2545 schema:
2546 type: array
2547 example: [{"id": 1, "name": "Product 1"}]
2548 items:
2549 type: object
2550 properties:
2551 id:
2552 type: integer
2553 name:
2554 type: string
2555"#,
2556 Some("yaml"),
2557 )
2558 .unwrap();
2559
2560 let operation = spec
2561 .spec
2562 .paths
2563 .paths
2564 .get("/products")
2565 .and_then(|p| p.as_item())
2566 .and_then(|p| p.get.as_ref())
2567 .unwrap();
2568
2569 let response =
2570 ResponseGenerator::generate_response(&spec, operation, 200, Some("application/json"))
2571 .unwrap();
2572
2573 let arr = response.as_array().expect("response should be array");
2575 assert!(!arr.is_empty());
2576 if let Some(item) = arr.first() {
2577 let obj = item.as_object().expect("item should be object");
2578 assert!(obj.contains_key("id") || obj.contains_key("name"));
2579 }
2580 }
2581
2582 #[test]
2583 fn test_generate_response_with_missing_response_reference() {
2584 let spec = OpenApiSpec::from_string(
2586 r#"openapi: 3.0.0
2587info:
2588 title: Test API
2589 version: 1.0.0
2590paths:
2591 /users:
2592 get:
2593 responses:
2594 '200':
2595 $ref: '#/components/responses/NonExistentResponse'
2596components:
2597 responses: {}
2598"#,
2599 Some("yaml"),
2600 )
2601 .unwrap();
2602
2603 let operation = spec
2604 .spec
2605 .paths
2606 .paths
2607 .get("/users")
2608 .and_then(|p| p.as_item())
2609 .and_then(|p| p.get.as_ref())
2610 .unwrap();
2611
2612 let response =
2613 ResponseGenerator::generate_response(&spec, operation, 200, Some("application/json"))
2614 .unwrap();
2615
2616 assert!(response.is_object());
2618 assert!(response.as_object().unwrap().is_empty());
2619 }
2620
2621 #[test]
2622 fn test_generate_response_with_no_response_for_status() {
2623 let spec = OpenApiSpec::from_string(
2625 r#"openapi: 3.0.0
2626info:
2627 title: Test API
2628 version: 1.0.0
2629paths:
2630 /users:
2631 get:
2632 responses:
2633 '404':
2634 description: Not found
2635"#,
2636 Some("yaml"),
2637 )
2638 .unwrap();
2639
2640 let operation = spec
2641 .spec
2642 .paths
2643 .paths
2644 .get("/users")
2645 .and_then(|p| p.as_item())
2646 .and_then(|p| p.get.as_ref())
2647 .unwrap();
2648
2649 let response =
2651 ResponseGenerator::generate_response(&spec, operation, 200, Some("application/json"))
2652 .unwrap();
2653
2654 assert!(response.is_object());
2656 assert!(response.as_object().unwrap().is_empty());
2657 }
2658}
2659
2660#[derive(Debug, Clone)]
2662pub struct MockResponse {
2663 pub status_code: u16,
2665 pub headers: HashMap<String, String>,
2667 pub body: Option<Value>,
2669}
2670
2671impl MockResponse {
2672 pub fn new(status_code: u16) -> Self {
2674 Self {
2675 status_code,
2676 headers: HashMap::new(),
2677 body: None,
2678 }
2679 }
2680
2681 pub fn with_header(mut self, name: String, value: String) -> Self {
2683 self.headers.insert(name, value);
2684 self
2685 }
2686
2687 pub fn with_body(mut self, body: Value) -> Self {
2689 self.body = Some(body);
2690 self
2691 }
2692}
2693
2694#[derive(Debug, Clone)]
2696pub struct OpenApiSecurityRequirement {
2697 pub scheme: String,
2699 pub scopes: Vec<String>,
2701}
2702
2703impl OpenApiSecurityRequirement {
2704 pub fn new(scheme: String, scopes: Vec<String>) -> Self {
2706 Self { scheme, scopes }
2707 }
2708}
2709
2710#[derive(Debug, Clone)]
2712pub struct OpenApiOperation {
2713 pub method: String,
2715 pub path: String,
2717 pub operation: openapiv3::Operation,
2719}
2720
2721impl OpenApiOperation {
2722 pub fn new(method: String, path: String, operation: openapiv3::Operation) -> Self {
2724 Self {
2725 method,
2726 path,
2727 operation,
2728 }
2729 }
2730}