1use crate::{
7 ai_response::{expand_prompt_template, AiResponseConfig, RequestContext},
8 OpenApiSpec, Result,
9};
10use crate::intelligent_behavior::config::Persona;
11use async_trait::async_trait;
12use chrono;
13use openapiv3::{Operation, ReferenceOr, Response, Responses, Schema};
14use rand::{rng, Rng};
15use serde_json::Value;
16use std::collections::{BTreeMap, 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 = expand_prompt_template(prompt_template, context);
64
65 tracing::info!("AI response generation requested with prompt: {}", expanded_prompt);
66
67 if let Some(gen) = generator {
69 tracing::debug!("Using provided AI generator for response");
70 return gen.generate(&expanded_prompt, ai_config).await;
71 }
72
73 tracing::warn!("No AI generator provided, returning placeholder response");
75 Ok(serde_json::json!({
76 "ai_response": "AI generation placeholder",
77 "note": "This endpoint is configured for AI-assisted responses, but no AI generator was provided",
78 "expanded_prompt": expanded_prompt,
79 "mode": format!("{:?}", ai_config.mode),
80 "temperature": ai_config.temperature,
81 "implementation_note": "Pass an AiGenerator implementation to ResponseGenerator::generate_ai_response to enable actual AI generation"
82 }))
83 }
84
85 pub fn generate_response(
87 spec: &OpenApiSpec,
88 operation: &Operation,
89 status_code: u16,
90 content_type: Option<&str>,
91 ) -> Result<Value> {
92 Self::generate_response_with_expansion(spec, operation, status_code, content_type, true)
93 }
94
95 pub fn generate_response_with_expansion(
97 spec: &OpenApiSpec,
98 operation: &Operation,
99 status_code: u16,
100 content_type: Option<&str>,
101 expand_tokens: bool,
102 ) -> Result<Value> {
103 Self::generate_response_with_expansion_and_mode(
104 spec,
105 operation,
106 status_code,
107 content_type,
108 expand_tokens,
109 None,
110 None,
111 )
112 }
113
114 pub fn generate_response_with_expansion_and_mode(
116 spec: &OpenApiSpec,
117 operation: &Operation,
118 status_code: u16,
119 content_type: Option<&str>,
120 expand_tokens: bool,
121 selection_mode: Option<crate::openapi::response_selection::ResponseSelectionMode>,
122 selector: Option<&crate::openapi::response_selection::ResponseSelector>,
123 ) -> Result<Value> {
124 Self::generate_response_with_expansion_and_mode_and_persona(
125 spec,
126 operation,
127 status_code,
128 content_type,
129 expand_tokens,
130 selection_mode,
131 selector,
132 None, )
134 }
135
136 pub fn generate_response_with_expansion_and_mode_and_persona(
138 spec: &OpenApiSpec,
139 operation: &Operation,
140 status_code: u16,
141 content_type: Option<&str>,
142 expand_tokens: bool,
143 selection_mode: Option<crate::openapi::response_selection::ResponseSelectionMode>,
144 selector: Option<&crate::openapi::response_selection::ResponseSelector>,
145 persona: Option<&Persona>,
146 ) -> Result<Value> {
147 Self::generate_response_with_scenario_and_mode_and_persona(
148 spec,
149 operation,
150 status_code,
151 content_type,
152 expand_tokens,
153 None, selection_mode,
155 selector,
156 persona,
157 )
158 }
159
160 pub fn generate_response_with_scenario(
186 spec: &OpenApiSpec,
187 operation: &Operation,
188 status_code: u16,
189 content_type: Option<&str>,
190 expand_tokens: bool,
191 scenario: Option<&str>,
192 ) -> Result<Value> {
193 Self::generate_response_with_scenario_and_mode(
194 spec,
195 operation,
196 status_code,
197 content_type,
198 expand_tokens,
199 scenario,
200 None,
201 None,
202 )
203 }
204
205 pub fn generate_response_with_scenario_and_mode(
207 spec: &OpenApiSpec,
208 operation: &Operation,
209 status_code: u16,
210 content_type: Option<&str>,
211 expand_tokens: bool,
212 scenario: Option<&str>,
213 selection_mode: Option<crate::openapi::response_selection::ResponseSelectionMode>,
214 selector: Option<&crate::openapi::response_selection::ResponseSelector>,
215 ) -> Result<Value> {
216 Self::generate_response_with_scenario_and_mode_and_persona(
217 spec,
218 operation,
219 status_code,
220 content_type,
221 expand_tokens,
222 scenario,
223 selection_mode,
224 selector,
225 None, )
227 }
228
229 pub fn generate_response_with_scenario_and_mode_and_persona(
231 spec: &OpenApiSpec,
232 operation: &Operation,
233 status_code: u16,
234 content_type: Option<&str>,
235 expand_tokens: bool,
236 scenario: Option<&str>,
237 selection_mode: Option<crate::openapi::response_selection::ResponseSelectionMode>,
238 selector: Option<&crate::openapi::response_selection::ResponseSelector>,
239 persona: Option<&Persona>,
240 ) -> Result<Value> {
241 let response = Self::find_response_for_status(&operation.responses, status_code);
243
244 match response {
245 Some(response_ref) => {
246 match response_ref {
247 ReferenceOr::Item(response) => {
248 Self::generate_from_response_with_scenario_and_mode(
249 spec,
250 response,
251 content_type,
252 expand_tokens,
253 scenario,
254 selection_mode,
255 selector,
256 )
257 }
258 ReferenceOr::Reference { reference } => {
259 if let Some(resolved_response) = spec.get_response(reference) {
261 Self::generate_from_response_with_scenario_and_mode(
262 spec,
263 resolved_response,
264 content_type,
265 expand_tokens,
266 scenario,
267 selection_mode,
268 selector,
269 )
270 } else {
271 Ok(Value::Object(serde_json::Map::new()))
273 }
274 }
275 }
276 }
277 None => {
278 Ok(Value::Object(serde_json::Map::new()))
280 }
281 }
282 }
283
284 fn find_response_for_status(
286 responses: &Responses,
287 status_code: u16,
288 ) -> Option<&ReferenceOr<Response>> {
289 if let Some(response) = responses.responses.get(&openapiv3::StatusCode::Code(status_code)) {
291 return Some(response);
292 }
293
294 if let Some(default_response) = &responses.default {
296 return Some(default_response);
297 }
298
299 None
300 }
301
302 fn generate_from_response(
304 spec: &OpenApiSpec,
305 response: &Response,
306 content_type: Option<&str>,
307 expand_tokens: bool,
308 ) -> Result<Value> {
309 Self::generate_from_response_with_scenario(
310 spec,
311 response,
312 content_type,
313 expand_tokens,
314 None,
315 )
316 }
317
318 fn generate_from_response_with_scenario(
320 spec: &OpenApiSpec,
321 response: &Response,
322 content_type: Option<&str>,
323 expand_tokens: bool,
324 scenario: Option<&str>,
325 ) -> Result<Value> {
326 Self::generate_from_response_with_scenario_and_mode(
327 spec,
328 response,
329 content_type,
330 expand_tokens,
331 scenario,
332 None,
333 None,
334 )
335 }
336
337 fn generate_from_response_with_scenario_and_mode(
339 spec: &OpenApiSpec,
340 response: &Response,
341 content_type: Option<&str>,
342 expand_tokens: bool,
343 scenario: Option<&str>,
344 selection_mode: Option<crate::openapi::response_selection::ResponseSelectionMode>,
345 selector: Option<&crate::openapi::response_selection::ResponseSelector>,
346 ) -> Result<Value> {
347 Self::generate_from_response_with_scenario_and_mode_and_persona(
348 spec,
349 response,
350 content_type,
351 expand_tokens,
352 scenario,
353 selection_mode,
354 selector,
355 None, )
357 }
358
359 fn generate_from_response_with_scenario_and_mode_and_persona(
361 spec: &OpenApiSpec,
362 response: &Response,
363 content_type: Option<&str>,
364 expand_tokens: bool,
365 scenario: Option<&str>,
366 selection_mode: Option<crate::openapi::response_selection::ResponseSelectionMode>,
367 selector: Option<&crate::openapi::response_selection::ResponseSelector>,
368 persona: Option<&Persona>,
369 ) -> Result<Value> {
370 if let Some(content_type) = content_type {
372 if let Some(media_type) = response.content.get(content_type) {
373 return Self::generate_from_media_type_with_scenario_and_mode_and_persona(
374 spec,
375 media_type,
376 expand_tokens,
377 scenario,
378 selection_mode,
379 selector,
380 persona,
381 );
382 }
383 }
384
385 let preferred_types = ["application/json", "application/xml", "text/plain"];
387
388 for content_type in &preferred_types {
389 if let Some(media_type) = response.content.get(*content_type) {
390 return Self::generate_from_media_type_with_scenario_and_mode_and_persona(
391 spec,
392 media_type,
393 expand_tokens,
394 scenario,
395 selection_mode,
396 selector,
397 persona,
398 );
399 }
400 }
401
402 if let Some((_, media_type)) = response.content.iter().next() {
404 return Self::generate_from_media_type_with_scenario_and_mode_and_persona(
405 spec,
406 media_type,
407 expand_tokens,
408 scenario,
409 selection_mode,
410 selector,
411 persona,
412 );
413 }
414
415 Ok(Value::Object(serde_json::Map::new()))
417 }
418
419 fn generate_from_media_type(
421 spec: &OpenApiSpec,
422 media_type: &openapiv3::MediaType,
423 expand_tokens: bool,
424 ) -> Result<Value> {
425 Self::generate_from_media_type_with_scenario(spec, media_type, expand_tokens, None)
426 }
427
428 fn generate_from_media_type_with_scenario(
430 spec: &OpenApiSpec,
431 media_type: &openapiv3::MediaType,
432 expand_tokens: bool,
433 scenario: Option<&str>,
434 ) -> Result<Value> {
435 Self::generate_from_media_type_with_scenario_and_mode(
436 spec,
437 media_type,
438 expand_tokens,
439 scenario,
440 None,
441 None,
442 )
443 }
444
445 fn generate_from_media_type_with_scenario_and_mode(
447 spec: &OpenApiSpec,
448 media_type: &openapiv3::MediaType,
449 expand_tokens: bool,
450 scenario: Option<&str>,
451 selection_mode: Option<crate::openapi::response_selection::ResponseSelectionMode>,
452 selector: Option<&crate::openapi::response_selection::ResponseSelector>,
453 ) -> Result<Value> {
454 Self::generate_from_media_type_with_scenario_and_mode_and_persona(
455 spec,
456 media_type,
457 expand_tokens,
458 scenario,
459 selection_mode,
460 selector,
461 None, )
463 }
464
465 fn generate_from_media_type_with_scenario_and_mode_and_persona(
467 spec: &OpenApiSpec,
468 media_type: &openapiv3::MediaType,
469 expand_tokens: bool,
470 scenario: Option<&str>,
471 selection_mode: Option<crate::openapi::response_selection::ResponseSelectionMode>,
472 selector: Option<&crate::openapi::response_selection::ResponseSelector>,
473 persona: Option<&Persona>,
474 ) -> Result<Value> {
475 if let Some(example) = &media_type.example {
477 tracing::debug!("Using explicit example from media type: {:?}", example);
478 if expand_tokens {
480 let expanded_example = Self::expand_templates(example);
481 return Ok(expanded_example);
482 } else {
483 return Ok(example.clone());
484 }
485 }
486
487 if !media_type.examples.is_empty() {
489 use crate::openapi::response_selection::{ResponseSelectionMode, ResponseSelector};
490
491 if let Some(scenario_name) = scenario {
493 if let Some(example_ref) = media_type.examples.get(scenario_name) {
494 tracing::debug!("Using scenario '{}' from examples map", scenario_name);
495 return Ok(Self::extract_example_value_with_persona(
496 spec,
497 example_ref,
498 expand_tokens,
499 persona,
500 media_type.schema.as_ref(),
501 )?);
502 } else {
503 tracing::warn!(
504 "Scenario '{}' not found in examples, falling back based on selection mode",
505 scenario_name
506 );
507 }
508 }
509
510 let mode = selection_mode.unwrap_or(ResponseSelectionMode::First);
512
513 let example_names: Vec<String> = media_type.examples.keys().cloned().collect();
515
516 if example_names.is_empty() {
517 } else if mode == ResponseSelectionMode::Scenario && scenario.is_some() {
519 } else {
521 let selected_index = if let Some(sel) = selector {
523 sel.select(&example_names)
524 } else {
525 let temp_selector = ResponseSelector::new(mode);
527 temp_selector.select(&example_names)
528 };
529
530 if let Some(example_name) = example_names.get(selected_index) {
531 if let Some(example_ref) = media_type.examples.get(example_name) {
532 tracing::debug!(
533 "Using example '{}' from examples map (mode: {:?}, index: {})",
534 example_name,
535 mode,
536 selected_index
537 );
538 return Ok(Self::extract_example_value_with_persona(
539 spec,
540 example_ref,
541 expand_tokens,
542 persona,
543 media_type.schema.as_ref(),
544 )?);
545 }
546 }
547 }
548
549 if let Some((example_name, example_ref)) = media_type.examples.iter().next() {
551 tracing::debug!(
552 "Using first example '{}' from examples map as fallback",
553 example_name
554 );
555 return Ok(Self::extract_example_value_with_persona(
556 spec,
557 example_ref,
558 expand_tokens,
559 persona,
560 media_type.schema.as_ref(),
561 )?);
562 }
563 }
564
565 if let Some(schema_ref) = &media_type.schema {
568 Ok(Self::generate_example_from_schema_ref(spec, schema_ref, persona))
569 } else {
570 Ok(Value::Object(serde_json::Map::new()))
571 }
572 }
573
574 fn extract_example_value(
577 spec: &OpenApiSpec,
578 example_ref: &ReferenceOr<openapiv3::Example>,
579 expand_tokens: bool,
580 ) -> Result<Value> {
581 Self::extract_example_value_with_persona(spec, example_ref, expand_tokens, None, None)
582 }
583
584 fn extract_example_value_with_persona(
586 spec: &OpenApiSpec,
587 example_ref: &ReferenceOr<openapiv3::Example>,
588 expand_tokens: bool,
589 persona: Option<&Persona>,
590 schema_ref: Option<&ReferenceOr<Schema>>,
591 ) -> Result<Value> {
592 let mut value = match example_ref {
593 ReferenceOr::Item(example) => {
594 if let Some(v) = &example.value {
595 tracing::debug!("Using example from examples map: {:?}", v);
596 if expand_tokens {
597 Self::expand_templates(v)
598 } else {
599 v.clone()
600 }
601 } else {
602 return Ok(Value::Object(serde_json::Map::new()));
603 }
604 }
605 ReferenceOr::Reference { reference } => {
606 if let Some(example) = spec.get_example(reference) {
608 if let Some(v) = &example.value {
609 tracing::debug!("Using resolved example reference: {:?}", v);
610 if expand_tokens {
611 Self::expand_templates(v)
612 } else {
613 v.clone()
614 }
615 } else {
616 return Ok(Value::Object(serde_json::Map::new()));
617 }
618 } else {
619 tracing::warn!("Example reference '{}' not found", reference);
620 return Ok(Value::Object(serde_json::Map::new()));
621 }
622 }
623 };
624
625 value = Self::expand_example_items_if_needed(spec, value, persona, schema_ref);
627
628 Ok(value)
629 }
630
631 fn expand_example_items_if_needed(
634 spec: &OpenApiSpec,
635 mut example: Value,
636 persona: Option<&Persona>,
637 schema_ref: Option<&ReferenceOr<Schema>>,
638 ) -> Value {
639 let has_nested_items = example.get("data")
642 .and_then(|v| v.as_object())
643 .map(|obj| obj.contains_key("items"))
644 .unwrap_or(false);
645
646 let has_flat_items = example.get("items").is_some();
647
648 if !has_nested_items && !has_flat_items {
649 return example; }
651
652 let total = example.get("data")
654 .and_then(|d| d.get("total"))
655 .or_else(|| example.get("total"))
656 .and_then(|v| {
657 v.as_u64().or_else(|| v.as_i64().map(|i| i as u64))
658 });
659
660 let limit = example.get("data")
661 .and_then(|d| d.get("limit"))
662 .or_else(|| example.get("limit"))
663 .and_then(|v| {
664 v.as_u64().or_else(|| v.as_i64().map(|i| i as u64))
665 });
666
667 let items_array = example.get("data")
669 .and_then(|d| d.get("items"))
670 .or_else(|| example.get("items"))
671 .and_then(|v| v.as_array())
672 .cloned();
673
674 if let (Some(total_val), Some(limit_val), Some(mut items)) = (total, limit, items_array) {
675 let current_count = items.len() as u64;
676 let expected_count = std::cmp::min(total_val, limit_val);
677 let max_items = 100; let expected_count = std::cmp::min(expected_count, max_items);
679
680 if current_count < expected_count && !items.is_empty() {
682 tracing::debug!(
683 "Expanding example items array: {} -> {} items (total={}, limit={})",
684 current_count,
685 expected_count,
686 total_val,
687 limit_val
688 );
689
690 let template = items[0].clone();
692 let additional_count = expected_count - current_count;
693
694 for i in 0..additional_count {
696 let mut new_item = template.clone();
697 let item_index = current_count + i + 1;
699 Self::add_item_variation(&mut new_item, item_index);
700 items.push(new_item);
701 }
702
703 if let Some(data_obj) = example.get_mut("data").and_then(|v| v.as_object_mut()) {
705 data_obj.insert("items".to_string(), Value::Array(items));
706 } else if let Some(root_obj) = example.as_object_mut() {
707 root_obj.insert("items".to_string(), Value::Array(items));
708 }
709 }
710 }
711
712 example
713 }
714
715 fn generate_example_from_schema_ref(
716 spec: &OpenApiSpec,
717 schema_ref: &ReferenceOr<Schema>,
718 persona: Option<&Persona>,
719 ) -> Value {
720 match schema_ref {
721 ReferenceOr::Item(schema) => Self::generate_example_from_schema(spec, schema, persona),
722 ReferenceOr::Reference { reference } => spec
723 .get_schema(reference)
724 .map(|schema| Self::generate_example_from_schema(spec, &schema.schema, persona))
725 .unwrap_or_else(|| Value::Object(serde_json::Map::new())),
726 }
727 }
728
729 fn generate_example_from_schema(spec: &OpenApiSpec, schema: &Schema, persona: Option<&Persona>) -> Value {
737 if let Some(example) = schema.schema_data.example.as_ref() {
740 tracing::debug!("Using schema-level example: {:?}", example);
741 return example.clone();
742 }
743
744 match &schema.schema_kind {
748 openapiv3::SchemaKind::Type(openapiv3::Type::String(_)) => {
749 Value::String("example string".to_string())
751 }
752 openapiv3::SchemaKind::Type(openapiv3::Type::Integer(_)) => Value::Number(42.into()),
753 openapiv3::SchemaKind::Type(openapiv3::Type::Number(_)) => {
754 Value::Number(serde_json::Number::from_f64(std::f64::consts::PI).unwrap())
755 }
756 openapiv3::SchemaKind::Type(openapiv3::Type::Boolean(_)) => Value::Bool(true),
757 openapiv3::SchemaKind::Type(openapiv3::Type::Object(obj)) => {
758 let mut pagination_metadata: Option<(u64, u64, u64)> = None; let has_items = obj.properties.iter()
765 .any(|(name, _)| name.to_lowercase() == "items");
766
767 if has_items {
768 let mut total_opt = None;
770 let mut page_opt = None;
771 let mut limit_opt = None;
772
773 for (prop_name, prop_schema) in &obj.properties {
774 let prop_lower = prop_name.to_lowercase();
775 let schema_ref: ReferenceOr<Schema> = match prop_schema {
777 ReferenceOr::Item(boxed) => ReferenceOr::Item(boxed.as_ref().clone()),
778 ReferenceOr::Reference { reference } => ReferenceOr::Reference { reference: reference.clone() },
779 };
780 if prop_lower == "total" || prop_lower == "count" || prop_lower == "size" {
781 total_opt = Self::extract_numeric_value_from_schema(&schema_ref);
782 } else if prop_lower == "page" {
783 page_opt = Self::extract_numeric_value_from_schema(&schema_ref);
784 } else if prop_lower == "limit" || prop_lower == "per_page" {
785 limit_opt = Self::extract_numeric_value_from_schema(&schema_ref);
786 }
787 }
788
789 if let Some(total) = total_opt {
791 let page = page_opt.unwrap_or(1);
792 let limit = limit_opt.unwrap_or(20);
793 pagination_metadata = Some((total, page, limit));
794 tracing::debug!(
795 "Detected pagination metadata: total={}, page={}, limit={}",
796 total, page, limit
797 );
798 } else {
799 if obj.properties.contains_key("items") {
802 if let Some(inferred_total) = Self::try_infer_total_from_context(spec, obj) {
806 let page = page_opt.unwrap_or(1);
807 let limit = limit_opt.unwrap_or(20);
808 pagination_metadata = Some((inferred_total, page, limit));
809 tracing::debug!(
810 "Inferred pagination metadata from parent entity: total={}, page={}, limit={}",
811 inferred_total, page, limit
812 );
813 } else {
814 if let Some(persona) = persona {
816 let count_keys = ["hive_count", "apiary_count", "item_count", "total_count"];
819 for key in &count_keys {
820 if let Some(count) = persona.get_numeric_trait(key) {
821 let page = page_opt.unwrap_or(1);
822 let limit = limit_opt.unwrap_or(20);
823 pagination_metadata = Some((count, page, limit));
824 tracing::debug!(
825 "Using persona trait '{}' for pagination: total={}, page={}, limit={}",
826 key, count, page, limit
827 );
828 break;
829 }
830 }
831 }
832 }
833 }
834 }
835 }
836
837 let mut map = serde_json::Map::new();
838 for (prop_name, prop_schema) in &obj.properties {
839 let prop_lower = prop_name.to_lowercase();
840
841 let is_items_array = prop_lower == "items" && pagination_metadata.is_some();
843
844 let value = match prop_schema {
845 ReferenceOr::Item(prop_schema) => {
846 if is_items_array {
849 Self::generate_array_with_count(
851 spec,
852 prop_schema.as_ref(),
853 pagination_metadata.unwrap(),
854 persona,
855 )
856 } else if let Some(prop_example) = prop_schema.schema_data.example.as_ref() {
857 tracing::debug!(
859 "Using example for property '{}': {:?}",
860 prop_name,
861 prop_example
862 );
863 prop_example.clone()
864 } else {
865 Self::generate_example_from_schema(spec, prop_schema.as_ref(), persona)
866 }
867 }
868 ReferenceOr::Reference { reference } => {
869 if let Some(resolved_schema) = spec.get_schema(reference) {
871 if is_items_array {
873 Self::generate_array_with_count(
875 spec,
876 &resolved_schema.schema,
877 pagination_metadata.unwrap(),
878 persona,
879 )
880 } else if let Some(ref_example) =
881 resolved_schema.schema.schema_data.example.as_ref()
882 {
883 tracing::debug!(
885 "Using example from referenced schema '{}': {:?}",
886 reference,
887 ref_example
888 );
889 ref_example.clone()
890 } else {
891 Self::generate_example_from_schema(
892 spec,
893 &resolved_schema.schema,
894 persona,
895 )
896 }
897 } else {
898 Self::generate_example_for_property(prop_name)
899 }
900 }
901 };
902 let value = match value {
903 Value::Null => Self::generate_example_for_property(prop_name),
904 Value::Object(ref obj) if obj.is_empty() => {
905 Self::generate_example_for_property(prop_name)
906 }
907 _ => value,
908 };
909 map.insert(prop_name.clone(), value);
910 }
911
912 if let Some((total, page, limit)) = pagination_metadata {
914 map.insert("total".to_string(), Value::Number(total.into()));
915 map.insert("page".to_string(), Value::Number(page.into()));
916 map.insert("limit".to_string(), Value::Number(limit.into()));
917 }
918
919 Value::Object(map)
920 }
921 openapiv3::SchemaKind::Type(openapiv3::Type::Array(arr)) => {
922 match &arr.items {
928 Some(item_schema) => {
929 let example_item = match item_schema {
930 ReferenceOr::Item(item_schema) => {
931 Self::generate_example_from_schema(spec, item_schema.as_ref(), persona)
934 }
935 ReferenceOr::Reference { reference } => {
936 if let Some(resolved_schema) = spec.get_schema(reference) {
939 Self::generate_example_from_schema(
940 spec,
941 &resolved_schema.schema,
942 persona,
943 )
944 } else {
945 Value::Object(serde_json::Map::new())
946 }
947 }
948 };
949 Value::Array(vec![example_item])
950 }
951 None => Value::Array(vec![Value::String("item".to_string())]),
952 }
953 }
954 _ => Value::Object(serde_json::Map::new()),
955 }
956 }
957
958 fn extract_numeric_value_from_schema(
961 schema_ref: &ReferenceOr<Schema>,
962 ) -> Option<u64> {
963 match schema_ref {
964 ReferenceOr::Item(schema) => {
965 if let Some(example) = schema.schema_data.example.as_ref() {
967 if let Some(num) = example.as_u64() {
968 return Some(num);
969 } else if let Some(num) = example.as_f64() {
970 return Some(num as u64);
971 }
972 }
973 if let Some(default) = schema.schema_data.default.as_ref() {
975 if let Some(num) = default.as_u64() {
976 return Some(num);
977 } else if let Some(num) = default.as_f64() {
978 return Some(num as u64);
979 }
980 }
981 None
985 }
986 ReferenceOr::Reference { reference: _ } => {
987 None
990 }
991 }
992 }
993
994 fn generate_array_with_count(
997 spec: &OpenApiSpec,
998 array_schema: &Schema,
999 pagination: (u64, u64, u64), persona: Option<&Persona>,
1001 ) -> Value {
1002 let (total, _page, limit) = pagination;
1003
1004 let count = std::cmp::min(total, limit);
1007
1008 let max_items = 100;
1010 let count = std::cmp::min(count, max_items);
1011
1012 tracing::debug!(
1013 "Generating array with count={} (total={}, limit={})",
1014 count,
1015 total,
1016 limit
1017 );
1018
1019 if let Some(example) = array_schema.schema_data.example.as_ref() {
1021 if let Some(example_array) = example.as_array() {
1022 if !example_array.is_empty() {
1023 let template_item = &example_array[0];
1025 let items: Vec<Value> = (0..count)
1026 .map(|i| {
1027 let mut item = template_item.clone();
1029 Self::add_item_variation(&mut item, i + 1);
1030 item
1031 })
1032 .collect();
1033 return Value::Array(items);
1034 }
1035 }
1036 }
1037
1038 if let openapiv3::SchemaKind::Type(openapiv3::Type::Array(arr)) = &array_schema.schema_kind {
1040 if let Some(item_schema) = &arr.items {
1041 let items: Vec<Value> = match item_schema {
1042 ReferenceOr::Item(item_schema) => {
1043 (0..count)
1044 .map(|i| {
1045 let mut item = Self::generate_example_from_schema(spec, item_schema.as_ref(), persona);
1046 Self::add_item_variation(&mut item, i + 1);
1048 item
1049 })
1050 .collect()
1051 }
1052 ReferenceOr::Reference { reference } => {
1053 if let Some(resolved_schema) = spec.get_schema(reference) {
1054 (0..count)
1055 .map(|i| {
1056 let mut item = Self::generate_example_from_schema(
1057 spec,
1058 &resolved_schema.schema,
1059 persona,
1060 );
1061 Self::add_item_variation(&mut item, i + 1);
1063 item
1064 })
1065 .collect()
1066 } else {
1067 vec![Value::Object(serde_json::Map::new()); count as usize]
1068 }
1069 }
1070 };
1071 return Value::Array(items);
1072 }
1073 }
1074
1075 Value::Array(
1077 (0..count)
1078 .map(|i| Value::String(format!("item_{}", i + 1)))
1079 .collect(),
1080 )
1081 }
1082
1083 fn add_item_variation(item: &mut Value, item_index: u64) {
1086 if let Some(obj) = item.as_object_mut() {
1087 if let Some(id_val) = obj.get_mut("id") {
1089 if let Some(id_str) = id_val.as_str() {
1090 let base_id = id_str.split('_').next().unwrap_or(id_str);
1092 *id_val = Value::String(format!("{}_{:03}", base_id, item_index));
1093 } else if let Some(id_num) = id_val.as_u64() {
1094 *id_val = Value::Number((id_num + item_index).into());
1095 }
1096 }
1097
1098 if let Some(name_val) = obj.get_mut("name") {
1100 if let Some(name_str) = name_val.as_str() {
1101 if name_str.contains('#') {
1102 *name_val = Value::String(format!("Hive #{}", item_index));
1104 } else {
1105 let apiary_names = [
1108 "Meadow Apiary", "Prairie Apiary", "Sunset Valley Apiary", "Golden Fields Apiary",
1110 "Miller Family Apiary", "Heartland Honey Co.", "Cornfield Apiary", "Harvest Moon Apiary",
1111 "Prairie Winds Apiary", "Amber Fields Apiary",
1112 "Coastal Apiary", "Sunset Coast Apiary", "Pacific Grove Apiary", "Golden Gate Apiary",
1114 "Napa Valley Apiary", "Coastal Breeze Apiary", "Pacific Heights Apiary", "Bay Area Apiary",
1115 "Sunset Valley Honey Co.", "Coastal Harvest Apiary",
1116 "Lone Star Apiary", "Texas Ranch Apiary", "Big Sky Apiary", "Prairie Rose Apiary",
1118 "Hill Country Apiary", "Lone Star Honey Co.", "Texas Pride Apiary", "Wildflower Ranch",
1119 "Desert Bloom Apiary", "Cactus Creek Apiary",
1120 "Orange Grove Apiary", "Citrus Grove Apiary", "Palm Grove Apiary", "Tropical Breeze Apiary",
1122 "Everglades Apiary", "Sunshine State Apiary", "Florida Keys Apiary", "Grove View Apiary",
1123 "Tropical Harvest Apiary", "Palm Coast Apiary",
1124 "Mountain View Apiary", "Valley Apiary", "Riverside Apiary", "Hilltop Apiary",
1126 "Forest Apiary", "Mountain Apiary", "Lakeside Apiary", "Ridge Apiary",
1127 "Brook Apiary", "Hillside Apiary",
1128 "Field Apiary", "Creek Apiary", "Woodland Apiary", "Farm Apiary",
1130 "Orchard Apiary", "Pasture Apiary", "Green Valley Apiary", "Blue Sky Apiary",
1131 "Sweet Honey Apiary", "Nature's Best Apiary",
1132 "Premium Honey Co.", "Artisan Apiary", "Heritage Apiary", "Summit Apiary",
1134 "Crystal Springs Apiary", "Maple Grove Apiary", "Wildflower Apiary", "Thistle Apiary",
1135 "Clover Field Apiary", "Honeycomb Apiary"
1136 ];
1137 let name_index = (item_index - 1) as usize % apiary_names.len();
1138 *name_val = Value::String(apiary_names[name_index].to_string());
1139 }
1140 }
1141 }
1142
1143 if let Some(location_val) = obj.get_mut("location") {
1145 if let Some(location_obj) = location_val.as_object_mut() {
1146 if let Some(address_val) = location_obj.get_mut("address") {
1148 if let Some(address_str) = address_val.as_str() {
1149 if let Some(num_str) = address_str.split_whitespace().next() {
1151 if let Ok(num) = num_str.parse::<u64>() {
1152 *address_val = Value::String(format!("{} Farm Road", num + item_index));
1153 } else {
1154 *address_val = Value::String(format!("{} Farm Road", 100 + item_index));
1155 }
1156 } else {
1157 *address_val = Value::String(format!("{} Farm Road", 100 + item_index));
1158 }
1159 }
1160 }
1161
1162 if let Some(lat_val) = location_obj.get_mut("latitude") {
1164 if let Some(lat) = lat_val.as_f64() {
1165 *lat_val = Value::Number(serde_json::Number::from_f64(lat + (item_index as f64 * 0.01)).unwrap());
1166 }
1167 }
1168 if let Some(lng_val) = location_obj.get_mut("longitude") {
1169 if let Some(lng) = lng_val.as_f64() {
1170 *lng_val = Value::Number(serde_json::Number::from_f64(lng + (item_index as f64 * 0.01)).unwrap());
1171 }
1172 }
1173 } else if let Some(address_str) = location_val.as_str() {
1174 if let Some(num_str) = address_str.split_whitespace().next() {
1176 if let Ok(num) = num_str.parse::<u64>() {
1177 *location_val = Value::String(format!("{} Farm Road", num + item_index));
1178 } else {
1179 *location_val = Value::String(format!("{} Farm Road", 100 + item_index));
1180 }
1181 }
1182 }
1183 }
1184
1185 if let Some(address_val) = obj.get_mut("address") {
1187 if let Some(address_str) = address_val.as_str() {
1188 if let Some(num_str) = address_str.split_whitespace().next() {
1189 if let Ok(num) = num_str.parse::<u64>() {
1190 *address_val = Value::String(format!("{} Farm Road", num + item_index));
1191 } else {
1192 *address_val = Value::String(format!("{} Farm Road", 100 + item_index));
1193 }
1194 }
1195 }
1196 }
1197
1198 if let Some(status_val) = obj.get_mut("status") {
1200 if let Some(status_str) = status_val.as_str() {
1201 let statuses = ["healthy", "sick", "needs_attention", "quarantined", "active", "inactive"];
1202 let status_index = (item_index - 1) as usize % statuses.len();
1203 let final_status = if (item_index - 1) % 10 < 7 {
1205 statuses[0] } else {
1207 statuses[status_index]
1208 };
1209 *status_val = Value::String(final_status.to_string());
1210 }
1211 }
1212
1213 if let Some(hive_type_val) = obj.get_mut("hive_type") {
1215 if let Some(_) = hive_type_val.as_str() {
1216 let hive_types = ["langstroth", "top_bar", "warre", "flow_hive", "national"];
1217 let type_index = (item_index - 1) as usize % hive_types.len();
1218 *hive_type_val = Value::String(hive_types[type_index].to_string());
1219 }
1220 }
1221
1222 if let Some(queen_val) = obj.get_mut("queen") {
1224 if let Some(queen_obj) = queen_val.as_object_mut() {
1225 if let Some(breed_val) = queen_obj.get_mut("breed") {
1226 if let Some(_) = breed_val.as_str() {
1227 let breeds = ["italian", "carniolan", "russian", "buckfast", "caucasian"];
1228 let breed_index = (item_index - 1) as usize % breeds.len();
1229 *breed_val = Value::String(breeds[breed_index].to_string());
1230 }
1231 }
1232 if let Some(age_val) = queen_obj.get_mut("age_days") {
1234 if let Some(base_age) = age_val.as_u64() {
1235 *age_val = Value::Number((base_age + (item_index * 10) % 200).into());
1236 } else if let Some(base_age) = age_val.as_i64() {
1237 *age_val = Value::Number((base_age + (item_index as i64 * 10) % 200).into());
1238 }
1239 }
1240 if let Some(color_val) = queen_obj.get_mut("mark_color") {
1242 if let Some(_) = color_val.as_str() {
1243 let colors = ["yellow", "white", "red", "green", "blue"];
1244 let color_index = (item_index - 1) as usize % colors.len();
1245 *color_val = Value::String(colors[color_index].to_string());
1246 }
1247 }
1248 }
1249 }
1250
1251 if let Some(desc_val) = obj.get_mut("description") {
1253 if let Some(desc_str) = desc_val.as_str() {
1254 let descriptions = [
1255 "Production apiary",
1256 "Research apiary",
1257 "Commercial operation",
1258 "Backyard apiary",
1259 "Educational apiary"
1260 ];
1261 let desc_index = (item_index - 1) as usize % descriptions.len();
1262 *desc_val = Value::String(descriptions[desc_index].to_string());
1263 }
1264 }
1265
1266 let timestamp_fields = ["created_at", "updated_at", "timestamp", "date", "forecastDate", "predictedDate"];
1269 for field_name in ×tamp_fields {
1270 if let Some(timestamp_val) = obj.get_mut(*field_name) {
1271 if let Some(_timestamp_str) = timestamp_val.as_str() {
1272 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;
1282 let base_month = 11;
1283
1284 let target_year = if months_ago >= base_month as u64 {
1286 base_year - 1
1287 } else {
1288 base_year
1289 };
1290 let target_month = if months_ago >= base_month as u64 {
1291 12 - (months_ago - base_month as u64) as u8
1292 } else {
1293 (base_month as u64 - months_ago) as u8
1294 };
1295 let target_day = std::cmp::min(28, 1 + days_offset as u8); let timestamp = format!("{:04}-{:02}-{:02}T{:02}:{:02}:00Z",
1299 target_year,
1300 target_month,
1301 target_day,
1302 hours_offset,
1303 minutes_offset
1304 );
1305 *timestamp_val = Value::String(timestamp);
1306 }
1307 }
1308 }
1309 }
1310 }
1311
1312 fn try_infer_total_from_context(
1315 spec: &OpenApiSpec,
1316 obj_type: &openapiv3::ObjectType,
1317 ) -> Option<u64> {
1318 if let Some(items_schema_ref) = obj_type.properties.get("items") {
1320 if let Some(components) = &spec.spec.components {
1323 let schemas = &components.schemas;
1324 for (schema_name, schema_ref) in schemas {
1327 if let ReferenceOr::Item(schema) = schema_ref {
1328 if let openapiv3::SchemaKind::Type(openapiv3::Type::Object(obj)) = &schema.schema_kind {
1329 for (prop_name, prop_schema) in &obj.properties {
1331 let prop_lower = prop_name.to_lowercase();
1332 if prop_lower.ends_with("_count") {
1333 let schema_ref: ReferenceOr<Schema> = match prop_schema {
1335 ReferenceOr::Item(boxed) => ReferenceOr::Item(boxed.as_ref().clone()),
1336 ReferenceOr::Reference { reference } => ReferenceOr::Reference { reference: reference.clone() },
1337 };
1338 if let Some(count) = Self::extract_numeric_value_from_schema(&schema_ref) {
1340 if count > 0 && count <= 1000 {
1342 tracing::debug!(
1343 "Inferred count {} from parent schema {} field {}",
1344 count,
1345 schema_name,
1346 prop_name
1347 );
1348 return Some(count);
1349 }
1350 }
1351 }
1352 }
1353 }
1354 }
1355 }
1356 }
1357 }
1358
1359 None
1360 }
1361
1362 fn infer_count_from_parent_schema(
1365 spec: &OpenApiSpec,
1366 parent_entity_name: &str,
1367 child_entity_name: &str,
1368 ) -> Option<u64> {
1369 let parent_schema_name = format!("{}", parent_entity_name);
1371 let count_field_name = format!("{}_count", child_entity_name);
1372
1373 if let Some(components) = &spec.spec.components {
1375 let schemas = &components.schemas;
1376 for (schema_name, schema_ref) in schemas {
1378 let schema_name_lower = schema_name.to_lowercase();
1379 if schema_name_lower.contains(&parent_entity_name.to_lowercase()) {
1380 if let ReferenceOr::Item(schema) = schema_ref {
1381 if let openapiv3::SchemaKind::Type(openapiv3::Type::Object(obj)) = &schema.schema_kind {
1383 for (prop_name, prop_schema) in &obj.properties {
1384 if prop_name.to_lowercase() == count_field_name.to_lowercase() {
1385 let schema_ref: ReferenceOr<Schema> = match prop_schema {
1387 ReferenceOr::Item(boxed) => ReferenceOr::Item(boxed.as_ref().clone()),
1388 ReferenceOr::Reference { reference } => ReferenceOr::Reference { reference: reference.clone() },
1389 };
1390 return Self::extract_numeric_value_from_schema(&schema_ref);
1392 }
1393 }
1394 }
1395 }
1396 }
1397 }
1398 }
1399
1400 None
1401 }
1402
1403 fn generate_example_for_property(prop_name: &str) -> Value {
1405 let prop_lower = prop_name.to_lowercase();
1406
1407 if prop_lower.contains("id") || prop_lower.contains("uuid") {
1409 Value::String(uuid::Uuid::new_v4().to_string())
1410 } else if prop_lower.contains("email") {
1411 Value::String(format!("user{}@example.com", rng().random_range(1000..=9999)))
1412 } else if prop_lower.contains("name") || prop_lower.contains("title") {
1413 let names = ["John Doe", "Jane Smith", "Bob Johnson", "Alice Brown"];
1414 Value::String(names[rng().random_range(0..names.len())].to_string())
1415 } else if prop_lower.contains("phone") || prop_lower.contains("mobile") {
1416 Value::String(format!("+1-555-{:04}", rng().random_range(1000..=9999)))
1417 } else if prop_lower.contains("address") || prop_lower.contains("street") {
1418 let streets = ["123 Main St", "456 Oak Ave", "789 Pine Rd", "321 Elm St"];
1419 Value::String(streets[rng().random_range(0..streets.len())].to_string())
1420 } else if prop_lower.contains("city") {
1421 let cities = ["New York", "London", "Tokyo", "Paris", "Sydney"];
1422 Value::String(cities[rng().random_range(0..cities.len())].to_string())
1423 } else if prop_lower.contains("country") {
1424 let countries = ["USA", "UK", "Japan", "France", "Australia"];
1425 Value::String(countries[rng().random_range(0..countries.len())].to_string())
1426 } else if prop_lower.contains("company") || prop_lower.contains("organization") {
1427 let companies = ["Acme Corp", "Tech Solutions", "Global Inc", "Innovate Ltd"];
1428 Value::String(companies[rng().random_range(0..companies.len())].to_string())
1429 } else if prop_lower.contains("url") || prop_lower.contains("website") {
1430 Value::String("https://example.com".to_string())
1431 } else if prop_lower.contains("age") {
1432 Value::Number((18 + rng().random_range(0..60)).into())
1433 } else if prop_lower.contains("count") || prop_lower.contains("quantity") {
1434 Value::Number((1 + rng().random_range(0..100)).into())
1435 } else if prop_lower.contains("price")
1436 || prop_lower.contains("amount")
1437 || prop_lower.contains("cost")
1438 {
1439 Value::Number(
1440 serde_json::Number::from_f64(
1441 (rng().random::<f64>() * 1000.0 * 100.0).round() / 100.0,
1442 )
1443 .unwrap(),
1444 )
1445 } else if prop_lower.contains("active")
1446 || prop_lower.contains("enabled")
1447 || prop_lower.contains("is_")
1448 {
1449 Value::Bool(rng().random_bool(0.5))
1450 } else if prop_lower.contains("date") || prop_lower.contains("time") {
1451 Value::String(chrono::Utc::now().to_rfc3339())
1452 } else if prop_lower.contains("description") || prop_lower.contains("comment") {
1453 Value::String("This is a sample description text.".to_string())
1454 } else {
1455 Value::String(format!("example {}", prop_name))
1456 }
1457 }
1458
1459 pub fn generate_from_examples(
1461 response: &Response,
1462 content_type: Option<&str>,
1463 ) -> Result<Option<Value>> {
1464 use openapiv3::ReferenceOr;
1465
1466 if let Some(content_type) = content_type {
1468 if let Some(media_type) = response.content.get(content_type) {
1469 if let Some(example) = &media_type.example {
1471 return Ok(Some(example.clone()));
1472 }
1473
1474 for (_, example_ref) in &media_type.examples {
1476 if let ReferenceOr::Item(example) = example_ref {
1477 if let Some(value) = &example.value {
1478 return Ok(Some(value.clone()));
1479 }
1480 }
1481 }
1483 }
1484 }
1485
1486 for (_, media_type) in &response.content {
1488 if let Some(example) = &media_type.example {
1490 return Ok(Some(example.clone()));
1491 }
1492
1493 for (_, example_ref) in &media_type.examples {
1495 if let ReferenceOr::Item(example) = example_ref {
1496 if let Some(value) = &example.value {
1497 return Ok(Some(value.clone()));
1498 }
1499 }
1500 }
1502 }
1503
1504 Ok(None)
1505 }
1506
1507 fn expand_templates(value: &Value) -> Value {
1509 match value {
1510 Value::String(s) => {
1511 let expanded = s
1512 .replace("{{now}}", &chrono::Utc::now().to_rfc3339())
1513 .replace("{{uuid}}", &uuid::Uuid::new_v4().to_string());
1514 Value::String(expanded)
1515 }
1516 Value::Object(map) => {
1517 let mut new_map = serde_json::Map::new();
1518 for (key, val) in map {
1519 new_map.insert(key.clone(), Self::expand_templates(val));
1520 }
1521 Value::Object(new_map)
1522 }
1523 Value::Array(arr) => {
1524 let new_arr: Vec<Value> = arr.iter().map(Self::expand_templates).collect();
1525 Value::Array(new_arr)
1526 }
1527 _ => value.clone(),
1528 }
1529 }
1530}
1531
1532#[cfg(test)]
1533mod tests {
1534 use super::*;
1535 use openapiv3::ReferenceOr;
1536
1537 #[test]
1538 fn generates_example_using_referenced_schemas() {
1539 let yaml = r#"
1540openapi: 3.0.3
1541info:
1542 title: Test API
1543 version: "1.0.0"
1544paths:
1545 /apiaries:
1546 get:
1547 responses:
1548 '200':
1549 description: ok
1550 content:
1551 application/json:
1552 schema:
1553 $ref: '#/components/schemas/Apiary'
1554components:
1555 schemas:
1556 Apiary:
1557 type: object
1558 properties:
1559 id:
1560 type: string
1561 hive:
1562 $ref: '#/components/schemas/Hive'
1563 Hive:
1564 type: object
1565 properties:
1566 name:
1567 type: string
1568 active:
1569 type: boolean
1570 "#;
1571
1572 let spec = OpenApiSpec::from_string(yaml, Some("yaml")).expect("load spec");
1573 let path_item = spec
1574 .spec
1575 .paths
1576 .paths
1577 .get("/apiaries")
1578 .and_then(ReferenceOr::as_item)
1579 .expect("path item");
1580 let operation = path_item.get.as_ref().expect("GET operation");
1581
1582 let response =
1583 ResponseGenerator::generate_response(&spec, operation, 200, Some("application/json"))
1584 .expect("generate response");
1585
1586 let obj = response.as_object().expect("response object");
1587 assert!(obj.contains_key("id"));
1588 let hive = obj.get("hive").and_then(|value| value.as_object()).expect("hive object");
1589 assert!(hive.contains_key("name"));
1590 assert!(hive.contains_key("active"));
1591 }
1592}
1593
1594#[derive(Debug, Clone)]
1596pub struct MockResponse {
1597 pub status_code: u16,
1599 pub headers: HashMap<String, String>,
1601 pub body: Option<Value>,
1603}
1604
1605impl MockResponse {
1606 pub fn new(status_code: u16) -> Self {
1608 Self {
1609 status_code,
1610 headers: HashMap::new(),
1611 body: None,
1612 }
1613 }
1614
1615 pub fn with_header(mut self, name: String, value: String) -> Self {
1617 self.headers.insert(name, value);
1618 self
1619 }
1620
1621 pub fn with_body(mut self, body: Value) -> Self {
1623 self.body = Some(body);
1624 self
1625 }
1626}
1627
1628#[derive(Debug, Clone)]
1630pub struct OpenApiSecurityRequirement {
1631 pub scheme: String,
1633 pub scopes: Vec<String>,
1635}
1636
1637impl OpenApiSecurityRequirement {
1638 pub fn new(scheme: String, scopes: Vec<String>) -> Self {
1640 Self { scheme, scopes }
1641 }
1642}
1643
1644#[derive(Debug, Clone)]
1646pub struct OpenApiOperation {
1647 pub method: String,
1649 pub path: String,
1651 pub operation: openapiv3::Operation,
1653}
1654
1655impl OpenApiOperation {
1656 pub fn new(method: String, path: String, operation: openapiv3::Operation) -> Self {
1658 Self {
1659 method,
1660 path,
1661 operation,
1662 }
1663 }
1664}