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