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/// Internal method that resolves parameter references with circular reference detection
69fn resolve_parameter_reference_with_visited(
70    spec: &OpenAPI,
71    reference: &str,
72    visited: &mut HashSet<String>,
73    depth: usize,
74) -> Result<Parameter, Error> {
75    // Check depth limit
76    if depth >= MAX_REFERENCE_DEPTH {
77        return Err(Error::validation_error(format!(
78            "Maximum reference depth ({MAX_REFERENCE_DEPTH}) exceeded while resolving '{reference}'"
79        )));
80    }
81
82    // Check for circular references
83    if !visited.insert(reference.to_string()) {
84        return Err(Error::validation_error(format!(
85            "Circular reference detected: '{reference}' is part of a reference cycle"
86        )));
87    }
88
89    // Parse the reference path
90    // Expected format: #/components/parameters/{parameter_name}
91    if !reference.starts_with("#/components/parameters/") {
92        return Err(Error::validation_error(format!(
93            "Invalid parameter reference format: '{reference}'. Expected format: #/components/parameters/{{name}}"
94        )));
95    }
96
97    let param_name = reference
98        .strip_prefix("#/components/parameters/")
99        .ok_or_else(|| {
100            Error::validation_error(format!("Invalid parameter reference: '{reference}'"))
101        })?;
102
103    // Look up the parameter in components
104    let components = spec.components.as_ref().ok_or_else(|| {
105        Error::validation_error(
106            "Cannot resolve parameter reference: OpenAPI spec has no components section"
107                .to_string(),
108        )
109    })?;
110
111    let param_ref = components.parameters.get(param_name).ok_or_else(|| {
112        Error::validation_error(format!("Parameter '{param_name}' not found in components"))
113    })?;
114
115    // Handle nested references (reference pointing to another reference)
116    match param_ref {
117        ReferenceOr::Item(param) => Ok(param.clone()),
118        ReferenceOr::Reference {
119            reference: nested_ref,
120        } => resolve_parameter_reference_with_visited(spec, nested_ref, visited, depth + 1),
121    }
122}