openapi_nexus_spec/oas31/spec/
reference.rs1use 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#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
19#[serde(untagged)]
20pub enum ObjectOrReference<T> {
21 Ref {
25 #[serde(rename = "$ref")]
27 ref_path: String,
28
29 #[serde(skip_serializing_if = "Option::is_none")]
31 summary: Option<String>,
32
33 #[serde(skip_serializing_if = "Option::is_none")]
35 description: Option<String>,
36 },
37
38 Object(T),
40}
41
42impl<T> ObjectOrReference<T>
43where
44 T: FromRef,
45{
46 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#[derive(Debug, Clone, PartialEq, Snafu)]
57#[snafu(visibility(pub))]
58pub enum ErrorRef {
59 #[snafu(display("Invalid type: {}", type_name))]
61 UnknownType { type_name: String },
62
63 #[snafu(display("Mismatched type: cannot reference a {} as a {}", expected, actual))]
65 MismatchedType { expected: RefType, actual: RefType },
66
67 #[snafu(display("Unresolvable path: {}", path))]
69 Unresolvable { path: String },
70}
71
72#[derive(Debug, Clone, Copy, PartialEq, Display)]
74pub enum RefType {
75 Schema,
77
78 Response,
80
81 Parameter,
83
84 Example,
86
87 RequestBody,
89
90 Header,
92
93 SecurityScheme,
95
96 Link,
98
99 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#[derive(Debug, Clone)]
128pub struct Ref {
129 pub source: String,
131
132 pub kind: RefType,
134
135 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
157pub trait FromRef: Clone {
161 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}