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