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
6pub mod transformer;
7pub mod validator;
8
9pub use transformer::SpecTransformer;
10pub use validator::SpecValidator;
11
12use crate::error::Error;
13use openapiv3::{OpenAPI, Operation, Parameter, PathItem, ReferenceOr};
14use std::collections::HashSet;
15
16/// A helper type to iterate over all HTTP methods in a `PathItem`
17pub type HttpMethodsIter<'a> = [(&'static str, &'a Option<Operation>); 8];
18
19/// Creates an iterator over all HTTP methods and their operations in a `PathItem`
20///
21/// # Arguments
22/// * `item` - The `PathItem` to extract operations from
23///
24/// # Returns
25/// An array of tuples containing the HTTP method name and its optional operation
26#[must_use]
27pub const fn http_methods_iter(item: &PathItem) -> HttpMethodsIter<'_> {
28    [
29        ("GET", &item.get),
30        ("POST", &item.post),
31        ("PUT", &item.put),
32        ("DELETE", &item.delete),
33        ("PATCH", &item.patch),
34        ("HEAD", &item.head),
35        ("OPTIONS", &item.options),
36        ("TRACE", &item.trace),
37    ]
38}
39
40/// Maximum depth for resolving parameter references to prevent stack overflow
41pub const MAX_REFERENCE_DEPTH: usize = 10;
42
43/// Resolves a parameter reference to its actual parameter definition
44///
45/// # Arguments
46/// * `spec` - The `OpenAPI` specification containing the components
47/// * `reference` - The reference string (e.g., "#/components/parameters/userId")
48///
49/// # Returns
50/// * `Ok(Parameter)` - The resolved parameter
51/// * `Err(Error)` - If resolution fails
52///
53/// # Errors
54/// Returns an error if:
55/// - The reference format is invalid
56/// - The referenced parameter doesn't exist
57/// - Circular references are detected
58/// - Maximum reference depth is exceeded
59pub fn resolve_parameter_reference(spec: &OpenAPI, reference: &str) -> Result<Parameter, Error> {
60    let mut visited = HashSet::new();
61    resolve_parameter_reference_with_visited(spec, reference, &mut visited, 0)
62}
63
64/// Internal method that resolves parameter references with circular reference detection
65fn resolve_parameter_reference_with_visited(
66    spec: &OpenAPI,
67    reference: &str,
68    visited: &mut HashSet<String>,
69    depth: usize,
70) -> Result<Parameter, Error> {
71    // Check depth limit
72    if depth >= MAX_REFERENCE_DEPTH {
73        return Err(Error::Validation(format!(
74            "Maximum reference depth ({MAX_REFERENCE_DEPTH}) exceeded while resolving '{reference}'"
75        )));
76    }
77
78    // Check for circular references
79    if !visited.insert(reference.to_string()) {
80        return Err(Error::Validation(format!(
81            "Circular reference detected: '{reference}' is part of a reference cycle"
82        )));
83    }
84
85    // Parse the reference path
86    // Expected format: #/components/parameters/{parameter_name}
87    if !reference.starts_with("#/components/parameters/") {
88        return Err(Error::Validation(format!(
89            "Invalid parameter reference format: '{reference}'. Expected format: #/components/parameters/{{name}}"
90        )));
91    }
92
93    let param_name = reference
94        .strip_prefix("#/components/parameters/")
95        .ok_or_else(|| Error::Validation(format!("Invalid parameter reference: '{reference}'")))?;
96
97    // Look up the parameter in components
98    let components = spec.components.as_ref().ok_or_else(|| {
99        Error::Validation(
100            "Cannot resolve parameter reference: OpenAPI spec has no components section"
101                .to_string(),
102        )
103    })?;
104
105    let param_ref = components.parameters.get(param_name).ok_or_else(|| {
106        Error::Validation(format!("Parameter '{param_name}' not found in components"))
107    })?;
108
109    // Handle nested references (reference pointing to another reference)
110    match param_ref {
111        ReferenceOr::Item(param) => Ok(param.clone()),
112        ReferenceOr::Reference {
113            reference: nested_ref,
114        } => resolve_parameter_reference_with_visited(spec, nested_ref, visited, depth + 1),
115    }
116}