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 {
373 match &schema.schema_kind {
374 openapiv3::SchemaKind::Type(openapiv3::Type::String(_)) => {
375 Value::String("example string".to_string())
377 }
378 openapiv3::SchemaKind::Type(openapiv3::Type::Integer(_)) => Value::Number(42.into()),
379 openapiv3::SchemaKind::Type(openapiv3::Type::Number(_)) => {
380 Value::Number(serde_json::Number::from_f64(std::f64::consts::PI).unwrap())
381 }
382 openapiv3::SchemaKind::Type(openapiv3::Type::Boolean(_)) => Value::Bool(true),
383 openapiv3::SchemaKind::Type(openapiv3::Type::Object(obj)) => {
384 let mut map = serde_json::Map::new();
385 for (prop_name, prop_schema) in &obj.properties {
386 let value = match prop_schema {
387 ReferenceOr::Item(prop_schema) => {
388 Self::generate_example_from_schema(spec, prop_schema.as_ref())
389 }
390 ReferenceOr::Reference { reference } => spec
391 .get_schema(reference)
392 .map(|schema| Self::generate_example_from_schema(spec, &schema.schema))
393 .unwrap_or_else(|| Self::generate_example_for_property(prop_name)),
394 };
395 let value = match value {
396 Value::Null => Self::generate_example_for_property(prop_name),
397 Value::Object(ref obj) if obj.is_empty() => {
398 Self::generate_example_for_property(prop_name)
399 }
400 _ => value,
401 };
402 map.insert(prop_name.clone(), value);
403 }
404 Value::Object(map)
405 }
406 openapiv3::SchemaKind::Type(openapiv3::Type::Array(arr)) => match &arr.items {
407 Some(item_schema) => {
408 let example_item = match item_schema {
409 ReferenceOr::Item(item_schema) => {
410 Self::generate_example_from_schema(spec, item_schema.as_ref())
411 }
412 ReferenceOr::Reference { reference } => spec
413 .get_schema(reference)
414 .map(|schema| Self::generate_example_from_schema(spec, &schema.schema))
415 .unwrap_or_else(|| Value::Object(serde_json::Map::new())),
416 };
417 Value::Array(vec![example_item])
418 }
419 None => Value::Array(vec![Value::String("item".to_string())]),
420 },
421 _ => Value::Object(serde_json::Map::new()),
422 }
423 }
424
425 fn generate_example_for_property(prop_name: &str) -> Value {
427 let prop_lower = prop_name.to_lowercase();
428
429 if prop_lower.contains("id") || prop_lower.contains("uuid") {
431 Value::String(uuid::Uuid::new_v4().to_string())
432 } else if prop_lower.contains("email") {
433 Value::String(format!("user{}@example.com", rng().random_range(1000..=9999)))
434 } else if prop_lower.contains("name") || prop_lower.contains("title") {
435 let names = ["John Doe", "Jane Smith", "Bob Johnson", "Alice Brown"];
436 Value::String(names[rng().random_range(0..names.len())].to_string())
437 } else if prop_lower.contains("phone") || prop_lower.contains("mobile") {
438 Value::String(format!("+1-555-{:04}", rng().random_range(1000..=9999)))
439 } else if prop_lower.contains("address") || prop_lower.contains("street") {
440 let streets = ["123 Main St", "456 Oak Ave", "789 Pine Rd", "321 Elm St"];
441 Value::String(streets[rng().random_range(0..streets.len())].to_string())
442 } else if prop_lower.contains("city") {
443 let cities = ["New York", "London", "Tokyo", "Paris", "Sydney"];
444 Value::String(cities[rng().random_range(0..cities.len())].to_string())
445 } else if prop_lower.contains("country") {
446 let countries = ["USA", "UK", "Japan", "France", "Australia"];
447 Value::String(countries[rng().random_range(0..countries.len())].to_string())
448 } else if prop_lower.contains("company") || prop_lower.contains("organization") {
449 let companies = ["Acme Corp", "Tech Solutions", "Global Inc", "Innovate Ltd"];
450 Value::String(companies[rng().random_range(0..companies.len())].to_string())
451 } else if prop_lower.contains("url") || prop_lower.contains("website") {
452 Value::String("https://example.com".to_string())
453 } else if prop_lower.contains("age") {
454 Value::Number((18 + rng().random_range(0..60)).into())
455 } else if prop_lower.contains("count") || prop_lower.contains("quantity") {
456 Value::Number((1 + rng().random_range(0..100)).into())
457 } else if prop_lower.contains("price")
458 || prop_lower.contains("amount")
459 || prop_lower.contains("cost")
460 {
461 Value::Number(
462 serde_json::Number::from_f64(
463 (rng().random::<f64>() * 1000.0 * 100.0).round() / 100.0,
464 )
465 .unwrap(),
466 )
467 } else if prop_lower.contains("active")
468 || prop_lower.contains("enabled")
469 || prop_lower.contains("is_")
470 {
471 Value::Bool(rng().random_bool(0.5))
472 } else if prop_lower.contains("date") || prop_lower.contains("time") {
473 Value::String(chrono::Utc::now().to_rfc3339())
474 } else if prop_lower.contains("description") || prop_lower.contains("comment") {
475 Value::String("This is a sample description text.".to_string())
476 } else {
477 Value::String(format!("example {}", prop_name))
478 }
479 }
480
481 pub fn generate_from_examples(
483 response: &Response,
484 content_type: Option<&str>,
485 ) -> Result<Option<Value>> {
486 use openapiv3::ReferenceOr;
487
488 if let Some(content_type) = content_type {
490 if let Some(media_type) = response.content.get(content_type) {
491 if let Some(example) = &media_type.example {
493 return Ok(Some(example.clone()));
494 }
495
496 for (_, example_ref) in &media_type.examples {
498 if let ReferenceOr::Item(example) = example_ref {
499 if let Some(value) = &example.value {
500 return Ok(Some(value.clone()));
501 }
502 }
503 }
505 }
506 }
507
508 for (_, media_type) in &response.content {
510 if let Some(example) = &media_type.example {
512 return Ok(Some(example.clone()));
513 }
514
515 for (_, example_ref) in &media_type.examples {
517 if let ReferenceOr::Item(example) = example_ref {
518 if let Some(value) = &example.value {
519 return Ok(Some(value.clone()));
520 }
521 }
522 }
524 }
525
526 Ok(None)
527 }
528
529 fn expand_templates(value: &Value) -> Value {
531 match value {
532 Value::String(s) => {
533 let expanded = s
534 .replace("{{now}}", &chrono::Utc::now().to_rfc3339())
535 .replace("{{uuid}}", &uuid::Uuid::new_v4().to_string());
536 Value::String(expanded)
537 }
538 Value::Object(map) => {
539 let mut new_map = serde_json::Map::new();
540 for (key, val) in map {
541 new_map.insert(key.clone(), Self::expand_templates(val));
542 }
543 Value::Object(new_map)
544 }
545 Value::Array(arr) => {
546 let new_arr: Vec<Value> = arr.iter().map(Self::expand_templates).collect();
547 Value::Array(new_arr)
548 }
549 _ => value.clone(),
550 }
551 }
552}
553
554#[cfg(test)]
555mod tests {
556 use super::*;
557 use openapiv3::ReferenceOr;
558
559 #[test]
560 fn generates_example_using_referenced_schemas() {
561 let yaml = r#"
562openapi: 3.0.3
563info:
564 title: Test API
565 version: "1.0.0"
566paths:
567 /apiaries:
568 get:
569 responses:
570 '200':
571 description: ok
572 content:
573 application/json:
574 schema:
575 $ref: '#/components/schemas/Apiary'
576components:
577 schemas:
578 Apiary:
579 type: object
580 properties:
581 id:
582 type: string
583 hive:
584 $ref: '#/components/schemas/Hive'
585 Hive:
586 type: object
587 properties:
588 name:
589 type: string
590 active:
591 type: boolean
592 "#;
593
594 let spec = OpenApiSpec::from_string(yaml, Some("yaml")).expect("load spec");
595 let path_item = spec
596 .spec
597 .paths
598 .paths
599 .get("/apiaries")
600 .and_then(ReferenceOr::as_item)
601 .expect("path item");
602 let operation = path_item.get.as_ref().expect("GET operation");
603
604 let response =
605 ResponseGenerator::generate_response(&spec, operation, 200, Some("application/json"))
606 .expect("generate response");
607
608 let obj = response.as_object().expect("response object");
609 assert!(obj.contains_key("id"));
610 let hive = obj.get("hive").and_then(|value| value.as_object()).expect("hive object");
611 assert!(hive.contains_key("name"));
612 assert!(hive.contains_key("active"));
613 }
614}
615
616#[derive(Debug, Clone)]
618pub struct MockResponse {
619 pub status_code: u16,
621 pub headers: HashMap<String, String>,
623 pub body: Option<Value>,
625}
626
627impl MockResponse {
628 pub fn new(status_code: u16) -> Self {
630 Self {
631 status_code,
632 headers: HashMap::new(),
633 body: None,
634 }
635 }
636
637 pub fn with_header(mut self, name: String, value: String) -> Self {
639 self.headers.insert(name, value);
640 self
641 }
642
643 pub fn with_body(mut self, body: Value) -> Self {
645 self.body = Some(body);
646 self
647 }
648}
649
650#[derive(Debug, Clone)]
652pub struct OpenApiSecurityRequirement {
653 pub scheme: String,
655 pub scopes: Vec<String>,
657}
658
659impl OpenApiSecurityRequirement {
660 pub fn new(scheme: String, scopes: Vec<String>) -> Self {
662 Self { scheme, scopes }
663 }
664}
665
666#[derive(Debug, Clone)]
668pub struct OpenApiOperation {
669 pub method: String,
671 pub path: String,
673 pub operation: openapiv3::Operation,
675}
676
677impl OpenApiOperation {
678 pub fn new(method: String, path: String, operation: openapiv3::Operation) -> Self {
680 Self {
681 method,
682 path,
683 operation,
684 }
685 }
686}