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    ///
373    /// Priority order:
374    /// 1. Schema-level example (schema.schema_data.example)
375    /// 2. Property-level examples when generating objects
376    /// 3. Generated values based on schema type
377    fn generate_example_from_schema(spec: &OpenApiSpec, schema: &Schema) -> Value {
378        // First, check for schema-level example in schema_data
379        // OpenAPI v3 stores examples in schema_data.example
380        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        // Note: schema-level example check happens at the top of the function (line 380-383)
386        // At this point, if we have a schema-level example, we've already returned it
387        // So we only generate defaults when no example exists
388        match &schema.schema_kind {
389            openapiv3::SchemaKind::Type(openapiv3::Type::String(_)) => {
390                // Use faker for string fields based on field name hints
391                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                            // Check for property-level example first
404                            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                            // Try to resolve reference and check for example
413                            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                // Check for array-level example (schema.schema_data.example contains the full array)
438                // Note: This check is actually redundant since we check at the top,
439                // but keeping it here for clarity and defensive programming
440                // If the array schema itself has an example, it's already handled at the top
441
442                match &arr.items {
443                    Some(item_schema) => {
444                        let example_item = match item_schema {
445                            ReferenceOr::Item(item_schema) => {
446                                // Recursively generate example for array item
447                                // This will check for item-level examples
448                                Self::generate_example_from_schema(spec, item_schema.as_ref())
449                            }
450                            ReferenceOr::Reference { reference } => {
451                                // Try to resolve reference and generate example
452                                // This will check for examples in referenced schema
453                                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    /// Generate example value for a property based on its name
470    fn generate_example_for_property(prop_name: &str) -> Value {
471        let prop_lower = prop_name.to_lowercase();
472
473        // Generate realistic data based on property name patterns
474        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    /// Generate example responses from OpenAPI examples
526    pub fn generate_from_examples(
527        response: &Response,
528        content_type: Option<&str>,
529    ) -> Result<Option<Value>> {
530        use openapiv3::ReferenceOr;
531
532        // If content_type is specified, look for examples in that media type
533        if let Some(content_type) = content_type {
534            if let Some(media_type) = response.content.get(content_type) {
535                // Check for single example first
536                if let Some(example) = &media_type.example {
537                    return Ok(Some(example.clone()));
538                }
539
540                // Check for multiple examples
541                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                    // Reference resolution would require spec parameter to be added to this function
548                }
549            }
550        }
551
552        // If no content_type specified or not found, check all media types
553        for (_, media_type) in &response.content {
554            // Check for single example first
555            if let Some(example) = &media_type.example {
556                return Ok(Some(example.clone()));
557            }
558
559            // Check for multiple examples
560            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                // Reference resolution would require spec parameter to be added to this function
567            }
568        }
569
570        Ok(None)
571    }
572
573    /// Expand templates like {{now}} and {{uuid}} in JSON values
574    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/// Mock response data
661#[derive(Debug, Clone)]
662pub struct MockResponse {
663    /// HTTP status code
664    pub status_code: u16,
665    /// Response headers
666    pub headers: HashMap<String, String>,
667    /// Response body
668    pub body: Option<Value>,
669}
670
671impl MockResponse {
672    /// Create a new mock response
673    pub fn new(status_code: u16) -> Self {
674        Self {
675            status_code,
676            headers: HashMap::new(),
677            body: None,
678        }
679    }
680
681    /// Add a header to the response
682    pub fn with_header(mut self, name: String, value: String) -> Self {
683        self.headers.insert(name, value);
684        self
685    }
686
687    /// Set the response body
688    pub fn with_body(mut self, body: Value) -> Self {
689        self.body = Some(body);
690        self
691    }
692}
693
694/// OpenAPI security requirement wrapper
695#[derive(Debug, Clone)]
696pub struct OpenApiSecurityRequirement {
697    /// The security scheme name
698    pub scheme: String,
699    /// Required scopes (for OAuth2)
700    pub scopes: Vec<String>,
701}
702
703impl OpenApiSecurityRequirement {
704    /// Create a new security requirement
705    pub fn new(scheme: String, scopes: Vec<String>) -> Self {
706        Self { scheme, scopes }
707    }
708}
709
710/// OpenAPI operation wrapper with path context
711#[derive(Debug, Clone)]
712pub struct OpenApiOperation {
713    /// The HTTP method
714    pub method: String,
715    /// The path this operation belongs to
716    pub path: String,
717    /// The OpenAPI operation
718    pub operation: openapiv3::Operation,
719}
720
721impl OpenApiOperation {
722    /// Create a new OpenApiOperation
723    pub fn new(method: String, path: String, operation: openapiv3::Operation) -> Self {
724        Self {
725            method,
726            path,
727            operation,
728        }
729    }
730}