1use crate::{
7 ai_response::{expand_prompt_template, AiResponseConfig, RequestContext},
8 OpenApiSpec, Result,
9};
10use async_trait::async_trait;
11use chrono;
12use openapiv3::{Operation, ReferenceOr, Response, Responses, Schema};
13use rand::{rng, Rng};
14use serde_json::Value;
15use std::collections::HashMap;
16use uuid;
17
18#[async_trait]
23pub trait AiGenerator: Send + Sync {
24 async fn generate(&self, prompt: &str, config: &AiResponseConfig) -> Result<Value>;
33}
34
35pub struct ResponseGenerator;
37
38impl ResponseGenerator {
39 pub async fn generate_ai_response(
52 ai_config: &AiResponseConfig,
53 context: &RequestContext,
54 generator: Option<&dyn AiGenerator>,
55 ) -> Result<Value> {
56 let prompt_template = ai_config
58 .prompt
59 .as_ref()
60 .ok_or_else(|| crate::Error::generic("AI prompt is required"))?;
61
62 let expanded_prompt = expand_prompt_template(prompt_template, context);
63
64 tracing::info!("AI response generation requested with prompt: {}", expanded_prompt);
65
66 if let Some(gen) = generator {
68 tracing::debug!("Using provided AI generator for response");
69 return gen.generate(&expanded_prompt, ai_config).await;
70 }
71
72 tracing::warn!("No AI generator provided, returning placeholder response");
74 Ok(serde_json::json!({
75 "ai_response": "AI generation placeholder",
76 "note": "This endpoint is configured for AI-assisted responses, but no AI generator was provided",
77 "expanded_prompt": expanded_prompt,
78 "mode": format!("{:?}", ai_config.mode),
79 "temperature": ai_config.temperature,
80 "implementation_note": "Pass an AiGenerator implementation to ResponseGenerator::generate_ai_response to enable actual AI generation"
81 }))
82 }
83
84 pub fn generate_response(
86 spec: &OpenApiSpec,
87 operation: &Operation,
88 status_code: u16,
89 content_type: Option<&str>,
90 ) -> Result<Value> {
91 Self::generate_response_with_expansion(spec, operation, status_code, content_type, true)
92 }
93
94 pub fn generate_response_with_expansion(
96 spec: &OpenApiSpec,
97 operation: &Operation,
98 status_code: u16,
99 content_type: Option<&str>,
100 expand_tokens: bool,
101 ) -> Result<Value> {
102 Self::generate_response_with_scenario(
103 spec,
104 operation,
105 status_code,
106 content_type,
107 expand_tokens,
108 None,
109 )
110 }
111
112 pub fn generate_response_with_scenario(
138 spec: &OpenApiSpec,
139 operation: &Operation,
140 status_code: u16,
141 content_type: Option<&str>,
142 expand_tokens: bool,
143 scenario: Option<&str>,
144 ) -> Result<Value> {
145 let response = Self::find_response_for_status(&operation.responses, status_code);
147
148 match response {
149 Some(response_ref) => {
150 match response_ref {
151 ReferenceOr::Item(response) => Self::generate_from_response_with_scenario(
152 spec,
153 response,
154 content_type,
155 expand_tokens,
156 scenario,
157 ),
158 ReferenceOr::Reference { reference } => {
159 if let Some(resolved_response) = spec.get_response(reference) {
161 Self::generate_from_response_with_scenario(
162 spec,
163 resolved_response,
164 content_type,
165 expand_tokens,
166 scenario,
167 )
168 } else {
169 Ok(Value::Object(serde_json::Map::new()))
171 }
172 }
173 }
174 }
175 None => {
176 Ok(Value::Object(serde_json::Map::new()))
178 }
179 }
180 }
181
182 fn find_response_for_status(
184 responses: &Responses,
185 status_code: u16,
186 ) -> Option<&ReferenceOr<Response>> {
187 if let Some(response) = responses.responses.get(&openapiv3::StatusCode::Code(status_code)) {
189 return Some(response);
190 }
191
192 if let Some(default_response) = &responses.default {
194 return Some(default_response);
195 }
196
197 None
198 }
199
200 fn generate_from_response(
202 spec: &OpenApiSpec,
203 response: &Response,
204 content_type: Option<&str>,
205 expand_tokens: bool,
206 ) -> Result<Value> {
207 Self::generate_from_response_with_scenario(
208 spec,
209 response,
210 content_type,
211 expand_tokens,
212 None,
213 )
214 }
215
216 fn generate_from_response_with_scenario(
218 spec: &OpenApiSpec,
219 response: &Response,
220 content_type: Option<&str>,
221 expand_tokens: bool,
222 scenario: Option<&str>,
223 ) -> Result<Value> {
224 if let Some(content_type) = content_type {
226 if let Some(media_type) = response.content.get(content_type) {
227 return Self::generate_from_media_type_with_scenario(
228 spec,
229 media_type,
230 expand_tokens,
231 scenario,
232 );
233 }
234 }
235
236 let preferred_types = ["application/json", "application/xml", "text/plain"];
238
239 for content_type in &preferred_types {
240 if let Some(media_type) = response.content.get(*content_type) {
241 return Self::generate_from_media_type_with_scenario(
242 spec,
243 media_type,
244 expand_tokens,
245 scenario,
246 );
247 }
248 }
249
250 if let Some((_, media_type)) = response.content.iter().next() {
252 return Self::generate_from_media_type_with_scenario(
253 spec,
254 media_type,
255 expand_tokens,
256 scenario,
257 );
258 }
259
260 Ok(Value::Object(serde_json::Map::new()))
262 }
263
264 fn generate_from_media_type(
266 spec: &OpenApiSpec,
267 media_type: &openapiv3::MediaType,
268 expand_tokens: bool,
269 ) -> Result<Value> {
270 Self::generate_from_media_type_with_scenario(spec, media_type, expand_tokens, None)
271 }
272
273 fn generate_from_media_type_with_scenario(
275 spec: &OpenApiSpec,
276 media_type: &openapiv3::MediaType,
277 expand_tokens: bool,
278 scenario: Option<&str>,
279 ) -> Result<Value> {
280 if let Some(example) = &media_type.example {
282 tracing::debug!("Using explicit example from media type: {:?}", example);
283 if expand_tokens {
285 let expanded_example = Self::expand_templates(example);
286 return Ok(expanded_example);
287 } else {
288 return Ok(example.clone());
289 }
290 }
291
292 if !media_type.examples.is_empty() {
294 if let Some(scenario_name) = scenario {
296 if let Some(example_ref) = media_type.examples.get(scenario_name) {
297 tracing::debug!("Using scenario '{}' from examples map", scenario_name);
298 return Self::extract_example_value(spec, example_ref, expand_tokens);
299 } else {
300 tracing::warn!(
301 "Scenario '{}' not found in examples, falling back to first example",
302 scenario_name
303 );
304 }
305 }
306
307 if let Some((example_name, example_ref)) = media_type.examples.iter().next() {
309 tracing::debug!("Using example '{}' from examples map", example_name);
310 return Self::extract_example_value(spec, example_ref, expand_tokens);
311 }
312 }
313
314 if let Some(schema_ref) = &media_type.schema {
316 Ok(Self::generate_example_from_schema_ref(spec, schema_ref))
317 } else {
318 Ok(Value::Object(serde_json::Map::new()))
319 }
320 }
321
322 fn extract_example_value(
324 spec: &OpenApiSpec,
325 example_ref: &ReferenceOr<openapiv3::Example>,
326 expand_tokens: bool,
327 ) -> Result<Value> {
328 match example_ref {
329 ReferenceOr::Item(example) => {
330 if let Some(value) = &example.value {
331 tracing::debug!("Using example from examples map: {:?}", value);
332 if expand_tokens {
333 return Ok(Self::expand_templates(value));
334 } else {
335 return Ok(value.clone());
336 }
337 }
338 }
339 ReferenceOr::Reference { reference } => {
340 if let Some(example) = spec.get_example(reference) {
342 if let Some(value) = &example.value {
343 tracing::debug!("Using resolved example reference: {:?}", value);
344 if expand_tokens {
345 return Ok(Self::expand_templates(value));
346 } else {
347 return Ok(value.clone());
348 }
349 }
350 } else {
351 tracing::warn!("Example reference '{}' not found", reference);
352 }
353 }
354 }
355 Ok(Value::Object(serde_json::Map::new()))
356 }
357
358 fn generate_example_from_schema_ref(
359 spec: &OpenApiSpec,
360 schema_ref: &ReferenceOr<Schema>,
361 ) -> Value {
362 match schema_ref {
363 ReferenceOr::Item(schema) => Self::generate_example_from_schema(spec, schema),
364 ReferenceOr::Reference { reference } => spec
365 .get_schema(reference)
366 .map(|schema| Self::generate_example_from_schema(spec, &schema.schema))
367 .unwrap_or_else(|| Value::Object(serde_json::Map::new())),
368 }
369 }
370
371 fn generate_example_from_schema(spec: &OpenApiSpec, schema: &Schema) -> Value {
378 if let Some(example) = schema.schema_data.example.as_ref() {
381 tracing::debug!("Using schema-level example: {:?}", example);
382 return example.clone();
383 }
384
385 match &schema.schema_kind {
389 openapiv3::SchemaKind::Type(openapiv3::Type::String(_)) => {
390 Value::String("example string".to_string())
392 }
393 openapiv3::SchemaKind::Type(openapiv3::Type::Integer(_)) => Value::Number(42.into()),
394 openapiv3::SchemaKind::Type(openapiv3::Type::Number(_)) => {
395 Value::Number(serde_json::Number::from_f64(std::f64::consts::PI).unwrap())
396 }
397 openapiv3::SchemaKind::Type(openapiv3::Type::Boolean(_)) => Value::Bool(true),
398 openapiv3::SchemaKind::Type(openapiv3::Type::Object(obj)) => {
399 let mut map = serde_json::Map::new();
400 for (prop_name, prop_schema) in &obj.properties {
401 let value = match prop_schema {
402 ReferenceOr::Item(prop_schema) => {
403 if let Some(prop_example) = prop_schema.schema_data.example.as_ref() {
405 tracing::debug!("Using example for property '{}': {:?}", prop_name, prop_example);
406 prop_example.clone()
407 } else {
408 Self::generate_example_from_schema(spec, prop_schema.as_ref())
409 }
410 }
411 ReferenceOr::Reference { reference } => {
412 if let Some(resolved_schema) = spec.get_schema(reference) {
414 if let Some(ref_example) = resolved_schema.schema.schema_data.example.as_ref() {
415 tracing::debug!("Using example from referenced schema '{}': {:?}", reference, ref_example);
416 ref_example.clone()
417 } else {
418 Self::generate_example_from_schema(spec, &resolved_schema.schema)
419 }
420 } else {
421 Self::generate_example_for_property(prop_name)
422 }
423 }
424 };
425 let value = match value {
426 Value::Null => Self::generate_example_for_property(prop_name),
427 Value::Object(ref obj) if obj.is_empty() => {
428 Self::generate_example_for_property(prop_name)
429 }
430 _ => value,
431 };
432 map.insert(prop_name.clone(), value);
433 }
434 Value::Object(map)
435 }
436 openapiv3::SchemaKind::Type(openapiv3::Type::Array(arr)) => {
437 match &arr.items {
443 Some(item_schema) => {
444 let example_item = match item_schema {
445 ReferenceOr::Item(item_schema) => {
446 Self::generate_example_from_schema(spec, item_schema.as_ref())
449 }
450 ReferenceOr::Reference { reference } => {
451 if let Some(resolved_schema) = spec.get_schema(reference) {
454 Self::generate_example_from_schema(spec, &resolved_schema.schema)
455 } else {
456 Value::Object(serde_json::Map::new())
457 }
458 }
459 };
460 Value::Array(vec![example_item])
461 }
462 None => Value::Array(vec![Value::String("item".to_string())]),
463 }
464 },
465 _ => Value::Object(serde_json::Map::new()),
466 }
467 }
468
469 fn generate_example_for_property(prop_name: &str) -> Value {
471 let prop_lower = prop_name.to_lowercase();
472
473 if prop_lower.contains("id") || prop_lower.contains("uuid") {
475 Value::String(uuid::Uuid::new_v4().to_string())
476 } else if prop_lower.contains("email") {
477 Value::String(format!("user{}@example.com", rng().random_range(1000..=9999)))
478 } else if prop_lower.contains("name") || prop_lower.contains("title") {
479 let names = ["John Doe", "Jane Smith", "Bob Johnson", "Alice Brown"];
480 Value::String(names[rng().random_range(0..names.len())].to_string())
481 } else if prop_lower.contains("phone") || prop_lower.contains("mobile") {
482 Value::String(format!("+1-555-{:04}", rng().random_range(1000..=9999)))
483 } else if prop_lower.contains("address") || prop_lower.contains("street") {
484 let streets = ["123 Main St", "456 Oak Ave", "789 Pine Rd", "321 Elm St"];
485 Value::String(streets[rng().random_range(0..streets.len())].to_string())
486 } else if prop_lower.contains("city") {
487 let cities = ["New York", "London", "Tokyo", "Paris", "Sydney"];
488 Value::String(cities[rng().random_range(0..cities.len())].to_string())
489 } else if prop_lower.contains("country") {
490 let countries = ["USA", "UK", "Japan", "France", "Australia"];
491 Value::String(countries[rng().random_range(0..countries.len())].to_string())
492 } else if prop_lower.contains("company") || prop_lower.contains("organization") {
493 let companies = ["Acme Corp", "Tech Solutions", "Global Inc", "Innovate Ltd"];
494 Value::String(companies[rng().random_range(0..companies.len())].to_string())
495 } else if prop_lower.contains("url") || prop_lower.contains("website") {
496 Value::String("https://example.com".to_string())
497 } else if prop_lower.contains("age") {
498 Value::Number((18 + rng().random_range(0..60)).into())
499 } else if prop_lower.contains("count") || prop_lower.contains("quantity") {
500 Value::Number((1 + rng().random_range(0..100)).into())
501 } else if prop_lower.contains("price")
502 || prop_lower.contains("amount")
503 || prop_lower.contains("cost")
504 {
505 Value::Number(
506 serde_json::Number::from_f64(
507 (rng().random::<f64>() * 1000.0 * 100.0).round() / 100.0,
508 )
509 .unwrap(),
510 )
511 } else if prop_lower.contains("active")
512 || prop_lower.contains("enabled")
513 || prop_lower.contains("is_")
514 {
515 Value::Bool(rng().random_bool(0.5))
516 } else if prop_lower.contains("date") || prop_lower.contains("time") {
517 Value::String(chrono::Utc::now().to_rfc3339())
518 } else if prop_lower.contains("description") || prop_lower.contains("comment") {
519 Value::String("This is a sample description text.".to_string())
520 } else {
521 Value::String(format!("example {}", prop_name))
522 }
523 }
524
525 pub fn generate_from_examples(
527 response: &Response,
528 content_type: Option<&str>,
529 ) -> Result<Option<Value>> {
530 use openapiv3::ReferenceOr;
531
532 if let Some(content_type) = content_type {
534 if let Some(media_type) = response.content.get(content_type) {
535 if let Some(example) = &media_type.example {
537 return Ok(Some(example.clone()));
538 }
539
540 for (_, example_ref) in &media_type.examples {
542 if let ReferenceOr::Item(example) = example_ref {
543 if let Some(value) = &example.value {
544 return Ok(Some(value.clone()));
545 }
546 }
547 }
549 }
550 }
551
552 for (_, media_type) in &response.content {
554 if let Some(example) = &media_type.example {
556 return Ok(Some(example.clone()));
557 }
558
559 for (_, example_ref) in &media_type.examples {
561 if let ReferenceOr::Item(example) = example_ref {
562 if let Some(value) = &example.value {
563 return Ok(Some(value.clone()));
564 }
565 }
566 }
568 }
569
570 Ok(None)
571 }
572
573 fn expand_templates(value: &Value) -> Value {
575 match value {
576 Value::String(s) => {
577 let expanded = s
578 .replace("{{now}}", &chrono::Utc::now().to_rfc3339())
579 .replace("{{uuid}}", &uuid::Uuid::new_v4().to_string());
580 Value::String(expanded)
581 }
582 Value::Object(map) => {
583 let mut new_map = serde_json::Map::new();
584 for (key, val) in map {
585 new_map.insert(key.clone(), Self::expand_templates(val));
586 }
587 Value::Object(new_map)
588 }
589 Value::Array(arr) => {
590 let new_arr: Vec<Value> = arr.iter().map(Self::expand_templates).collect();
591 Value::Array(new_arr)
592 }
593 _ => value.clone(),
594 }
595 }
596}
597
598#[cfg(test)]
599mod tests {
600 use super::*;
601 use openapiv3::ReferenceOr;
602
603 #[test]
604 fn generates_example_using_referenced_schemas() {
605 let yaml = r#"
606openapi: 3.0.3
607info:
608 title: Test API
609 version: "1.0.0"
610paths:
611 /apiaries:
612 get:
613 responses:
614 '200':
615 description: ok
616 content:
617 application/json:
618 schema:
619 $ref: '#/components/schemas/Apiary'
620components:
621 schemas:
622 Apiary:
623 type: object
624 properties:
625 id:
626 type: string
627 hive:
628 $ref: '#/components/schemas/Hive'
629 Hive:
630 type: object
631 properties:
632 name:
633 type: string
634 active:
635 type: boolean
636 "#;
637
638 let spec = OpenApiSpec::from_string(yaml, Some("yaml")).expect("load spec");
639 let path_item = spec
640 .spec
641 .paths
642 .paths
643 .get("/apiaries")
644 .and_then(ReferenceOr::as_item)
645 .expect("path item");
646 let operation = path_item.get.as_ref().expect("GET operation");
647
648 let response =
649 ResponseGenerator::generate_response(&spec, operation, 200, Some("application/json"))
650 .expect("generate response");
651
652 let obj = response.as_object().expect("response object");
653 assert!(obj.contains_key("id"));
654 let hive = obj.get("hive").and_then(|value| value.as_object()).expect("hive object");
655 assert!(hive.contains_key("name"));
656 assert!(hive.contains_key("active"));
657 }
658}
659
660#[derive(Debug, Clone)]
662pub struct MockResponse {
663 pub status_code: u16,
665 pub headers: HashMap<String, String>,
667 pub body: Option<Value>,
669}
670
671impl MockResponse {
672 pub fn new(status_code: u16) -> Self {
674 Self {
675 status_code,
676 headers: HashMap::new(),
677 body: None,
678 }
679 }
680
681 pub fn with_header(mut self, name: String, value: String) -> Self {
683 self.headers.insert(name, value);
684 self
685 }
686
687 pub fn with_body(mut self, body: Value) -> Self {
689 self.body = Some(body);
690 self
691 }
692}
693
694#[derive(Debug, Clone)]
696pub struct OpenApiSecurityRequirement {
697 pub scheme: String,
699 pub scopes: Vec<String>,
701}
702
703impl OpenApiSecurityRequirement {
704 pub fn new(scheme: String, scopes: Vec<String>) -> Self {
706 Self { scheme, scopes }
707 }
708}
709
710#[derive(Debug, Clone)]
712pub struct OpenApiOperation {
713 pub method: String,
715 pub path: String,
717 pub operation: openapiv3::Operation,
719}
720
721impl OpenApiOperation {
722 pub fn new(method: String, path: String, operation: openapiv3::Operation) -> Self {
724 Self {
725 method,
726 path,
727 operation,
728 }
729 }
730}