Skip to main content

openapiv3_resolve/
lib.rs

1#![doc = include_str!("../README.md")]
2#![forbid(unsafe_code)]
3#![deny(missing_docs)]
4#![deny(
5    clippy::unwrap_used,
6    clippy::expect_used,
7    clippy::indexing_slicing,
8    clippy::panic
9)]
10
11pub use indexmap;
12pub use openapiv3;
13
14mod component;
15mod error;
16mod reference;
17mod resolved;
18
19pub use component::Component;
20pub use error::ResolveError;
21pub use reference::Section;
22pub use resolved::*;
23
24use openapiv3::{OpenAPI, ReferenceOr};
25use reference::ComponentRef;
26use std::borrow::Cow;
27
28/// How many `$ref` hops a single resolution may follow.
29///
30/// A cyclic document has no inline item at the end of the chain, so without a
31/// bound the walk never terminates. Chains this long do not occur in practice.
32pub const MAX_REFERENCE_HOPS: usize = 100;
33
34/// Resolves a `$ref` pointer against a whole document.
35pub trait Resolve {
36    /// Resolves `reference` to a component, following chains of `$ref`s.
37    ///
38    /// The type argument decides which section is searched, so it usually has
39    /// to be named: `openapi.resolve_ref::<Schema>("#/components/schemas/Pet")`.
40    fn resolve_ref<'a, T: Component>(&'a self, reference: &str) -> Result<&'a T, ResolveError>;
41}
42
43/// Resolves a `ReferenceOr<T>` (or its boxed variant) to the item it denotes.
44pub trait ResolveWithOpenAPI<T> {
45    /// Returns the inline item, or the item the `$ref` points at.
46    ///
47    /// The result borrows from whichever of the two arguments it came from, so
48    /// its lifetime is the shorter of them: resolving out of a temporary
49    /// `ReferenceOr` yields a borrow that cannot outlive that temporary, even
50    /// when the value in fact came from `openapi`.
51    fn resolve<'a>(&'a self, openapi: &'a OpenAPI) -> Result<&'a T, ResolveError>;
52}
53
54/// Resolves an optional `ReferenceOr<T>` field.
55///
56/// Kept separate from [`ResolveWithOpenAPI`] so that an absent field — which
57/// is valid — stays distinguishable from a broken reference.
58pub trait ResolveOptionalWithOpenAPI<T> {
59    /// Returns `Ok(None)` if the field is absent, and an error only if a
60    /// reference that *is* present cannot be resolved.
61    fn resolve_optional<'a>(&'a self, openapi: &'a OpenAPI) -> Result<Option<&'a T>, ResolveError>;
62}
63
64impl Resolve for OpenAPI {
65    fn resolve_ref<'a, T: Component>(&'a self, reference: &str) -> Result<&'a T, ResolveError> {
66        walk(self, reference).map(|(_, item)| item)
67    }
68}
69
70impl<T: Component> ResolveWithOpenAPI<T> for ReferenceOr<T> {
71    fn resolve<'a>(&'a self, openapi: &'a OpenAPI) -> Result<&'a T, ResolveError> {
72        match self {
73            ReferenceOr::Item(item) => Ok(item),
74            ReferenceOr::Reference { reference } => openapi.resolve_ref(reference),
75        }
76    }
77}
78
79impl<T: Component> ResolveWithOpenAPI<T> for ReferenceOr<Box<T>> {
80    fn resolve<'a>(&'a self, openapi: &'a OpenAPI) -> Result<&'a T, ResolveError> {
81        match self {
82            ReferenceOr::Item(item) => Ok(item),
83            ReferenceOr::Reference { reference } => openapi.resolve_ref(reference),
84        }
85    }
86}
87
88impl<T, R> ResolveOptionalWithOpenAPI<T> for Option<R>
89where
90    R: ResolveWithOpenAPI<T>,
91{
92    fn resolve_optional<'a>(&'a self, openapi: &'a OpenAPI) -> Result<Option<&'a T>, ResolveError> {
93        match self {
94            Some(value) => value.resolve(openapi).map(Some),
95            None => Ok(None),
96        }
97    }
98}
99
100/// Follows `reference` to the inline item at the end of its chain, returning
101/// the name that item is stored under alongside it.
102///
103/// The name is what distinguishes this from [`Resolve::resolve_ref`]: a full
104/// resolution has to share the result between every reference to the same
105/// component, and the final name is the key to share it under.
106pub(crate) fn walk<'a, 'p, T: Component>(
107    openapi: &'a OpenAPI,
108    reference: &'p str,
109) -> Result<(Cow<'p, str>, &'a T), ResolveError>
110where
111    'a: 'p,
112{
113    // Iterative on purpose: recursing here let a cyclic document overflow
114    // the stack, which aborts the process instead of returning an error.
115    let mut pointer: &'p str = reference;
116    let mut hops: usize = 0;
117
118    loop {
119        let (parsed, entry) = lookup::<T>(openapi, pointer)?;
120        match entry {
121            ReferenceOr::Item(item) => return Ok((parsed.name, item)),
122            ReferenceOr::Reference { reference: next } => {
123                hops += 1;
124                if hops > MAX_REFERENCE_HOPS {
125                    return Err(ResolveError::ReferenceChainTooLong {
126                        reference: reference.to_owned(),
127                        last: next.clone(),
128                        max_hops: MAX_REFERENCE_HOPS,
129                    });
130                }
131                pointer = next.as_str();
132            }
133        }
134    }
135}
136
137/// Looks up one hop: parses the pointer and reads the entry, without following it.
138fn lookup<'a, 'p, T: Component>(
139    openapi: &'a OpenAPI,
140    pointer: &'p str,
141) -> Result<(ComponentRef<'p>, &'a ReferenceOr<T>), ResolveError> {
142    let parsed = ComponentRef::parse(pointer)?;
143
144    if parsed.section != T::SECTION {
145        return Err(ResolveError::SectionMismatch {
146            expected: T::SECTION,
147            found: parsed.section,
148        });
149    }
150
151    let entry = lookup_named::<T>(openapi, &parsed.name)?;
152    Ok((parsed, entry))
153}
154
155/// Reads the entry stored under `name` in `T`'s section, without following it.
156///
157/// The one entry point that takes a component *name* rather than a pointer,
158/// for the place the specification lets a document name a component directly:
159/// a discriminator mapping value.
160pub(crate) fn lookup_named<'a, T: Component>(
161    openapi: &'a OpenAPI,
162    name: &str,
163) -> Result<&'a ReferenceOr<T>, ResolveError> {
164    let section = T::section(openapi).ok_or(ResolveError::ComponentsMissing {
165        section: T::SECTION,
166    })?;
167    section.get(name).ok_or_else(|| ResolveError::NotFound {
168        section: T::SECTION,
169        name: name.to_owned(),
170    })
171}