Skip to main content

mockforge_openapi/
spec.rs

1//! OpenAPI specification loading and parsing
2//!
3//! This module handles loading OpenAPI specifications from files,
4//! parsing them, and providing basic operations on the specs.
5//! It also supports Swagger 2.0 specifications by converting them
6//! to OpenAPI 3.0 format automatically.
7
8use crate::swagger_convert;
9use mockforge_foundation::error::{Error, Result};
10use openapiv3::{OpenAPI, ReferenceOr, Schema};
11use std::collections::HashSet;
12use std::path::Path;
13use tokio::fs;
14use tracing;
15
16/// OpenAPI specification loader and parser
17#[derive(Debug, Clone)]
18pub struct OpenApiSpec {
19    /// The parsed OpenAPI specification
20    pub spec: OpenAPI,
21    /// Path to the original spec file
22    pub file_path: Option<String>,
23    /// Raw OpenAPI document preserved as JSON for resolving unsupported constructs
24    pub raw_document: Option<serde_json::Value>,
25}
26
27impl OpenApiSpec {
28    /// Load OpenAPI spec from a file path
29    ///
30    /// Supports both OpenAPI 3.x and Swagger 2.0 specifications.
31    /// Swagger 2.0 specs are automatically converted to OpenAPI 3.0 format.
32    pub async fn from_file<P: AsRef<Path>>(path: P) -> Result<Self> {
33        let path_ref = path.as_ref();
34        let content = fs::read_to_string(path_ref)
35            .await
36            .map_err(|e| Error::io_with_context("reading OpenAPI spec file", e.to_string()))?;
37
38        let raw_json = if path_ref.extension().and_then(|s| s.to_str()) == Some("yaml")
39            || path_ref.extension().and_then(|s| s.to_str()) == Some("yml")
40        {
41            let yaml_value: serde_yaml::Value = serde_yaml::from_str(&content)
42                .map_err(|e| Error::config(format!("Failed to parse YAML OpenAPI spec: {}", e)))?;
43            serde_json::to_value(&yaml_value).map_err(|e| {
44                Error::config(format!("Failed to convert YAML OpenAPI spec to JSON: {}", e))
45            })?
46        } else {
47            serde_json::from_str(&content)
48                .map_err(|e| Error::config(format!("Failed to parse JSON OpenAPI spec: {}", e)))?
49        };
50
51        // Check if this is a Swagger 2.0 spec and convert if necessary
52        let (raw_document, spec) = if swagger_convert::is_swagger_2(&raw_json) {
53            tracing::info!("Detected Swagger 2.0 specification, converting to OpenAPI 3.0");
54            let converted =
55                swagger_convert::convert_swagger_to_openapi3(&raw_json).map_err(|e| {
56                    Error::config(format!("Failed to convert Swagger 2.0 to OpenAPI 3.0: {}", e))
57                })?;
58            let spec: OpenAPI = serde_json::from_value(converted.clone()).map_err(|e| {
59                Error::config(format!("Failed to parse converted OpenAPI spec: {}", e))
60            })?;
61            (converted, spec)
62        } else {
63            let spec: OpenAPI = serde_json::from_value(raw_json.clone()).map_err(|e| {
64                // Enhanced error reporting for debugging missing field errors
65                let error_str = format!("{}", e);
66                let mut error_msg = format!("Failed to read OpenAPI spec: {}", e);
67
68                // If it's a missing field error, add diagnostic information
69                if error_str.contains("missing field") {
70                    tracing::error!("OpenAPI deserialization error: {}", error_str);
71
72                    // Add context about the spec structure
73                    if let Some(info) = raw_json.get("info") {
74                        if let Some(info_obj) = info.as_object() {
75                            let has_desc = info_obj.contains_key("description");
76                            error_msg
77                                .push_str(&format!(" | Info.description present: {}", has_desc));
78                        }
79                    }
80                    if let Some(servers) = raw_json.get("servers") {
81                        if let Some(servers_arr) = servers.as_array() {
82                            error_msg.push_str(&format!(" | Servers count: {}", servers_arr.len()));
83                        }
84                    }
85                }
86
87                Error::config(error_msg)
88            })?;
89            (raw_json, spec)
90        };
91
92        Ok(Self {
93            spec,
94            file_path: path_ref.to_str().map(|s| s.to_string()),
95            raw_document: Some(raw_document),
96        })
97    }
98
99    /// Load OpenAPI spec from string content
100    ///
101    /// Supports both OpenAPI 3.x and Swagger 2.0 specifications.
102    /// Swagger 2.0 specs are automatically converted to OpenAPI 3.0 format.
103    pub fn from_string(content: &str, format: Option<&str>) -> Result<Self> {
104        let raw_json = if format == Some("yaml") || format == Some("yml") {
105            let yaml_value: serde_yaml::Value = serde_yaml::from_str(content)
106                .map_err(|e| Error::config(format!("Failed to parse YAML OpenAPI spec: {}", e)))?;
107            serde_json::to_value(&yaml_value).map_err(|e| {
108                Error::config(format!("Failed to convert YAML OpenAPI spec to JSON: {}", e))
109            })?
110        } else {
111            serde_json::from_str(content)
112                .map_err(|e| Error::config(format!("Failed to parse JSON OpenAPI spec: {}", e)))?
113        };
114
115        // Check if this is a Swagger 2.0 spec and convert if necessary
116        let (raw_document, spec) = if swagger_convert::is_swagger_2(&raw_json) {
117            let converted =
118                swagger_convert::convert_swagger_to_openapi3(&raw_json).map_err(|e| {
119                    Error::config(format!("Failed to convert Swagger 2.0 to OpenAPI 3.0: {}", e))
120                })?;
121            let spec: OpenAPI = serde_json::from_value(converted.clone()).map_err(|e| {
122                Error::config(format!("Failed to parse converted OpenAPI spec: {}", e))
123            })?;
124            (converted, spec)
125        } else {
126            let spec: OpenAPI = serde_json::from_value(raw_json.clone())
127                .map_err(|e| Error::io_with_context("reading OpenAPI spec", e.to_string()))?;
128            (raw_json, spec)
129        };
130
131        Ok(Self {
132            spec,
133            file_path: None,
134            raw_document: Some(raw_document),
135        })
136    }
137
138    /// Load OpenAPI spec from JSON value
139    ///
140    /// Supports both OpenAPI 3.x and Swagger 2.0 specifications.
141    /// Swagger 2.0 specs are automatically converted to OpenAPI 3.0 format.
142    pub fn from_json(json: serde_json::Value) -> Result<Self> {
143        // Check if this is a Swagger 2.0 spec and convert if necessary
144        let (raw_document, spec) = if swagger_convert::is_swagger_2(&json) {
145            let converted = swagger_convert::convert_swagger_to_openapi3(&json).map_err(|e| {
146                Error::config(format!("Failed to convert Swagger 2.0 to OpenAPI 3.0: {}", e))
147            })?;
148            let spec: OpenAPI = serde_json::from_value(converted.clone()).map_err(|e| {
149                Error::config(format!("Failed to parse converted OpenAPI spec: {}", e))
150            })?;
151            (converted, spec)
152        } else {
153            let json_for_doc = json.clone();
154            let spec: OpenAPI = serde_json::from_value(json)
155                .map_err(|e| Error::config(format!("Failed to parse JSON OpenAPI spec: {}", e)))?;
156            (json_for_doc, spec)
157        };
158
159        Ok(Self {
160            spec,
161            file_path: None,
162            raw_document: Some(raw_document),
163        })
164    }
165
166    /// Validate the OpenAPI specification
167    ///
168    /// This method provides basic validation. For comprehensive validation
169    /// with detailed error messages, use `spec_parser::OpenApiValidator::validate()`.
170    pub fn validate(&self) -> Result<()> {
171        // Basic validation - check that we have at least one path
172        if self.spec.paths.paths.is_empty() {
173            return Err(Error::validation("OpenAPI spec must contain at least one path"));
174        }
175
176        // Check that info section has required fields
177        if self.spec.info.title.is_empty() {
178            return Err(Error::validation("OpenAPI spec info must have a title"));
179        }
180
181        if self.spec.info.version.is_empty() {
182            return Err(Error::validation("OpenAPI spec info must have a version"));
183        }
184
185        Ok(())
186    }
187
188    /// Enhanced validation with detailed error reporting
189    pub fn validate_enhanced(&self) -> crate::spec_parser::ValidationResult {
190        // Convert to JSON value for enhanced validator
191        if let Some(raw) = &self.raw_document {
192            let format = if raw.get("swagger").is_some() {
193                crate::spec_parser::SpecFormat::OpenApi20
194            } else if let Some(version) = raw.get("openapi").and_then(|v| v.as_str()) {
195                if version.starts_with("3.1") {
196                    crate::spec_parser::SpecFormat::OpenApi31
197                } else {
198                    crate::spec_parser::SpecFormat::OpenApi30
199                }
200            } else {
201                // Default to 3.0 if we can't determine
202                crate::spec_parser::SpecFormat::OpenApi30
203            };
204            crate::spec_parser::OpenApiValidator::validate(raw, format)
205        } else {
206            // Fallback to basic validation if no raw document
207            crate::spec_parser::ValidationResult::failure(vec![
208                crate::spec_parser::ValidationError::new(
209                    "Cannot perform enhanced validation without raw document".to_string(),
210                ),
211            ])
212        }
213    }
214
215    /// Get the OpenAPI version
216    pub fn version(&self) -> &str {
217        &self.spec.openapi
218    }
219
220    /// Get the API title
221    pub fn title(&self) -> &str {
222        &self.spec.info.title
223    }
224
225    /// Get the API description
226    pub fn description(&self) -> Option<&str> {
227        self.spec.info.description.as_deref()
228    }
229
230    /// Get the API version
231    pub fn api_version(&self) -> &str {
232        &self.spec.info.version
233    }
234
235    /// Get the server URLs
236    pub fn servers(&self) -> &[openapiv3::Server] {
237        &self.spec.servers
238    }
239
240    /// Get all paths defined in the spec
241    pub fn paths(&self) -> &openapiv3::Paths {
242        &self.spec.paths
243    }
244
245    /// Get all schemas defined in the spec
246    pub fn schemas(&self) -> Option<&indexmap::IndexMap<String, ReferenceOr<Schema>>> {
247        self.spec.components.as_ref().map(|c| &c.schemas)
248    }
249
250    /// Get all security schemes defined in the spec
251    pub fn security_schemes(
252        &self,
253    ) -> Option<&indexmap::IndexMap<String, ReferenceOr<openapiv3::SecurityScheme>>> {
254        self.spec.components.as_ref().map(|c| &c.security_schemes)
255    }
256
257    /// Get all operations for a given path
258    pub fn operations_for_path(
259        &self,
260        path: &str,
261    ) -> std::collections::HashMap<String, openapiv3::Operation> {
262        let mut operations = std::collections::HashMap::new();
263
264        if let Some(path_item_ref) = self.spec.paths.paths.get(path) {
265            // Handle the ReferenceOr<PathItem> case
266            if let Some(path_item) = path_item_ref.as_item() {
267                // Round 40 (#888 / #79) — Srikanth's Google Apigee
268                // spec puts the shared auth / format query
269                // parameters at PATH level, not on each operation.
270                // OpenAPI 3.0 §4.7.10.1: "Parameters that are
271                // included in the Operation Object inherit the
272                // parameters defined in the Path Item Object. If a
273                // parameter is already defined at the Path Item, the
274                // new definition will override it but can never
275                // remove it." We materialise that inheritance HERE
276                // (the lowest common point under both registry
277                // builders), so a request that violates a path-level
278                // `enum` or `type: boolean` reaches the validator's
279                // parameter loop instead of silently passing. We
280                // also resolve `$ref` parameters via
281                // `components.parameters` so the validator's loop
282                // (which skips `ReferenceOr::Reference` entries via
283                // `as_item()`) actually sees them.
284                let resolved_path_params: Vec<openapiv3::ReferenceOr<openapiv3::Parameter>> =
285                    path_item.parameters.iter().map(|p| self.resolve_parameter_ref(p)).collect();
286                let merge = |op: &openapiv3::Operation| -> openapiv3::Operation {
287                    // Resolve op-level refs too — same as path-level.
288                    let mut resolved_op = op.clone();
289                    resolved_op.parameters =
290                        op.parameters.iter().map(|p| self.resolve_parameter_ref(p)).collect();
291                    merge_path_params_into_operation(&resolved_op, &resolved_path_params)
292                };
293                if let Some(op) = &path_item.get {
294                    operations.insert("GET".to_string(), merge(op));
295                }
296                if let Some(op) = &path_item.post {
297                    operations.insert("POST".to_string(), merge(op));
298                }
299                if let Some(op) = &path_item.put {
300                    operations.insert("PUT".to_string(), merge(op));
301                }
302                if let Some(op) = &path_item.delete {
303                    operations.insert("DELETE".to_string(), merge(op));
304                }
305                if let Some(op) = &path_item.patch {
306                    operations.insert("PATCH".to_string(), merge(op));
307                }
308                if let Some(op) = &path_item.head {
309                    operations.insert("HEAD".to_string(), merge(op));
310                }
311                if let Some(op) = &path_item.options {
312                    operations.insert("OPTIONS".to_string(), merge(op));
313                }
314                if let Some(op) = &path_item.trace {
315                    operations.insert("TRACE".to_string(), merge(op));
316                }
317            }
318        }
319
320        operations
321    }
322
323    /// Get all paths with their operations
324    pub fn all_paths_and_operations(
325        &self,
326    ) -> std::collections::HashMap<String, std::collections::HashMap<String, openapiv3::Operation>>
327    {
328        self.spec
329            .paths
330            .paths
331            .iter()
332            .map(|(path, _)| (path.clone(), self.operations_for_path(path)))
333            .collect()
334    }
335
336    /// Get a schema by reference (returns wrapped OpenApiSchema)
337    pub fn get_schema(&self, reference: &str) -> Option<crate::schema::OpenApiSchema> {
338        self.resolve_schema(reference).map(crate::schema::OpenApiSchema::new)
339    }
340
341    /// Resolve a schema reference to the raw Schema
342    ///
343    /// This resolves `$ref` references like `#/components/schemas/User` to the
344    /// actual schema definition, handling nested references recursively.
345    pub fn resolve_schema_ref(&self, reference: &str) -> Option<Schema> {
346        self.resolve_schema(reference)
347    }
348
349    /// Round 40 (#888 / #79) — resolve a parameter `$ref` (typically
350    /// `#/components/parameters/foo`) into the inline `Parameter`
351    /// item it points at. Returns the input unchanged when the
352    /// reference can't be resolved (e.g. external `$ref`) so the
353    /// validator can fall back to its prior behaviour (skip via
354    /// `as_item()`) instead of panicking. Used by
355    /// `operations_for_path` to materialise refs at registry build
356    /// time, since the validator's parameter loop skips
357    /// `ReferenceOr::Reference` entries — which was why Srikanth's
358    /// Google Apigee spec silently passed every path-level param
359    /// violation: the path-level `parameters:` list is entirely
360    /// `$ref:` to shared common params like `_.xgafv`,
361    /// `prettyPrint`, etc.
362    pub fn resolve_parameter_ref(
363        &self,
364        p_ref: &openapiv3::ReferenceOr<openapiv3::Parameter>,
365    ) -> openapiv3::ReferenceOr<openapiv3::Parameter> {
366        match p_ref {
367            openapiv3::ReferenceOr::Item(_) => p_ref.clone(),
368            openapiv3::ReferenceOr::Reference { reference } => {
369                let Some(name) = reference.strip_prefix("#/components/parameters/") else {
370                    return p_ref.clone();
371                };
372                let Some(components) = self.spec.components.as_ref() else {
373                    return p_ref.clone();
374                };
375                match components.parameters.get(name) {
376                    Some(openapiv3::ReferenceOr::Item(p)) => {
377                        openapiv3::ReferenceOr::Item(p.clone())
378                    }
379                    Some(openapiv3::ReferenceOr::Reference { reference: nested }) => {
380                        // Tail-resolve a chained ref (rare in practice
381                        // but allowed by the spec).
382                        let Some(nested_name) = nested.strip_prefix("#/components/parameters/")
383                        else {
384                            return p_ref.clone();
385                        };
386                        match components.parameters.get(nested_name) {
387                            Some(openapiv3::ReferenceOr::Item(p)) => {
388                                openapiv3::ReferenceOr::Item(p.clone())
389                            }
390                            _ => p_ref.clone(),
391                        }
392                    }
393                    None => p_ref.clone(),
394                }
395            }
396        }
397    }
398
399    /// Validate security requirements
400    pub fn validate_security_requirements(
401        &self,
402        security_requirements: &[openapiv3::SecurityRequirement],
403        auth_header: Option<&str>,
404        api_key: Option<&str>,
405    ) -> Result<()> {
406        if security_requirements.is_empty() {
407            return Ok(());
408        }
409
410        // Security requirements are OR'd - if any requirement is satisfied, pass
411        for requirement in security_requirements {
412            if self.is_security_requirement_satisfied(requirement, auth_header, api_key)? {
413                return Ok(());
414            }
415        }
416
417        Err(Error::validation(
418            "Security validation failed: no valid authentication provided",
419        ))
420    }
421
422    fn resolve_schema(&self, reference: &str) -> Option<Schema> {
423        let mut visited = HashSet::new();
424        self.resolve_schema_recursive(reference, &mut visited)
425    }
426
427    fn resolve_schema_recursive(
428        &self,
429        reference: &str,
430        visited: &mut HashSet<String>,
431    ) -> Option<Schema> {
432        if !visited.insert(reference.to_string()) {
433            tracing::warn!("Detected recursive schema reference: {}", reference);
434            return None;
435        }
436
437        let schema_name = reference.strip_prefix("#/components/schemas/")?;
438        let components = self.spec.components.as_ref()?;
439        let schema_ref = components.schemas.get(schema_name)?;
440
441        match schema_ref {
442            ReferenceOr::Item(schema) => Some(schema.clone()),
443            ReferenceOr::Reference { reference: nested } => {
444                self.resolve_schema_recursive(nested, visited)
445            }
446        }
447    }
448
449    /// Check if a single security requirement is satisfied
450    fn is_security_requirement_satisfied(
451        &self,
452        requirement: &openapiv3::SecurityRequirement,
453        auth_header: Option<&str>,
454        api_key: Option<&str>,
455    ) -> Result<bool> {
456        // All schemes in the requirement must be satisfied (AND)
457        for (scheme_name, _scopes) in requirement {
458            if !self.is_security_scheme_satisfied(scheme_name, auth_header, api_key)? {
459                return Ok(false);
460            }
461        }
462        Ok(true)
463    }
464
465    /// Check if a security scheme is satisfied
466    fn is_security_scheme_satisfied(
467        &self,
468        scheme_name: &str,
469        auth_header: Option<&str>,
470        api_key: Option<&str>,
471    ) -> Result<bool> {
472        let security_schemes = match self.security_schemes() {
473            Some(schemes) => schemes,
474            None => return Ok(false),
475        };
476
477        let scheme = match security_schemes.get(scheme_name) {
478            Some(scheme) => scheme,
479            None => {
480                return Err(Error::config(format!("Security scheme '{}' not found", scheme_name)))
481            }
482        };
483
484        let scheme = match scheme {
485            ReferenceOr::Item(s) => s,
486            ReferenceOr::Reference { reference } => {
487                // Resolve $ref like "#/components/securitySchemes/BearerAuth"
488                let ref_name =
489                    reference.strip_prefix("#/components/securitySchemes/").ok_or_else(|| {
490                        Error::config(format!(
491                            "Unsupported security scheme reference format: {}",
492                            reference
493                        ))
494                    })?;
495                match security_schemes.get(ref_name) {
496                    Some(ReferenceOr::Item(resolved)) => resolved,
497                    Some(ReferenceOr::Reference { .. }) => {
498                        return Err(Error::config(format!(
499                            "Nested security scheme reference not supported: {}",
500                            ref_name
501                        )))
502                    }
503                    None => {
504                        return Err(Error::config(format!(
505                            "Security scheme '{}' not found",
506                            ref_name
507                        )))
508                    }
509                }
510            }
511        };
512
513        match scheme {
514            openapiv3::SecurityScheme::HTTP { scheme, .. } => {
515                match scheme.as_str() {
516                    "bearer" => match auth_header {
517                        Some(header) if header.starts_with("Bearer ") => Ok(true),
518                        _ => Ok(false),
519                    },
520                    "basic" => match auth_header {
521                        Some(header) if header.starts_with("Basic ") => Ok(true),
522                        _ => Ok(false),
523                    },
524                    _ => Ok(false), // Unsupported scheme
525                }
526            }
527            openapiv3::SecurityScheme::APIKey { location, .. } => match location {
528                openapiv3::APIKeyLocation::Header => Ok(auth_header.is_some()),
529                openapiv3::APIKeyLocation::Query => Ok(api_key.is_some()),
530                openapiv3::APIKeyLocation::Cookie => Ok(api_key.is_some()),
531            },
532            openapiv3::SecurityScheme::OpenIDConnect { .. } => {
533                // OpenID Connect uses Bearer tokens, same as OAuth2
534                match auth_header {
535                    Some(header) if header.starts_with("Bearer ") => Ok(true),
536                    _ => Ok(false),
537                }
538            }
539            openapiv3::SecurityScheme::OAuth2 { .. } => {
540                // For OAuth2, check if Bearer token is provided
541                match auth_header {
542                    Some(header) if header.starts_with("Bearer ") => Ok(true),
543                    _ => Ok(false),
544                }
545            }
546        }
547    }
548
549    /// Get global security requirements
550    pub fn get_global_security_requirements(&self) -> Vec<openapiv3::SecurityRequirement> {
551        self.spec.security.clone().unwrap_or_default()
552    }
553
554    /// Resolve a request body reference
555    pub fn get_request_body(&self, reference: &str) -> Option<&openapiv3::RequestBody> {
556        if let Some(components) = &self.spec.components {
557            if let Some(param_name) = reference.strip_prefix("#/components/requestBodies/") {
558                if let Some(request_body_ref) = components.request_bodies.get(param_name) {
559                    return request_body_ref.as_item();
560                }
561            }
562        }
563        None
564    }
565
566    /// Resolve a response reference
567    pub fn get_response(&self, reference: &str) -> Option<&openapiv3::Response> {
568        if let Some(components) = &self.spec.components {
569            if let Some(response_name) = reference.strip_prefix("#/components/responses/") {
570                if let Some(response_ref) = components.responses.get(response_name) {
571                    return response_ref.as_item();
572                }
573            }
574        }
575        None
576    }
577
578    /// Resolve an example reference
579    pub fn get_example(&self, reference: &str) -> Option<&openapiv3::Example> {
580        if let Some(components) = &self.spec.components {
581            if let Some(example_name) = reference.strip_prefix("#/components/examples/") {
582                if let Some(example_ref) = components.examples.get(example_name) {
583                    return example_ref.as_item();
584                }
585            }
586        }
587        None
588    }
589}
590
591/// Round 40 (#888 / #79) — merge path-level parameters from a
592/// `PathItem` into an `Operation`'s own parameters per OpenAPI 3.0
593/// §4.7.10.1. Returns a cloned `Operation` whose `parameters` list
594/// contains every path-level entry, followed by every operation-level
595/// entry, with collisions on `(name, in)` resolved in favour of the
596/// operation-level definition. The original `Operation` is not
597/// mutated. Lives in `spec.rs` so every registry builder that calls
598/// `operations_for_path` benefits from the merge automatically.
599pub(crate) fn merge_path_params_into_operation(
600    operation: &openapiv3::Operation,
601    path_level_params: &[openapiv3::ReferenceOr<openapiv3::Parameter>],
602) -> openapiv3::Operation {
603    use std::collections::HashSet;
604    if path_level_params.is_empty() {
605        return operation.clone();
606    }
607    let mut op_keys: HashSet<(String, String)> = HashSet::new();
608    for p_ref in &operation.parameters {
609        if let Some(key) = parameter_key(p_ref) {
610            op_keys.insert(key);
611        }
612    }
613    let mut merged: Vec<openapiv3::ReferenceOr<openapiv3::Parameter>> =
614        Vec::with_capacity(path_level_params.len() + operation.parameters.len());
615    for p_ref in path_level_params {
616        match parameter_key(p_ref) {
617            Some(key) if op_keys.contains(&key) => {}
618            _ => merged.push(p_ref.clone()),
619        }
620    }
621    merged.extend(operation.parameters.iter().cloned());
622    let mut cloned = operation.clone();
623    cloned.parameters = merged;
624    cloned
625}
626
627fn parameter_key(p_ref: &openapiv3::ReferenceOr<openapiv3::Parameter>) -> Option<(String, String)> {
628    let p = p_ref.as_item()?;
629    let (name, in_loc) = match p {
630        openapiv3::Parameter::Path { parameter_data, .. } => (parameter_data.name.clone(), "path"),
631        openapiv3::Parameter::Query { parameter_data, .. } => {
632            (parameter_data.name.clone(), "query")
633        }
634        openapiv3::Parameter::Header { parameter_data, .. } => {
635            (parameter_data.name.clone(), "header")
636        }
637        openapiv3::Parameter::Cookie { parameter_data, .. } => {
638            (parameter_data.name.clone(), "cookie")
639        }
640    };
641    Some((name, in_loc.to_string()))
642}
643
644#[cfg(test)]
645mod tests {
646    use super::*;
647    use openapiv3::{SchemaKind, Type};
648
649    #[test]
650    fn resolves_security_scheme_ref() {
651        let yaml = r#"
652openapi: 3.0.3
653info:
654  title: Test API
655  version: "1.0.0"
656paths:
657  /test:
658    get:
659      security:
660        - BearerRef: []
661      responses:
662        '200':
663          description: OK
664components:
665  securitySchemes:
666    BearerAuth:
667      type: http
668      scheme: bearer
669    BearerRef:
670      $ref: '#/components/securitySchemes/BearerAuth'
671        "#;
672
673        let spec = OpenApiSpec::from_string(yaml, Some("yaml")).expect("spec parses");
674
675        // Bearer token should satisfy the referenced scheme
676        let result = spec
677            .is_security_scheme_satisfied("BearerRef", Some("Bearer token123"), None)
678            .expect("should resolve ref");
679        assert!(result);
680
681        // Missing token should fail
682        let result = spec
683            .is_security_scheme_satisfied("BearerRef", None, None)
684            .expect("should resolve ref");
685        assert!(!result);
686    }
687
688    #[test]
689    fn resolves_nested_schema_references() {
690        let yaml = r#"
691openapi: 3.0.3
692info:
693  title: Test API
694  version: "1.0.0"
695paths: {}
696components:
697  schemas:
698    Apiary:
699      type: object
700      properties:
701        id:
702          type: string
703        hive:
704          $ref: '#/components/schemas/Hive'
705    Hive:
706      type: object
707      properties:
708        name:
709          type: string
710    HiveWrapper:
711      $ref: '#/components/schemas/Hive'
712        "#;
713
714        let spec = OpenApiSpec::from_string(yaml, Some("yaml")).expect("spec parses");
715
716        let apiary = spec.get_schema("#/components/schemas/Apiary").expect("resolve apiary schema");
717        assert!(matches!(apiary.schema.schema_kind, SchemaKind::Type(Type::Object(_))));
718
719        let wrapper = spec
720            .get_schema("#/components/schemas/HiveWrapper")
721            .expect("resolve wrapper schema");
722        assert!(matches!(wrapper.schema.schema_kind, SchemaKind::Type(Type::Object(_))));
723    }
724}