Skip to main content

aperture_cli/spec/
mod.rs

1//! `OpenAPI` specification validation and transformation module
2//!
3//! This module separates the concerns of validating and transforming `OpenAPI` specifications
4//! into distinct, testable components following the Single Responsibility Principle.
5
6use crate::constants;
7
8pub mod parser;
9pub mod transformer;
10pub mod validator;
11
12pub use parser::parse_openapi;
13pub use transformer::SpecTransformer;
14pub use validator::SpecValidator;
15
16use crate::error::Error;
17use openapiv3::{OpenAPI, Operation, Parameter, PathItem, ReferenceOr};
18use std::collections::HashSet;
19
20/// A helper type to iterate over all HTTP methods in a `PathItem`
21pub type HttpMethodsIter<'a> = [(&'static str, &'a Option<Operation>); 8];
22
23/// Creates an iterator over all HTTP methods and their operations in a `PathItem`
24///
25/// # Arguments
26/// * `item` - The `PathItem` to extract operations from
27///
28/// # Returns
29/// An array of tuples containing the HTTP method name and its optional operation
30#[must_use]
31pub const fn http_methods_iter(item: &PathItem) -> HttpMethodsIter<'_> {
32    [
33        (constants::HTTP_METHOD_GET, &item.get),
34        (constants::HTTP_METHOD_POST, &item.post),
35        (constants::HTTP_METHOD_PUT, &item.put),
36        (constants::HTTP_METHOD_DELETE, &item.delete),
37        (constants::HTTP_METHOD_PATCH, &item.patch),
38        (constants::HTTP_METHOD_HEAD, &item.head),
39        (constants::HTTP_METHOD_OPTIONS, &item.options),
40        ("TRACE", &item.trace),
41    ]
42}
43
44/// Maximum depth for resolving parameter references to prevent stack overflow
45pub const MAX_REFERENCE_DEPTH: usize = 10;
46
47/// Resolves a parameter reference to its actual parameter definition
48///
49/// # Arguments
50/// * `spec` - The `OpenAPI` specification containing the components
51/// * `reference` - The reference string (e.g., "#/components/parameters/userId")
52///
53/// # Returns
54/// * `Ok(Parameter)` - The resolved parameter
55/// * `Err(Error)` - If resolution fails
56///
57/// # Errors
58/// Returns an error if:
59/// - The reference format is invalid
60/// - The referenced parameter doesn't exist
61/// - Circular references are detected
62/// - Maximum reference depth is exceeded
63pub fn resolve_parameter_reference(spec: &OpenAPI, reference: &str) -> Result<Parameter, Error> {
64    let mut visited = HashSet::new();
65    resolve_parameter_reference_with_visited(spec, reference, &mut visited, 0)
66}
67
68/// Resolves a schema reference to its actual schema definition
69///
70/// This function resolves top-level `$ref` references to schemas defined in
71/// `#/components/schemas/`. It handles chained references (schema A references
72/// schema B which references schema C) with circular reference detection.
73///
74/// # Arguments
75/// * `spec` - The `OpenAPI` specification containing the components
76/// * `reference` - The reference string (e.g., "#/components/schemas/User")
77///
78/// # Returns
79/// * `Ok(Schema)` - The resolved schema
80/// * `Err(Error)` - If resolution fails
81///
82/// # Errors
83/// Returns an error if:
84/// - The reference format is invalid
85/// - The referenced schema doesn't exist
86/// - Circular references are detected
87/// - Maximum reference depth is exceeded
88///
89/// # Limitations
90///
91/// **Nested references are not resolved**: This function only resolves the
92/// top-level schema reference. If the resolved schema contains nested `$ref`
93/// within its properties, those remain unresolved. For example:
94///
95/// ```json
96/// // #/components/schemas/Order resolves to:
97/// {
98///   "type": "object",
99///   "properties": {
100///     "customer": { "$ref": "#/components/schemas/Customer" }  // NOT resolved
101///   }
102/// }
103/// ```
104///
105/// Implementing recursive resolution of nested references would require
106/// traversing the entire schema tree, which adds complexity and risk of
107/// infinite loops with self-referential schemas (e.g., a `User` with a
108/// `friends: User[]` property).
109pub fn resolve_schema_reference(
110    spec: &OpenAPI,
111    reference: &str,
112) -> Result<openapiv3::Schema, Error> {
113    let mut visited = HashSet::new();
114    resolve_schema_reference_with_visited(spec, reference, &mut visited, 0)
115}
116
117/// Internal method that resolves schema references with circular reference detection
118fn resolve_schema_reference_with_visited(
119    spec: &OpenAPI,
120    reference: &str,
121    visited: &mut HashSet<String>,
122    depth: usize,
123) -> Result<openapiv3::Schema, Error> {
124    // Check depth limit
125    if depth >= MAX_REFERENCE_DEPTH {
126        return Err(Error::validation_error(format!(
127            "Maximum reference depth ({MAX_REFERENCE_DEPTH}) exceeded while resolving '{reference}'"
128        )));
129    }
130
131    // Check for circular references
132    if !visited.insert(reference.to_string()) {
133        return Err(Error::validation_error(format!(
134            "Circular reference detected: '{reference}' is part of a reference cycle"
135        )));
136    }
137
138    // Parse the reference path
139    // Expected format: #/components/schemas/{schema_name}
140    if !reference.starts_with("#/components/schemas/") {
141        return Err(Error::validation_error(format!(
142            "Invalid schema reference format: '{reference}'. Expected format: #/components/schemas/{{name}}"
143        )));
144    }
145
146    let schema_name = reference
147        .strip_prefix("#/components/schemas/")
148        .ok_or_else(|| {
149            Error::validation_error(format!("Invalid schema reference: '{reference}'"))
150        })?;
151
152    // Look up the schema in components
153    let components = spec.components.as_ref().ok_or_else(|| {
154        Error::validation_error(
155            "Cannot resolve schema reference: OpenAPI spec has no components section".to_string(),
156        )
157    })?;
158
159    let schema_ref = components.schemas.get(schema_name).ok_or_else(|| {
160        Error::validation_error(format!("Schema '{schema_name}' not found in components"))
161    })?;
162
163    // Handle nested references (reference pointing to another reference)
164    match schema_ref {
165        ReferenceOr::Item(schema) => Ok(schema.clone()),
166        ReferenceOr::Reference {
167            reference: nested_ref,
168        } => resolve_schema_reference_with_visited(spec, nested_ref, visited, depth + 1),
169    }
170}
171
172/// Internal method that resolves parameter references with circular reference detection
173fn resolve_parameter_reference_with_visited(
174    spec: &OpenAPI,
175    reference: &str,
176    visited: &mut HashSet<String>,
177    depth: usize,
178) -> Result<Parameter, Error> {
179    // Check depth limit
180    if depth >= MAX_REFERENCE_DEPTH {
181        return Err(Error::validation_error(format!(
182            "Maximum reference depth ({MAX_REFERENCE_DEPTH}) exceeded while resolving '{reference}'"
183        )));
184    }
185
186    // Check for circular references
187    if !visited.insert(reference.to_string()) {
188        return Err(Error::validation_error(format!(
189            "Circular reference detected: '{reference}' is part of a reference cycle"
190        )));
191    }
192
193    // Parse the reference path
194    // Expected format: #/components/parameters/{parameter_name}
195    if !reference.starts_with("#/components/parameters/") {
196        return Err(Error::validation_error(format!(
197            "Invalid parameter reference format: '{reference}'. Expected format: #/components/parameters/{{name}}"
198        )));
199    }
200
201    let param_name = reference
202        .strip_prefix("#/components/parameters/")
203        .ok_or_else(|| {
204            Error::validation_error(format!("Invalid parameter reference: '{reference}'"))
205        })?;
206
207    // Look up the parameter in components
208    let components = spec.components.as_ref().ok_or_else(|| {
209        Error::validation_error(
210            "Cannot resolve parameter reference: OpenAPI spec has no components section"
211                .to_string(),
212        )
213    })?;
214
215    let param_ref = components.parameters.get(param_name).ok_or_else(|| {
216        Error::validation_error(format!("Parameter '{param_name}' not found in components"))
217    })?;
218
219    // Handle nested references (reference pointing to another reference)
220    match param_ref {
221        ReferenceOr::Item(param) => Ok(param.clone()),
222        ReferenceOr::Reference {
223            reference: nested_ref,
224        } => resolve_parameter_reference_with_visited(spec, nested_ref, visited, depth + 1),
225    }
226}