1use crate::intelligent_behavior::config::Persona;
7use crate::{
8 ai_response::{AiResponseConfig, RequestContext},
9 OpenApiSpec, Result,
10};
11use async_trait::async_trait;
12use chrono;
13use openapiv3::{Operation, ReferenceOr, Response, Responses, Schema};
14use rand::{thread_rng, Rng};
15use serde_json::Value;
16use std::collections::HashMap;
17use uuid;
18
19#[async_trait]
24pub trait AiGenerator: Send + Sync {
25 async fn generate(&self, prompt: &str, config: &AiResponseConfig) -> Result<Value>;
34}
35
36pub struct ResponseGenerator;
38
39impl ResponseGenerator {
40 pub async fn generate_ai_response(
53 ai_config: &AiResponseConfig,
54 context: &RequestContext,
55 generator: Option<&dyn AiGenerator>,
56 ) -> Result<Value> {
57 let prompt_template = ai_config
59 .prompt
60 .as_ref()
61 .ok_or_else(|| crate::Error::generic("AI prompt is required"))?;
62
63 let expanded_prompt = prompt_template
67 .replace("{{method}}", &context.method)
68 .replace("{{path}}", &context.path);
69
70 tracing::info!("AI response generation requested with prompt: {}", expanded_prompt);
71
72 if let Some(gen) = generator {
74 tracing::debug!("Using provided AI generator for response");
75 return gen.generate(&expanded_prompt, ai_config).await;
76 }
77
78 tracing::warn!("No AI generator provided, returning placeholder response");
80 Ok(serde_json::json!({
81 "ai_response": "AI generation placeholder",
82 "note": "This endpoint is configured for AI-assisted responses, but no AI generator was provided",
83 "expanded_prompt": expanded_prompt,
84 "mode": format!("{:?}", ai_config.mode),
85 "temperature": ai_config.temperature,
86 "implementation_note": "Pass an AiGenerator implementation to ResponseGenerator::generate_ai_response to enable actual AI generation"
87 }))
88 }
89
90 pub fn generate_response(
92 spec: &OpenApiSpec,
93 operation: &Operation,
94 status_code: u16,
95 content_type: Option<&str>,
96 ) -> Result<Value> {
97 Self::generate_response_with_expansion(spec, operation, status_code, content_type, true)
98 }
99
100 pub fn generate_response_with_expansion(
102 spec: &OpenApiSpec,
103 operation: &Operation,
104 status_code: u16,
105 content_type: Option<&str>,
106 expand_tokens: bool,
107 ) -> Result<Value> {
108 Self::generate_response_with_expansion_and_mode(
109 spec,
110 operation,
111 status_code,
112 content_type,
113 expand_tokens,
114 None,
115 None,
116 )
117 }
118
119 pub fn generate_response_with_expansion_and_mode(
121 spec: &OpenApiSpec,
122 operation: &Operation,
123 status_code: u16,
124 content_type: Option<&str>,
125 expand_tokens: bool,
126 selection_mode: Option<crate::openapi::response_selection::ResponseSelectionMode>,
127 selector: Option<&crate::openapi::response_selection::ResponseSelector>,
128 ) -> Result<Value> {
129 Self::generate_response_with_expansion_and_mode_and_persona(
130 spec,
131 operation,
132 status_code,
133 content_type,
134 expand_tokens,
135 selection_mode,
136 selector,
137 None, )
139 }
140
141 pub fn generate_response_with_expansion_and_mode_and_persona(
143 spec: &OpenApiSpec,
144 operation: &Operation,
145 status_code: u16,
146 content_type: Option<&str>,
147 expand_tokens: bool,
148 selection_mode: Option<crate::openapi::response_selection::ResponseSelectionMode>,
149 selector: Option<&crate::openapi::response_selection::ResponseSelector>,
150 persona: Option<&Persona>,
151 ) -> Result<Value> {
152 Self::generate_response_with_scenario_and_mode_and_persona(
153 spec,
154 operation,
155 status_code,
156 content_type,
157 expand_tokens,
158 None, selection_mode,
160 selector,
161 persona,
162 )
163 }
164
165 pub fn generate_response_with_scenario(
191 spec: &OpenApiSpec,
192 operation: &Operation,
193 status_code: u16,
194 content_type: Option<&str>,
195 expand_tokens: bool,
196 scenario: Option<&str>,
197 ) -> Result<Value> {
198 Self::generate_response_with_scenario_and_mode(
199 spec,
200 operation,
201 status_code,
202 content_type,
203 expand_tokens,
204 scenario,
205 None,
206 None,
207 )
208 }
209
210 pub fn generate_response_with_scenario_and_mode(
212 spec: &OpenApiSpec,
213 operation: &Operation,
214 status_code: u16,
215 content_type: Option<&str>,
216 expand_tokens: bool,
217 scenario: Option<&str>,
218 selection_mode: Option<crate::openapi::response_selection::ResponseSelectionMode>,
219 selector: Option<&crate::openapi::response_selection::ResponseSelector>,
220 ) -> Result<Value> {
221 Self::generate_response_with_scenario_and_mode_and_persona(
222 spec,
223 operation,
224 status_code,
225 content_type,
226 expand_tokens,
227 scenario,
228 selection_mode,
229 selector,
230 None, )
232 }
233
234 pub fn generate_response_with_scenario_and_mode_and_persona(
236 spec: &OpenApiSpec,
237 operation: &Operation,
238 status_code: u16,
239 content_type: Option<&str>,
240 expand_tokens: bool,
241 scenario: Option<&str>,
242 selection_mode: Option<crate::openapi::response_selection::ResponseSelectionMode>,
243 selector: Option<&crate::openapi::response_selection::ResponseSelector>,
244 persona: Option<&Persona>,
245 ) -> Result<Value> {
246 let response = Self::find_response_for_status(&operation.responses, status_code);
248
249 tracing::debug!(
250 "Finding response for status code {}: {:?}",
251 status_code,
252 if response.is_some() {
253 "found"
254 } else {
255 "not found"
256 }
257 );
258
259 match response {
260 Some(response_ref) => {
261 match response_ref {
262 ReferenceOr::Item(response) => {
263 tracing::debug!(
264 "Using direct response item with {} content types",
265 response.content.len()
266 );
267 Self::generate_from_response_with_scenario_and_mode(
268 spec,
269 response,
270 content_type,
271 expand_tokens,
272 scenario,
273 selection_mode,
274 selector,
275 )
276 }
277 ReferenceOr::Reference { reference } => {
278 tracing::debug!("Resolving response reference: {}", reference);
279 if let Some(resolved_response) = spec.get_response(reference) {
281 tracing::debug!(
282 "Resolved response reference with {} content types",
283 resolved_response.content.len()
284 );
285 Self::generate_from_response_with_scenario_and_mode(
286 spec,
287 resolved_response,
288 content_type,
289 expand_tokens,
290 scenario,
291 selection_mode,
292 selector,
293 )
294 } else {
295 tracing::warn!("Response reference '{}' not found in spec", reference);
296 Ok(Value::Object(serde_json::Map::new()))
298 }
299 }
300 }
301 }
302 None => {
303 tracing::warn!(
304 "No response found for status code {} in operation. Available status codes: {:?}",
305 status_code,
306 operation.responses.responses.keys().collect::<Vec<_>>()
307 );
308 Ok(Value::Object(serde_json::Map::new()))
310 }
311 }
312 }
313
314 fn find_response_for_status(
316 responses: &Responses,
317 status_code: u16,
318 ) -> Option<&ReferenceOr<Response>> {
319 if let Some(response) = responses.responses.get(&openapiv3::StatusCode::Code(status_code)) {
321 return Some(response);
322 }
323
324 if let Some(default_response) = &responses.default {
326 return Some(default_response);
327 }
328
329 None
330 }
331
332 fn generate_from_response(
334 spec: &OpenApiSpec,
335 response: &Response,
336 content_type: Option<&str>,
337 expand_tokens: bool,
338 ) -> Result<Value> {
339 Self::generate_from_response_with_scenario(
340 spec,
341 response,
342 content_type,
343 expand_tokens,
344 None,
345 )
346 }
347
348 fn generate_from_response_with_scenario(
350 spec: &OpenApiSpec,
351 response: &Response,
352 content_type: Option<&str>,
353 expand_tokens: bool,
354 scenario: Option<&str>,
355 ) -> Result<Value> {
356 Self::generate_from_response_with_scenario_and_mode(
357 spec,
358 response,
359 content_type,
360 expand_tokens,
361 scenario,
362 None,
363 None,
364 )
365 }
366
367 fn generate_from_response_with_scenario_and_mode(
369 spec: &OpenApiSpec,
370 response: &Response,
371 content_type: Option<&str>,
372 expand_tokens: bool,
373 scenario: Option<&str>,
374 selection_mode: Option<crate::openapi::response_selection::ResponseSelectionMode>,
375 selector: Option<&crate::openapi::response_selection::ResponseSelector>,
376 ) -> Result<Value> {
377 Self::generate_from_response_with_scenario_and_mode_and_persona(
378 spec,
379 response,
380 content_type,
381 expand_tokens,
382 scenario,
383 selection_mode,
384 selector,
385 None, )
387 }
388
389 fn generate_from_response_with_scenario_and_mode_and_persona(
391 spec: &OpenApiSpec,
392 response: &Response,
393 content_type: Option<&str>,
394 expand_tokens: bool,
395 scenario: Option<&str>,
396 selection_mode: Option<crate::openapi::response_selection::ResponseSelectionMode>,
397 selector: Option<&crate::openapi::response_selection::ResponseSelector>,
398 persona: Option<&Persona>,
399 ) -> Result<Value> {
400 if let Some(content_type) = content_type {
402 if let Some(media_type) = response.content.get(content_type) {
403 return Self::generate_from_media_type_with_scenario_and_mode_and_persona(
404 spec,
405 media_type,
406 expand_tokens,
407 scenario,
408 selection_mode,
409 selector,
410 persona,
411 );
412 }
413 }
414
415 let preferred_types = ["application/json", "application/xml", "text/plain"];
417
418 for content_type in &preferred_types {
419 if let Some(media_type) = response.content.get(*content_type) {
420 return Self::generate_from_media_type_with_scenario_and_mode_and_persona(
421 spec,
422 media_type,
423 expand_tokens,
424 scenario,
425 selection_mode,
426 selector,
427 persona,
428 );
429 }
430 }
431
432 if let Some((_, media_type)) = response.content.iter().next() {
434 return Self::generate_from_media_type_with_scenario_and_mode_and_persona(
435 spec,
436 media_type,
437 expand_tokens,
438 scenario,
439 selection_mode,
440 selector,
441 persona,
442 );
443 }
444
445 Ok(Value::Object(serde_json::Map::new()))
447 }
448
449 fn generate_from_media_type(
451 spec: &OpenApiSpec,
452 media_type: &openapiv3::MediaType,
453 expand_tokens: bool,
454 ) -> Result<Value> {
455 Self::generate_from_media_type_with_scenario(spec, media_type, expand_tokens, None)
456 }
457
458 fn generate_from_media_type_with_scenario(
460 spec: &OpenApiSpec,
461 media_type: &openapiv3::MediaType,
462 expand_tokens: bool,
463 scenario: Option<&str>,
464 ) -> Result<Value> {
465 Self::generate_from_media_type_with_scenario_and_mode(
466 spec,
467 media_type,
468 expand_tokens,
469 scenario,
470 None,
471 None,
472 )
473 }
474
475 fn generate_from_media_type_with_scenario_and_mode(
477 spec: &OpenApiSpec,
478 media_type: &openapiv3::MediaType,
479 expand_tokens: bool,
480 scenario: Option<&str>,
481 selection_mode: Option<crate::openapi::response_selection::ResponseSelectionMode>,
482 selector: Option<&crate::openapi::response_selection::ResponseSelector>,
483 ) -> Result<Value> {
484 Self::generate_from_media_type_with_scenario_and_mode_and_persona(
485 spec,
486 media_type,
487 expand_tokens,
488 scenario,
489 selection_mode,
490 selector,
491 None, )
493 }
494
495 fn generate_from_media_type_with_scenario_and_mode_and_persona(
497 spec: &OpenApiSpec,
498 media_type: &openapiv3::MediaType,
499 expand_tokens: bool,
500 scenario: Option<&str>,
501 selection_mode: Option<crate::openapi::response_selection::ResponseSelectionMode>,
502 selector: Option<&crate::openapi::response_selection::ResponseSelector>,
503 persona: Option<&Persona>,
504 ) -> Result<Value> {
505 if let Some(example) = &media_type.example {
509 tracing::debug!("Using explicit example from media type: {:?}", example);
510 if expand_tokens {
512 let expanded_example = Self::expand_templates(example);
513 return Ok(expanded_example);
514 } else {
515 return Ok(example.clone());
516 }
517 }
518
519 if !media_type.examples.is_empty() {
523 use crate::openapi::response_selection::{ResponseSelectionMode, ResponseSelector};
524
525 tracing::debug!(
526 "Found {} examples in media type, available examples: {:?}",
527 media_type.examples.len(),
528 media_type.examples.keys().collect::<Vec<_>>()
529 );
530
531 if let Some(scenario_name) = scenario {
533 if let Some(example_ref) = media_type.examples.get(scenario_name) {
534 tracing::debug!("Using scenario '{}' from examples map", scenario_name);
535 match Self::extract_example_value_with_persona(
536 spec,
537 example_ref,
538 expand_tokens,
539 persona,
540 media_type.schema.as_ref(),
541 ) {
542 Ok(value) => return Ok(value),
543 Err(e) => {
544 tracing::warn!(
545 "Failed to extract example for scenario '{}': {}, falling back",
546 scenario_name,
547 e
548 );
549 }
550 }
551 } else {
552 tracing::warn!(
553 "Scenario '{}' not found in examples, falling back based on selection mode",
554 scenario_name
555 );
556 }
557 }
558
559 let mode = selection_mode.unwrap_or(ResponseSelectionMode::First);
561
562 let example_names: Vec<String> = media_type.examples.keys().cloned().collect();
564
565 if example_names.is_empty() {
566 tracing::warn!("Examples map is empty, falling back to schema generation");
568 } else if mode == ResponseSelectionMode::Scenario && scenario.is_some() {
569 tracing::debug!("Scenario not found, using selection mode: {:?}", mode);
571 } else {
572 let selected_index = if let Some(sel) = selector {
574 sel.select(&example_names)
575 } else {
576 let temp_selector = ResponseSelector::new(mode);
578 temp_selector.select(&example_names)
579 };
580
581 if let Some(example_name) = example_names.get(selected_index) {
582 if let Some(example_ref) = media_type.examples.get(example_name) {
583 tracing::debug!(
584 "Using example '{}' from examples map (mode: {:?}, index: {})",
585 example_name,
586 mode,
587 selected_index
588 );
589 match Self::extract_example_value_with_persona(
590 spec,
591 example_ref,
592 expand_tokens,
593 persona,
594 media_type.schema.as_ref(),
595 ) {
596 Ok(value) => return Ok(value),
597 Err(e) => {
598 tracing::warn!(
599 "Failed to extract example '{}': {}, trying fallback",
600 example_name,
601 e
602 );
603 }
604 }
605 }
606 }
607 }
608
609 if let Some((example_name, example_ref)) = media_type.examples.iter().next() {
612 tracing::debug!(
613 "Using first example '{}' from examples map as fallback",
614 example_name
615 );
616 match Self::extract_example_value_with_persona(
617 spec,
618 example_ref,
619 expand_tokens,
620 persona,
621 media_type.schema.as_ref(),
622 ) {
623 Ok(value) => {
624 tracing::debug!(
625 "Successfully extracted fallback example '{}'",
626 example_name
627 );
628 return Ok(value);
629 }
630 Err(e) => {
631 tracing::error!(
632 "Failed to extract fallback example '{}': {}, falling back to schema generation",
633 example_name,
634 e
635 );
636 }
638 }
639 }
640 } else {
641 tracing::debug!("No examples found in media type, will use schema generation");
642 }
643
644 if let Some(schema_ref) = &media_type.schema {
647 Ok(Self::generate_example_from_schema_ref(spec, schema_ref, persona))
648 } else {
649 Ok(Value::Object(serde_json::Map::new()))
650 }
651 }
652
653 fn extract_example_value(
656 spec: &OpenApiSpec,
657 example_ref: &ReferenceOr<openapiv3::Example>,
658 expand_tokens: bool,
659 ) -> Result<Value> {
660 Self::extract_example_value_with_persona(spec, example_ref, expand_tokens, None, None)
661 }
662
663 fn extract_example_value_with_persona(
665 spec: &OpenApiSpec,
666 example_ref: &ReferenceOr<openapiv3::Example>,
667 expand_tokens: bool,
668 persona: Option<&Persona>,
669 schema_ref: Option<&ReferenceOr<Schema>>,
670 ) -> Result<Value> {
671 let mut value = match example_ref {
672 ReferenceOr::Item(example) => {
673 if let Some(v) = &example.value {
674 tracing::debug!("Using example from examples map: {:?}", v);
675 if expand_tokens {
676 Self::expand_templates(v)
677 } else {
678 v.clone()
679 }
680 } else {
681 return Ok(Value::Object(serde_json::Map::new()));
682 }
683 }
684 ReferenceOr::Reference { reference } => {
685 if let Some(example) = spec.get_example(reference) {
687 if let Some(v) = &example.value {
688 tracing::debug!("Using resolved example reference: {:?}", v);
689 if expand_tokens {
690 Self::expand_templates(v)
691 } else {
692 v.clone()
693 }
694 } else {
695 return Ok(Value::Object(serde_json::Map::new()));
696 }
697 } else {
698 tracing::warn!("Example reference '{}' not found", reference);
699 return Ok(Value::Object(serde_json::Map::new()));
700 }
701 }
702 };
703
704 value = Self::expand_example_items_if_needed(spec, value, persona, schema_ref);
706
707 Ok(value)
708 }
709
710 fn expand_example_items_if_needed(
713 spec: &OpenApiSpec,
714 mut example: Value,
715 persona: Option<&Persona>,
716 schema_ref: Option<&ReferenceOr<Schema>>,
717 ) -> Value {
718 let has_nested_items = example
721 .get("data")
722 .and_then(|v| v.as_object())
723 .map(|obj| obj.contains_key("items"))
724 .unwrap_or(false);
725
726 let has_flat_items = example.get("items").is_some();
727
728 if !has_nested_items && !has_flat_items {
729 return example; }
731
732 let total = example
734 .get("data")
735 .and_then(|d| d.get("total"))
736 .or_else(|| example.get("total"))
737 .and_then(|v| v.as_u64().or_else(|| v.as_i64().map(|i| i as u64)));
738
739 let limit = example
740 .get("data")
741 .and_then(|d| d.get("limit"))
742 .or_else(|| example.get("limit"))
743 .and_then(|v| v.as_u64().or_else(|| v.as_i64().map(|i| i as u64)));
744
745 let items_array = example
747 .get("data")
748 .and_then(|d| d.get("items"))
749 .or_else(|| example.get("items"))
750 .and_then(|v| v.as_array())
751 .cloned();
752
753 if let (Some(total_val), Some(limit_val), Some(mut items)) = (total, limit, items_array) {
754 let current_count = items.len() as u64;
755 let expected_count = std::cmp::min(total_val, limit_val);
756 let max_items = 100; let expected_count = std::cmp::min(expected_count, max_items);
758
759 if current_count < expected_count && !items.is_empty() {
761 tracing::debug!(
762 "Expanding example items array: {} -> {} items (total={}, limit={})",
763 current_count,
764 expected_count,
765 total_val,
766 limit_val
767 );
768
769 let template = items[0].clone();
771 let additional_count = expected_count - current_count;
772
773 for i in 0..additional_count {
775 let mut new_item = template.clone();
776 let item_index = current_count + i + 1;
778 Self::add_item_variation(&mut new_item, item_index);
779 items.push(new_item);
780 }
781
782 if let Some(data_obj) = example.get_mut("data").and_then(|v| v.as_object_mut()) {
784 data_obj.insert("items".to_string(), Value::Array(items));
785 } else if let Some(root_obj) = example.as_object_mut() {
786 root_obj.insert("items".to_string(), Value::Array(items));
787 }
788 }
789 }
790
791 example
792 }
793
794 fn generate_example_from_schema_ref(
795 spec: &OpenApiSpec,
796 schema_ref: &ReferenceOr<Schema>,
797 persona: Option<&Persona>,
798 ) -> Value {
799 match schema_ref {
800 ReferenceOr::Item(schema) => Self::generate_example_from_schema(spec, schema, persona),
801 ReferenceOr::Reference { reference } => spec
802 .get_schema(reference)
803 .map(|schema| Self::generate_example_from_schema(spec, &schema.schema, persona))
804 .unwrap_or_else(|| Value::Object(serde_json::Map::new())),
805 }
806 }
807
808 fn generate_example_from_schema(
816 spec: &OpenApiSpec,
817 schema: &Schema,
818 persona: Option<&Persona>,
819 ) -> Value {
820 if let Some(example) = schema.schema_data.example.as_ref() {
823 tracing::debug!("Using schema-level example: {:?}", example);
824 return example.clone();
825 }
826
827 match &schema.schema_kind {
831 openapiv3::SchemaKind::Type(openapiv3::Type::String(_)) => {
832 Value::String("example string".to_string())
834 }
835 openapiv3::SchemaKind::Type(openapiv3::Type::Integer(_)) => Value::Number(42.into()),
836 openapiv3::SchemaKind::Type(openapiv3::Type::Number(_)) => {
837 Value::Number(serde_json::Number::from_f64(std::f64::consts::PI).unwrap())
838 }
839 openapiv3::SchemaKind::Type(openapiv3::Type::Boolean(_)) => Value::Bool(true),
840 openapiv3::SchemaKind::Type(openapiv3::Type::Object(obj)) => {
841 let mut pagination_metadata: Option<(u64, u64, u64)> = None; let has_items =
848 obj.properties.iter().any(|(name, _)| name.to_lowercase() == "items");
849
850 if has_items {
851 let mut total_opt = None;
853 let mut page_opt = None;
854 let mut limit_opt = None;
855
856 for (prop_name, prop_schema) in &obj.properties {
857 let prop_lower = prop_name.to_lowercase();
858 let schema_ref: ReferenceOr<Schema> = match prop_schema {
860 ReferenceOr::Item(boxed) => ReferenceOr::Item(boxed.as_ref().clone()),
861 ReferenceOr::Reference { reference } => ReferenceOr::Reference {
862 reference: reference.clone(),
863 },
864 };
865 if prop_lower == "total" || prop_lower == "count" || prop_lower == "size" {
866 total_opt = Self::extract_numeric_value_from_schema(&schema_ref);
867 } else if prop_lower == "page" {
868 page_opt = Self::extract_numeric_value_from_schema(&schema_ref);
869 } else if prop_lower == "limit" || prop_lower == "per_page" {
870 limit_opt = Self::extract_numeric_value_from_schema(&schema_ref);
871 }
872 }
873
874 if let Some(total) = total_opt {
876 let page = page_opt.unwrap_or(1);
877 let limit = limit_opt.unwrap_or(20);
878 pagination_metadata = Some((total, page, limit));
879 tracing::debug!(
880 "Detected pagination metadata: total={}, page={}, limit={}",
881 total,
882 page,
883 limit
884 );
885 } else {
886 if obj.properties.contains_key("items") {
889 if let Some(inferred_total) =
893 Self::try_infer_total_from_context(spec, obj)
894 {
895 let page = page_opt.unwrap_or(1);
896 let limit = limit_opt.unwrap_or(20);
897 pagination_metadata = Some((inferred_total, page, limit));
898 tracing::debug!(
899 "Inferred pagination metadata from parent entity: total={}, page={}, limit={}",
900 inferred_total, page, limit
901 );
902 } else {
903 if let Some(persona) = persona {
905 let count_keys =
908 ["hive_count", "apiary_count", "item_count", "total_count"];
909 for key in &count_keys {
910 if let Some(count) = persona.get_numeric_trait(key) {
911 let page = page_opt.unwrap_or(1);
912 let limit = limit_opt.unwrap_or(20);
913 pagination_metadata = Some((count, page, limit));
914 tracing::debug!(
915 "Using persona trait '{}' for pagination: total={}, page={}, limit={}",
916 key, count, page, limit
917 );
918 break;
919 }
920 }
921 }
922 }
923 }
924 }
925 }
926
927 let mut map = serde_json::Map::new();
928 for (prop_name, prop_schema) in &obj.properties {
929 let prop_lower = prop_name.to_lowercase();
930
931 let is_items_array = prop_lower == "items" && pagination_metadata.is_some();
933
934 let value = match prop_schema {
935 ReferenceOr::Item(prop_schema) => {
936 if is_items_array {
939 Self::generate_array_with_count(
941 spec,
942 prop_schema.as_ref(),
943 pagination_metadata.unwrap(),
944 persona,
945 )
946 } else if let Some(prop_example) =
947 prop_schema.schema_data.example.as_ref()
948 {
949 tracing::debug!(
951 "Using example for property '{}': {:?}",
952 prop_name,
953 prop_example
954 );
955 prop_example.clone()
956 } else {
957 Self::generate_example_from_schema(
958 spec,
959 prop_schema.as_ref(),
960 persona,
961 )
962 }
963 }
964 ReferenceOr::Reference { reference } => {
965 if let Some(resolved_schema) = spec.get_schema(reference) {
967 if is_items_array {
969 Self::generate_array_with_count(
971 spec,
972 &resolved_schema.schema,
973 pagination_metadata.unwrap(),
974 persona,
975 )
976 } else if let Some(ref_example) =
977 resolved_schema.schema.schema_data.example.as_ref()
978 {
979 tracing::debug!(
981 "Using example from referenced schema '{}': {:?}",
982 reference,
983 ref_example
984 );
985 ref_example.clone()
986 } else {
987 Self::generate_example_from_schema(
988 spec,
989 &resolved_schema.schema,
990 persona,
991 )
992 }
993 } else {
994 Self::generate_example_for_property(prop_name)
995 }
996 }
997 };
998 let value = match value {
999 Value::Null => Self::generate_example_for_property(prop_name),
1000 Value::Object(ref obj) if obj.is_empty() => {
1001 Self::generate_example_for_property(prop_name)
1002 }
1003 _ => value,
1004 };
1005 map.insert(prop_name.clone(), value);
1006 }
1007
1008 if let Some((total, page, limit)) = pagination_metadata {
1010 map.insert("total".to_string(), Value::Number(total.into()));
1011 map.insert("page".to_string(), Value::Number(page.into()));
1012 map.insert("limit".to_string(), Value::Number(limit.into()));
1013 }
1014
1015 Value::Object(map)
1016 }
1017 openapiv3::SchemaKind::Type(openapiv3::Type::Array(arr)) => {
1018 match &arr.items {
1024 Some(item_schema) => {
1025 let example_item = match item_schema {
1026 ReferenceOr::Item(item_schema) => {
1027 Self::generate_example_from_schema(
1030 spec,
1031 item_schema.as_ref(),
1032 persona,
1033 )
1034 }
1035 ReferenceOr::Reference { reference } => {
1036 if let Some(resolved_schema) = spec.get_schema(reference) {
1039 Self::generate_example_from_schema(
1040 spec,
1041 &resolved_schema.schema,
1042 persona,
1043 )
1044 } else {
1045 Value::Object(serde_json::Map::new())
1046 }
1047 }
1048 };
1049 Value::Array(vec![example_item])
1050 }
1051 None => Value::Array(vec![Value::String("item".to_string())]),
1052 }
1053 }
1054 _ => Value::Object(serde_json::Map::new()),
1055 }
1056 }
1057
1058 fn extract_numeric_value_from_schema(schema_ref: &ReferenceOr<Schema>) -> Option<u64> {
1061 match schema_ref {
1062 ReferenceOr::Item(schema) => {
1063 if let Some(example) = schema.schema_data.example.as_ref() {
1065 if let Some(num) = example.as_u64() {
1066 return Some(num);
1067 } else if let Some(num) = example.as_f64() {
1068 return Some(num as u64);
1069 }
1070 }
1071 if let Some(default) = schema.schema_data.default.as_ref() {
1073 if let Some(num) = default.as_u64() {
1074 return Some(num);
1075 } else if let Some(num) = default.as_f64() {
1076 return Some(num as u64);
1077 }
1078 }
1079 None
1083 }
1084 ReferenceOr::Reference { reference: _ } => {
1085 None
1088 }
1089 }
1090 }
1091
1092 fn generate_array_with_count(
1095 spec: &OpenApiSpec,
1096 array_schema: &Schema,
1097 pagination: (u64, u64, u64), persona: Option<&Persona>,
1099 ) -> Value {
1100 let (total, _page, limit) = pagination;
1101
1102 let count = std::cmp::min(total, limit);
1105
1106 let max_items = 100;
1108 let count = std::cmp::min(count, max_items);
1109
1110 tracing::debug!("Generating array with count={} (total={}, limit={})", count, total, limit);
1111
1112 if let Some(example) = array_schema.schema_data.example.as_ref() {
1114 if let Some(example_array) = example.as_array() {
1115 if !example_array.is_empty() {
1116 let template_item = &example_array[0];
1118 let items: Vec<Value> = (0..count)
1119 .map(|i| {
1120 let mut item = template_item.clone();
1122 Self::add_item_variation(&mut item, i + 1);
1123 item
1124 })
1125 .collect();
1126 return Value::Array(items);
1127 }
1128 }
1129 }
1130
1131 if let openapiv3::SchemaKind::Type(openapiv3::Type::Array(arr)) = &array_schema.schema_kind
1133 {
1134 if let Some(item_schema) = &arr.items {
1135 let items: Vec<Value> = match item_schema {
1136 ReferenceOr::Item(item_schema) => {
1137 (0..count)
1138 .map(|i| {
1139 let mut item = Self::generate_example_from_schema(
1140 spec,
1141 item_schema.as_ref(),
1142 persona,
1143 );
1144 Self::add_item_variation(&mut item, i + 1);
1146 item
1147 })
1148 .collect()
1149 }
1150 ReferenceOr::Reference { reference } => {
1151 if let Some(resolved_schema) = spec.get_schema(reference) {
1152 (0..count)
1153 .map(|i| {
1154 let mut item = Self::generate_example_from_schema(
1155 spec,
1156 &resolved_schema.schema,
1157 persona,
1158 );
1159 Self::add_item_variation(&mut item, i + 1);
1161 item
1162 })
1163 .collect()
1164 } else {
1165 vec![Value::Object(serde_json::Map::new()); count as usize]
1166 }
1167 }
1168 };
1169 return Value::Array(items);
1170 }
1171 }
1172
1173 Value::Array((0..count).map(|i| Value::String(format!("item_{}", i + 1))).collect())
1175 }
1176
1177 fn add_item_variation(item: &mut Value, item_index: u64) {
1180 if let Some(obj) = item.as_object_mut() {
1181 if let Some(id_val) = obj.get_mut("id") {
1183 if let Some(id_str) = id_val.as_str() {
1184 let base_id = id_str.split('_').next().unwrap_or(id_str);
1186 *id_val = Value::String(format!("{}_{:03}", base_id, item_index));
1187 } else if let Some(id_num) = id_val.as_u64() {
1188 *id_val = Value::Number((id_num + item_index).into());
1189 }
1190 }
1191
1192 if let Some(name_val) = obj.get_mut("name") {
1194 if let Some(name_str) = name_val.as_str() {
1195 if name_str.contains('#') {
1196 *name_val = Value::String(format!("Hive #{}", item_index));
1198 } else {
1199 let apiary_names = [
1202 "Meadow Apiary",
1204 "Prairie Apiary",
1205 "Sunset Valley Apiary",
1206 "Golden Fields Apiary",
1207 "Miller Family Apiary",
1208 "Heartland Honey Co.",
1209 "Cornfield Apiary",
1210 "Harvest Moon Apiary",
1211 "Prairie Winds Apiary",
1212 "Amber Fields Apiary",
1213 "Coastal Apiary",
1215 "Sunset Coast Apiary",
1216 "Pacific Grove Apiary",
1217 "Golden Gate Apiary",
1218 "Napa Valley Apiary",
1219 "Coastal Breeze Apiary",
1220 "Pacific Heights Apiary",
1221 "Bay Area Apiary",
1222 "Sunset Valley Honey Co.",
1223 "Coastal Harvest Apiary",
1224 "Lone Star Apiary",
1226 "Texas Ranch Apiary",
1227 "Big Sky Apiary",
1228 "Prairie Rose Apiary",
1229 "Hill Country Apiary",
1230 "Lone Star Honey Co.",
1231 "Texas Pride Apiary",
1232 "Wildflower Ranch",
1233 "Desert Bloom Apiary",
1234 "Cactus Creek Apiary",
1235 "Orange Grove Apiary",
1237 "Citrus Grove Apiary",
1238 "Palm Grove Apiary",
1239 "Tropical Breeze Apiary",
1240 "Everglades Apiary",
1241 "Sunshine State Apiary",
1242 "Florida Keys Apiary",
1243 "Grove View Apiary",
1244 "Tropical Harvest Apiary",
1245 "Palm Coast Apiary",
1246 "Mountain View Apiary",
1248 "Valley Apiary",
1249 "Riverside Apiary",
1250 "Hilltop Apiary",
1251 "Forest Apiary",
1252 "Mountain Apiary",
1253 "Lakeside Apiary",
1254 "Ridge Apiary",
1255 "Brook Apiary",
1256 "Hillside Apiary",
1257 "Field Apiary",
1259 "Creek Apiary",
1260 "Woodland Apiary",
1261 "Farm Apiary",
1262 "Orchard Apiary",
1263 "Pasture Apiary",
1264 "Green Valley Apiary",
1265 "Blue Sky Apiary",
1266 "Sweet Honey Apiary",
1267 "Nature's Best Apiary",
1268 "Premium Honey Co.",
1270 "Artisan Apiary",
1271 "Heritage Apiary",
1272 "Summit Apiary",
1273 "Crystal Springs Apiary",
1274 "Maple Grove Apiary",
1275 "Wildflower Apiary",
1276 "Thistle Apiary",
1277 "Clover Field Apiary",
1278 "Honeycomb Apiary",
1279 ];
1280 let name_index = (item_index - 1) as usize % apiary_names.len();
1281 *name_val = Value::String(apiary_names[name_index].to_string());
1282 }
1283 }
1284 }
1285
1286 if let Some(location_val) = obj.get_mut("location") {
1288 if let Some(location_obj) = location_val.as_object_mut() {
1289 if let Some(address_val) = location_obj.get_mut("address") {
1291 if let Some(address_str) = address_val.as_str() {
1292 if let Some(num_str) = address_str.split_whitespace().next() {
1294 if let Ok(num) = num_str.parse::<u64>() {
1295 *address_val =
1296 Value::String(format!("{} Farm Road", num + item_index));
1297 } else {
1298 *address_val =
1299 Value::String(format!("{} Farm Road", 100 + item_index));
1300 }
1301 } else {
1302 *address_val =
1303 Value::String(format!("{} Farm Road", 100 + item_index));
1304 }
1305 }
1306 }
1307
1308 if let Some(lat_val) = location_obj.get_mut("latitude") {
1310 if let Some(lat) = lat_val.as_f64() {
1311 *lat_val = Value::Number(
1312 serde_json::Number::from_f64(lat + (item_index as f64 * 0.01))
1313 .unwrap(),
1314 );
1315 }
1316 }
1317 if let Some(lng_val) = location_obj.get_mut("longitude") {
1318 if let Some(lng) = lng_val.as_f64() {
1319 *lng_val = Value::Number(
1320 serde_json::Number::from_f64(lng + (item_index as f64 * 0.01))
1321 .unwrap(),
1322 );
1323 }
1324 }
1325 } else if let Some(address_str) = location_val.as_str() {
1326 if let Some(num_str) = address_str.split_whitespace().next() {
1328 if let Ok(num) = num_str.parse::<u64>() {
1329 *location_val =
1330 Value::String(format!("{} Farm Road", num + item_index));
1331 } else {
1332 *location_val =
1333 Value::String(format!("{} Farm Road", 100 + item_index));
1334 }
1335 }
1336 }
1337 }
1338
1339 if let Some(address_val) = obj.get_mut("address") {
1341 if let Some(address_str) = address_val.as_str() {
1342 if let Some(num_str) = address_str.split_whitespace().next() {
1343 if let Ok(num) = num_str.parse::<u64>() {
1344 *address_val = Value::String(format!("{} Farm Road", num + item_index));
1345 } else {
1346 *address_val = Value::String(format!("{} Farm Road", 100 + item_index));
1347 }
1348 }
1349 }
1350 }
1351
1352 if let Some(status_val) = obj.get_mut("status") {
1354 if let Some(status_str) = status_val.as_str() {
1355 let statuses = [
1356 "healthy",
1357 "sick",
1358 "needs_attention",
1359 "quarantined",
1360 "active",
1361 "inactive",
1362 ];
1363 let status_index = (item_index - 1) as usize % statuses.len();
1364 let final_status = if (item_index - 1) % 10 < 7 {
1366 statuses[0] } else {
1368 statuses[status_index]
1369 };
1370 *status_val = Value::String(final_status.to_string());
1371 }
1372 }
1373
1374 if let Some(hive_type_val) = obj.get_mut("hive_type") {
1376 if hive_type_val.as_str().is_some() {
1377 let hive_types = ["langstroth", "top_bar", "warre", "flow_hive", "national"];
1378 let type_index = (item_index - 1) as usize % hive_types.len();
1379 *hive_type_val = Value::String(hive_types[type_index].to_string());
1380 }
1381 }
1382
1383 if let Some(queen_val) = obj.get_mut("queen") {
1385 if let Some(queen_obj) = queen_val.as_object_mut() {
1386 if let Some(breed_val) = queen_obj.get_mut("breed") {
1387 if breed_val.as_str().is_some() {
1388 let breeds =
1389 ["italian", "carniolan", "russian", "buckfast", "caucasian"];
1390 let breed_index = (item_index - 1) as usize % breeds.len();
1391 *breed_val = Value::String(breeds[breed_index].to_string());
1392 }
1393 }
1394 if let Some(age_val) = queen_obj.get_mut("age_days") {
1396 if let Some(base_age) = age_val.as_u64() {
1397 *age_val = Value::Number((base_age + (item_index * 10) % 200).into());
1398 } else if let Some(base_age) = age_val.as_i64() {
1399 *age_val =
1400 Value::Number((base_age + (item_index as i64 * 10) % 200).into());
1401 }
1402 }
1403 if let Some(color_val) = queen_obj.get_mut("mark_color") {
1405 if color_val.as_str().is_some() {
1406 let colors = ["yellow", "white", "red", "green", "blue"];
1407 let color_index = (item_index - 1) as usize % colors.len();
1408 *color_val = Value::String(colors[color_index].to_string());
1409 }
1410 }
1411 }
1412 }
1413
1414 if let Some(desc_val) = obj.get_mut("description") {
1416 if let Some(desc_str) = desc_val.as_str() {
1417 let descriptions = [
1418 "Production apiary",
1419 "Research apiary",
1420 "Commercial operation",
1421 "Backyard apiary",
1422 "Educational apiary",
1423 ];
1424 let desc_index = (item_index - 1) as usize % descriptions.len();
1425 *desc_val = Value::String(descriptions[desc_index].to_string());
1426 }
1427 }
1428
1429 let timestamp_fields = [
1432 "created_at",
1433 "updated_at",
1434 "timestamp",
1435 "date",
1436 "forecastDate",
1437 "predictedDate",
1438 ];
1439 for field_name in ×tamp_fields {
1440 if let Some(timestamp_val) = obj.get_mut(*field_name) {
1441 if let Some(_timestamp_str) = timestamp_val.as_str() {
1442 let months_ago = 12 + ((item_index - 1) % 6); let days_offset = (item_index - 1) % 28; let hours_offset = ((item_index * 7) % 24) as u8; let minutes_offset = ((item_index * 11) % 60) as u8; let base_year = 2024;
1452 let base_month = 11;
1453
1454 let target_year = if months_ago >= base_month as u64 {
1456 base_year - 1
1457 } else {
1458 base_year
1459 };
1460 let target_month = if months_ago >= base_month as u64 {
1461 12 - (months_ago - base_month as u64) as u8
1462 } else {
1463 (base_month as u64 - months_ago) as u8
1464 };
1465 let target_day = std::cmp::min(28, 1 + days_offset as u8); let timestamp = format!(
1469 "{:04}-{:02}-{:02}T{:02}:{:02}:00Z",
1470 target_year, target_month, target_day, hours_offset, minutes_offset
1471 );
1472 *timestamp_val = Value::String(timestamp);
1473 }
1474 }
1475 }
1476 }
1477 }
1478
1479 fn try_infer_total_from_context(
1482 spec: &OpenApiSpec,
1483 obj_type: &openapiv3::ObjectType,
1484 ) -> Option<u64> {
1485 if let Some(items_schema_ref) = obj_type.properties.get("items") {
1487 if let Some(components) = &spec.spec.components {
1490 let schemas = &components.schemas;
1491 for (schema_name, schema_ref) in schemas {
1494 if let ReferenceOr::Item(schema) = schema_ref {
1495 if let openapiv3::SchemaKind::Type(openapiv3::Type::Object(obj)) =
1496 &schema.schema_kind
1497 {
1498 for (prop_name, prop_schema) in &obj.properties {
1500 let prop_lower = prop_name.to_lowercase();
1501 if prop_lower.ends_with("_count") {
1502 let schema_ref: ReferenceOr<Schema> = match prop_schema {
1504 ReferenceOr::Item(boxed) => {
1505 ReferenceOr::Item(boxed.as_ref().clone())
1506 }
1507 ReferenceOr::Reference { reference } => {
1508 ReferenceOr::Reference {
1509 reference: reference.clone(),
1510 }
1511 }
1512 };
1513 if let Some(count) =
1515 Self::extract_numeric_value_from_schema(&schema_ref)
1516 {
1517 if count > 0 && count <= 1000 {
1519 tracing::debug!(
1520 "Inferred count {} from parent schema {} field {}",
1521 count,
1522 schema_name,
1523 prop_name
1524 );
1525 return Some(count);
1526 }
1527 }
1528 }
1529 }
1530 }
1531 }
1532 }
1533 }
1534 }
1535
1536 None
1537 }
1538
1539 fn infer_count_from_parent_schema(
1542 spec: &OpenApiSpec,
1543 parent_entity_name: &str,
1544 child_entity_name: &str,
1545 ) -> Option<u64> {
1546 let parent_schema_name = parent_entity_name.to_string();
1548 let count_field_name = format!("{}_count", child_entity_name);
1549
1550 if let Some(components) = &spec.spec.components {
1552 let schemas = &components.schemas;
1553 for (schema_name, schema_ref) in schemas {
1555 let schema_name_lower = schema_name.to_lowercase();
1556 if schema_name_lower.contains(&parent_entity_name.to_lowercase()) {
1557 if let ReferenceOr::Item(schema) = schema_ref {
1558 if let openapiv3::SchemaKind::Type(openapiv3::Type::Object(obj)) =
1560 &schema.schema_kind
1561 {
1562 for (prop_name, prop_schema) in &obj.properties {
1563 if prop_name.to_lowercase() == count_field_name.to_lowercase() {
1564 let schema_ref: ReferenceOr<Schema> = match prop_schema {
1566 ReferenceOr::Item(boxed) => {
1567 ReferenceOr::Item(boxed.as_ref().clone())
1568 }
1569 ReferenceOr::Reference { reference } => {
1570 ReferenceOr::Reference {
1571 reference: reference.clone(),
1572 }
1573 }
1574 };
1575 return Self::extract_numeric_value_from_schema(&schema_ref);
1577 }
1578 }
1579 }
1580 }
1581 }
1582 }
1583 }
1584
1585 None
1586 }
1587
1588 fn generate_example_for_property(prop_name: &str) -> Value {
1590 let prop_lower = prop_name.to_lowercase();
1591
1592 if prop_lower.contains("id") || prop_lower.contains("uuid") {
1594 Value::String(uuid::Uuid::new_v4().to_string())
1595 } else if prop_lower.contains("email") {
1596 Value::String(format!("user{}@example.com", thread_rng().random_range(1000..=9999)))
1597 } else if prop_lower.contains("name") || prop_lower.contains("title") {
1598 let names = ["John Doe", "Jane Smith", "Bob Johnson", "Alice Brown"];
1599 Value::String(names[thread_rng().random_range(0..names.len())].to_string())
1600 } else if prop_lower.contains("phone") || prop_lower.contains("mobile") {
1601 Value::String(format!("+1-555-{:04}", thread_rng().random_range(1000..=9999)))
1602 } else if prop_lower.contains("address") || prop_lower.contains("street") {
1603 let streets = ["123 Main St", "456 Oak Ave", "789 Pine Rd", "321 Elm St"];
1604 Value::String(streets[thread_rng().random_range(0..streets.len())].to_string())
1605 } else if prop_lower.contains("city") {
1606 let cities = ["New York", "London", "Tokyo", "Paris", "Sydney"];
1607 Value::String(cities[thread_rng().random_range(0..cities.len())].to_string())
1608 } else if prop_lower.contains("country") {
1609 let countries = ["USA", "UK", "Japan", "France", "Australia"];
1610 Value::String(countries[thread_rng().random_range(0..countries.len())].to_string())
1611 } else if prop_lower.contains("company") || prop_lower.contains("organization") {
1612 let companies = ["Acme Corp", "Tech Solutions", "Global Inc", "Innovate Ltd"];
1613 Value::String(companies[thread_rng().random_range(0..companies.len())].to_string())
1614 } else if prop_lower.contains("url") || prop_lower.contains("website") {
1615 Value::String("https://example.com".to_string())
1616 } else if prop_lower.contains("age") {
1617 Value::Number((18 + thread_rng().random_range(0..60)).into())
1618 } else if prop_lower.contains("count") || prop_lower.contains("quantity") {
1619 Value::Number((1 + thread_rng().random_range(0..100)).into())
1620 } else if prop_lower.contains("price")
1621 || prop_lower.contains("amount")
1622 || prop_lower.contains("cost")
1623 {
1624 Value::Number(
1625 serde_json::Number::from_f64(
1626 (thread_rng().random::<f64>() * 1000.0 * 100.0).round() / 100.0,
1627 )
1628 .unwrap(),
1629 )
1630 } else if prop_lower.contains("active")
1631 || prop_lower.contains("enabled")
1632 || prop_lower.contains("is_")
1633 {
1634 Value::Bool(thread_rng().random_bool(0.5))
1635 } else if prop_lower.contains("date") || prop_lower.contains("time") {
1636 Value::String(chrono::Utc::now().to_rfc3339())
1637 } else if prop_lower.contains("description") || prop_lower.contains("comment") {
1638 Value::String("This is a sample description text.".to_string())
1639 } else {
1640 Value::String(format!("example {}", prop_name))
1641 }
1642 }
1643
1644 pub fn generate_from_examples(
1646 response: &Response,
1647 content_type: Option<&str>,
1648 ) -> Result<Option<Value>> {
1649 use openapiv3::ReferenceOr;
1650
1651 if let Some(content_type) = content_type {
1653 if let Some(media_type) = response.content.get(content_type) {
1654 if let Some(example) = &media_type.example {
1656 return Ok(Some(example.clone()));
1657 }
1658
1659 for (_, example_ref) in &media_type.examples {
1661 if let ReferenceOr::Item(example) = example_ref {
1662 if let Some(value) = &example.value {
1663 return Ok(Some(value.clone()));
1664 }
1665 }
1666 }
1668 }
1669 }
1670
1671 for (_, media_type) in &response.content {
1673 if let Some(example) = &media_type.example {
1675 return Ok(Some(example.clone()));
1676 }
1677
1678 for (_, example_ref) in &media_type.examples {
1680 if let ReferenceOr::Item(example) = example_ref {
1681 if let Some(value) = &example.value {
1682 return Ok(Some(value.clone()));
1683 }
1684 }
1685 }
1687 }
1688
1689 Ok(None)
1690 }
1691
1692 fn expand_templates(value: &Value) -> Value {
1694 match value {
1695 Value::String(s) => {
1696 let expanded = s
1697 .replace("{{now}}", &chrono::Utc::now().to_rfc3339())
1698 .replace("{{uuid}}", &uuid::Uuid::new_v4().to_string());
1699 Value::String(expanded)
1700 }
1701 Value::Object(map) => {
1702 let mut new_map = serde_json::Map::new();
1703 for (key, val) in map {
1704 new_map.insert(key.clone(), Self::expand_templates(val));
1705 }
1706 Value::Object(new_map)
1707 }
1708 Value::Array(arr) => {
1709 let new_arr: Vec<Value> = arr.iter().map(Self::expand_templates).collect();
1710 Value::Array(new_arr)
1711 }
1712 _ => value.clone(),
1713 }
1714 }
1715}
1716
1717#[cfg(test)]
1718mod tests {
1719 use super::*;
1720 use openapiv3::ReferenceOr;
1721
1722 #[test]
1723 fn generates_example_using_referenced_schemas() {
1724 let yaml = r#"
1725openapi: 3.0.3
1726info:
1727 title: Test API
1728 version: "1.0.0"
1729paths:
1730 /apiaries:
1731 get:
1732 responses:
1733 '200':
1734 description: ok
1735 content:
1736 application/json:
1737 schema:
1738 $ref: '#/components/schemas/Apiary'
1739components:
1740 schemas:
1741 Apiary:
1742 type: object
1743 properties:
1744 id:
1745 type: string
1746 hive:
1747 $ref: '#/components/schemas/Hive'
1748 Hive:
1749 type: object
1750 properties:
1751 name:
1752 type: string
1753 active:
1754 type: boolean
1755 "#;
1756
1757 let spec = OpenApiSpec::from_string(yaml, Some("yaml")).expect("load spec");
1758 let path_item = spec
1759 .spec
1760 .paths
1761 .paths
1762 .get("/apiaries")
1763 .and_then(ReferenceOr::as_item)
1764 .expect("path item");
1765 let operation = path_item.get.as_ref().expect("GET operation");
1766
1767 let response =
1768 ResponseGenerator::generate_response(&spec, operation, 200, Some("application/json"))
1769 .expect("generate response");
1770
1771 let obj = response.as_object().expect("response object");
1772 assert!(obj.contains_key("id"));
1773 let hive = obj.get("hive").and_then(|value| value.as_object()).expect("hive object");
1774 assert!(hive.contains_key("name"));
1775 assert!(hive.contains_key("active"));
1776 }
1777}
1778
1779#[derive(Debug, Clone)]
1781pub struct MockResponse {
1782 pub status_code: u16,
1784 pub headers: HashMap<String, String>,
1786 pub body: Option<Value>,
1788}
1789
1790impl MockResponse {
1791 pub fn new(status_code: u16) -> Self {
1793 Self {
1794 status_code,
1795 headers: HashMap::new(),
1796 body: None,
1797 }
1798 }
1799
1800 pub fn with_header(mut self, name: String, value: String) -> Self {
1802 self.headers.insert(name, value);
1803 self
1804 }
1805
1806 pub fn with_body(mut self, body: Value) -> Self {
1808 self.body = Some(body);
1809 self
1810 }
1811}
1812
1813#[derive(Debug, Clone)]
1815pub struct OpenApiSecurityRequirement {
1816 pub scheme: String,
1818 pub scopes: Vec<String>,
1820}
1821
1822impl OpenApiSecurityRequirement {
1823 pub fn new(scheme: String, scopes: Vec<String>) -> Self {
1825 Self { scheme, scopes }
1826 }
1827}
1828
1829#[derive(Debug, Clone)]
1831pub struct OpenApiOperation {
1832 pub method: String,
1834 pub path: String,
1836 pub operation: openapiv3::Operation,
1838}
1839
1840impl OpenApiOperation {
1841 pub fn new(method: String, path: String, operation: openapiv3::Operation) -> Self {
1843 Self {
1844 method,
1845 path,
1846 operation,
1847 }
1848 }
1849}