Skip to main content

openapi_nexus_spec/oas31/spec/
reference.rs

1use std::{str::FromStr, sync::OnceLock};
2
3use derive_more::Display;
4use regex::Regex;
5use serde::{Deserialize, Serialize};
6use snafu::Snafu;
7
8use super::OpenApiV31Spec;
9
10fn re_ref() -> &'static Regex {
11    static RE_REF: OnceLock<Regex> = OnceLock::new();
12    RE_REF.get_or_init(|| {
13        Regex::new("^(?P<source>[^#]*)#/components/(?P<type>[^/]+)/(?P<name>.+)$").unwrap()
14    })
15}
16
17/// Container for a type of OpenAPI object, or a reference to one.
18#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
19#[serde(untagged)]
20pub enum ObjectOrReference<T> {
21    /// Object reference.
22    ///
23    /// See <https://spec.openapis.org/oas/v3.1.1#reference-object>.
24    Ref {
25        /// Path, file reference, or URL pointing to object.
26        #[serde(rename = "$ref")]
27        ref_path: String,
28
29        /// Summary override.
30        #[serde(skip_serializing_if = "Option::is_none")]
31        summary: Option<String>,
32
33        /// Description override.
34        #[serde(skip_serializing_if = "Option::is_none")]
35        description: Option<String>,
36    },
37
38    /// Inline object.
39    Object(T),
40}
41
42impl<T> ObjectOrReference<T>
43where
44    T: FromRef,
45{
46    /// Resolves the object (if needed) from the given `spec` and returns it.
47    pub fn resolve(&self, spec: &OpenApiV31Spec) -> Result<T, ErrorRef> {
48        match self {
49            Self::Object(component) => Ok(component.clone()),
50            Self::Ref { ref_path, .. } => T::from_ref(spec, ref_path),
51        }
52    }
53}
54
55/// Object reference error.
56#[derive(Debug, Clone, PartialEq, Snafu)]
57#[snafu(visibility(pub))]
58pub enum ErrorRef {
59    /// Referenced object has unknown type.
60    #[snafu(display("Invalid type: {}", type_name))]
61    UnknownType { type_name: String },
62
63    /// Referenced object was not of expected type.
64    #[snafu(display("Mismatched type: cannot reference a {} as a {}", expected, actual))]
65    MismatchedType { expected: RefType, actual: RefType },
66
67    /// Reference path points outside the given spec file.
68    #[snafu(display("Unresolvable path: {}", path))]
69    Unresolvable { path: String },
70}
71
72/// Component type of a reference.
73#[derive(Debug, Clone, Copy, PartialEq, Display)]
74pub enum RefType {
75    /// Schema component type.
76    Schema,
77
78    /// Response component type.
79    Response,
80
81    /// Parameter component type.
82    Parameter,
83
84    /// Example component type.
85    Example,
86
87    /// Request body component type.
88    RequestBody,
89
90    /// Header component type.
91    Header,
92
93    /// Security scheme component type.
94    SecurityScheme,
95
96    /// Link component type.
97    Link,
98
99    /// Callback component type.
100    Callback,
101}
102
103impl FromStr for RefType {
104    type Err = ErrorRef;
105
106    fn from_str(typ: &str) -> Result<Self, Self::Err> {
107        Ok(match typ {
108            "schemas" => Self::Schema,
109            "responses" => Self::Response,
110            "parameters" => Self::Parameter,
111            "examples" => Self::Example,
112            "requestBodies" => Self::RequestBody,
113            "headers" => Self::Header,
114            "securitySchemes" => Self::SecurityScheme,
115            "links" => Self::Link,
116            "callbacks" => Self::Callback,
117            typ => {
118                return Err(ErrorRef::UnknownType {
119                    type_name: typ.to_owned(),
120                });
121            }
122        })
123    }
124}
125
126/// Parsed reference path.
127#[derive(Debug, Clone)]
128pub struct Ref {
129    /// Source file of the object being references.
130    pub source: String,
131
132    /// Type of object being referenced.
133    pub kind: RefType,
134
135    /// Name of object being referenced.
136    pub name: String,
137}
138
139impl FromStr for Ref {
140    type Err = ErrorRef;
141
142    fn from_str(path: &str) -> Result<Self, Self::Err> {
143        let parts = re_ref()
144            .captures(path)
145            .ok_or_else(|| ErrorRef::Unresolvable {
146                path: path.to_owned(),
147            })?;
148
149        Ok(Self {
150            source: parts["source"].to_owned(),
151            kind: parts["type"].parse()?,
152            name: parts["name"].to_owned(),
153        })
154    }
155}
156
157/// Find an object from a reference path (`$ref`).
158///
159/// Implemented for object types which can be shared via a spec's `components` object.
160pub trait FromRef: Clone {
161    /// Finds an object in `spec` using the given `path`.
162    fn from_ref(spec: &OpenApiV31Spec, path: &str) -> Result<Self, ErrorRef>;
163}
164
165#[cfg(test)]
166mod tests {
167    use serde_json::json;
168
169    use super::ObjectOrReference;
170
171    #[test]
172    fn ref_serialization_omits_empty_overrides() {
173        let reference = ObjectOrReference::<()>::Ref {
174            ref_path: "#/components/examples/RustMascot".to_owned(),
175            summary: None,
176            description: None,
177        };
178
179        let serialized = serde_json::to_value(reference).expect("serializing ref");
180
181        assert_eq!(
182            serialized,
183            json!({
184                "$ref": "#/components/examples/RustMascot",
185            })
186        );
187    }
188
189    #[test]
190    fn ref_serialization_includes_present_overrides() {
191        let reference = ObjectOrReference::<()>::Ref {
192            ref_path: "#/components/examples/RustMascot".to_owned(),
193            summary: Some("Rust mascot override".to_owned()),
194            description: Some("Let Ferris do the talking.".to_owned()),
195        };
196
197        let serialized = serde_json::to_value(reference).expect("serializing ref");
198
199        assert_eq!(
200            serialized,
201            json!({
202                "$ref": "#/components/examples/RustMascot",
203                "summary": "Rust mascot override",
204                "description": "Let Ferris do the talking.",
205            })
206        );
207    }
208}