mockforge_core/openapi/
response.rs

1//! OpenAPI response generation and mocking
2//!
3//! This module provides functionality for generating mock responses
4//! based on OpenAPI specifications.
5
6use 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/// Trait for AI response generation
19///
20/// This trait allows the HTTP layer to provide custom AI generation
21/// implementations without creating circular dependencies between crates.
22#[async_trait]
23pub trait AiGenerator: Send + Sync {
24    /// Generate an AI response from a prompt
25    ///
26    /// # Arguments
27    /// * `prompt` - The expanded prompt to send to the LLM
28    /// * `config` - The AI response configuration with temperature, max_tokens, etc.
29    ///
30    /// # Returns
31    /// A JSON value containing the generated response
32    async fn generate(&self, prompt: &str, config: &AiResponseConfig) -> Result<Value>;
33}
34
35/// Response generator for creating mock responses
36pub struct ResponseGenerator;
37
38impl ResponseGenerator {
39    /// Generate an AI-assisted response using LLM
40    ///
41    /// This method generates a dynamic response based on request context
42    /// using the configured LLM provider (OpenAI, Anthropic, etc.)
43    ///
44    /// # Arguments
45    /// * `ai_config` - The AI response configuration
46    /// * `context` - The request context for prompt expansion
47    /// * `generator` - Optional AI generator implementation (if None, returns placeholder)
48    ///
49    /// # Returns
50    /// A JSON value containing the generated response
51    pub async fn generate_ai_response(
52        ai_config: &AiResponseConfig,
53        context: &RequestContext,
54        generator: Option<&dyn AiGenerator>,
55    ) -> Result<Value> {
56        // Get the prompt template and expand it with request context
57        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        // Use the provided generator if available
67        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        // Fallback: return a descriptive placeholder if no generator is provided
73        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    /// Generate a mock response for an operation and status code
85    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    /// Generate a mock response for an operation and status code with token expansion control
95    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    /// Generate a mock response with scenario support
113    ///
114    /// This method allows selection of specific example scenarios from the OpenAPI spec.
115    /// Scenarios are defined using the standard OpenAPI `examples` field (not the singular `example`).
116    ///
117    /// # Arguments
118    /// * `spec` - The OpenAPI specification
119    /// * `operation` - The operation to generate a response for
120    /// * `status_code` - The HTTP status code
121    /// * `content_type` - Optional content type (e.g., "application/json")
122    /// * `expand_tokens` - Whether to expand template tokens like {{now}} and {{uuid}}
123    /// * `scenario` - Optional scenario name to select from the examples map
124    ///
125    /// # Example
126    /// ```yaml
127    /// responses:
128    ///   '200':
129    ///     content:
130    ///       application/json:
131    ///         examples:
132    ///           happy:
133    ///             value: { "status": "success", "message": "All good!" }
134    ///           error:
135    ///             value: { "status": "error", "message": "Something went wrong" }
136    /// ```
137    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        // Find the response for the status code
146        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                        // Resolve the reference
160                        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                            // Reference not found, return empty object
170                            Ok(Value::Object(serde_json::Map::new()))
171                        }
172                    }
173                }
174            }
175            None => {
176                // No response found for this status code
177                Ok(Value::Object(serde_json::Map::new()))
178            }
179        }
180    }
181
182    /// Find response for a given status code
183    fn find_response_for_status(
184        responses: &Responses,
185        status_code: u16,
186    ) -> Option<&ReferenceOr<Response>> {
187        // First try exact match
188        if let Some(response) = responses.responses.get(&openapiv3::StatusCode::Code(status_code)) {
189            return Some(response);
190        }
191
192        // Try default response
193        if let Some(default_response) = &responses.default {
194            return Some(default_response);
195        }
196
197        None
198    }
199
200    /// Generate response from a Response object
201    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    /// Generate response from a Response object with scenario support
217    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 content_type is specified, look for that media type
225        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        // If no content_type specified or not found, try common content types
237        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 no suitable content type found, return the first available
251        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        // No content found, return empty object
261        Ok(Value::Object(serde_json::Map::new()))
262    }
263
264    /// Generate response from a MediaType with optional scenario selection
265    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    /// Generate response from a MediaType with scenario support
274    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        // First, check if there's an explicit example
281        if let Some(example) = &media_type.example {
282            tracing::debug!("Using explicit example from media type: {:?}", example);
283            // Expand templates in the example if enabled
284            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        // Then check examples map - with scenario support
293        if !media_type.examples.is_empty() {
294            // If a scenario is specified, try to find it first
295            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            // Fall back to first example if no scenario specified or scenario not found
308            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        // Fall back to schema-based generation
315        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    /// Extract value from an example reference
323    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                // Resolve the example reference
341                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    /// Generate example data from an OpenAPI schema
372    fn generate_example_from_schema(spec: &OpenApiSpec, schema: &Schema) -> Value {
373        match &schema.schema_kind {
374            openapiv3::SchemaKind::Type(openapiv3::Type::String(_)) => {
375                // Use faker for string fields based on field name hints
376                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    /// Generate example value for a property based on its name
426    fn generate_example_for_property(prop_name: &str) -> Value {
427        let prop_lower = prop_name.to_lowercase();
428
429        // Generate realistic data based on property name patterns
430        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    /// Generate example responses from OpenAPI examples
482    pub fn generate_from_examples(
483        response: &Response,
484        content_type: Option<&str>,
485    ) -> Result<Option<Value>> {
486        use openapiv3::ReferenceOr;
487
488        // If content_type is specified, look for examples in that media type
489        if let Some(content_type) = content_type {
490            if let Some(media_type) = response.content.get(content_type) {
491                // Check for single example first
492                if let Some(example) = &media_type.example {
493                    return Ok(Some(example.clone()));
494                }
495
496                // Check for multiple examples
497                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                    // Reference resolution would require spec parameter to be added to this function
504                }
505            }
506        }
507
508        // If no content_type specified or not found, check all media types
509        for (_, media_type) in &response.content {
510            // Check for single example first
511            if let Some(example) = &media_type.example {
512                return Ok(Some(example.clone()));
513            }
514
515            // Check for multiple examples
516            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                // Reference resolution would require spec parameter to be added to this function
523            }
524        }
525
526        Ok(None)
527    }
528
529    /// Expand templates like {{now}} and {{uuid}} in JSON values
530    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/// Mock response data
617#[derive(Debug, Clone)]
618pub struct MockResponse {
619    /// HTTP status code
620    pub status_code: u16,
621    /// Response headers
622    pub headers: HashMap<String, String>,
623    /// Response body
624    pub body: Option<Value>,
625}
626
627impl MockResponse {
628    /// Create a new mock response
629    pub fn new(status_code: u16) -> Self {
630        Self {
631            status_code,
632            headers: HashMap::new(),
633            body: None,
634        }
635    }
636
637    /// Add a header to the response
638    pub fn with_header(mut self, name: String, value: String) -> Self {
639        self.headers.insert(name, value);
640        self
641    }
642
643    /// Set the response body
644    pub fn with_body(mut self, body: Value) -> Self {
645        self.body = Some(body);
646        self
647    }
648}
649
650/// OpenAPI security requirement wrapper
651#[derive(Debug, Clone)]
652pub struct OpenApiSecurityRequirement {
653    /// The security scheme name
654    pub scheme: String,
655    /// Required scopes (for OAuth2)
656    pub scopes: Vec<String>,
657}
658
659impl OpenApiSecurityRequirement {
660    /// Create a new security requirement
661    pub fn new(scheme: String, scopes: Vec<String>) -> Self {
662        Self { scheme, scopes }
663    }
664}
665
666/// OpenAPI operation wrapper with path context
667#[derive(Debug, Clone)]
668pub struct OpenApiOperation {
669    /// The HTTP method
670    pub method: String,
671    /// The path this operation belongs to
672    pub path: String,
673    /// The OpenAPI operation
674    pub operation: openapiv3::Operation,
675}
676
677impl OpenApiOperation {
678    /// Create a new OpenApiOperation
679    pub fn new(method: String, path: String, operation: openapiv3::Operation) -> Self {
680        Self {
681            method,
682            path,
683            operation,
684        }
685    }
686}