1use crate::{
2 OpenApiContentFormat, OpenApiDocumentLoadError, OpenApiSource, OpenApiSpecificationVersion,
3 RawOpenApiDocument, TypedOpenApiDocument, TypedOpenApiParseError, load_raw_document,
4 load_raw_document_from_source, parse_typed_document,
5};
6
7pub enum LoadedOpenApiDocument {
8 Swagger2 {
9 raw: RawOpenApiDocument,
10 },
11 OpenApi30 {
12 raw: RawOpenApiDocument,
13 document: openapiv3::OpenAPI,
14 },
15 OpenApi31 {
16 raw: RawOpenApiDocument,
17 document: openapiv3_1::OpenApi,
18 },
19 OpenApi31Raw {
20 raw: RawOpenApiDocument,
21 },
22}
23
24impl LoadedOpenApiDocument {
25 pub fn raw(&self) -> &RawOpenApiDocument {
26 match self {
27 Self::Swagger2 { raw }
28 | Self::OpenApi30 { raw, .. }
29 | Self::OpenApi31 { raw, .. }
30 | Self::OpenApi31Raw { raw } => raw,
31 }
32 }
33
34 pub fn source(&self) -> &OpenApiSource {
35 self.raw().source()
36 }
37
38 pub fn format(&self) -> OpenApiContentFormat {
39 self.raw().format()
40 }
41
42 pub fn specification_version(&self) -> OpenApiSpecificationVersion {
43 match self {
44 Self::Swagger2 { .. } => OpenApiSpecificationVersion::Swagger2,
45 Self::OpenApi30 { .. } => OpenApiSpecificationVersion::OpenApi30,
46 Self::OpenApi31 { .. } | Self::OpenApi31Raw { .. } => {
47 OpenApiSpecificationVersion::OpenApi31
48 }
49 }
50 }
51
52 pub fn as_openapi30(&self) -> Option<&openapiv3::OpenAPI> {
53 match self {
54 Self::Swagger2 { .. } | Self::OpenApi31 { .. } | Self::OpenApi31Raw { .. } => {
55 None
56 }
57 Self::OpenApi30 { document, .. } => Some(document),
58 }
59 }
60
61 pub fn as_openapi31(&self) -> Option<&openapiv3_1::OpenApi> {
62 match self {
63 Self::Swagger2 { .. } | Self::OpenApi30 { .. } | Self::OpenApi31Raw { .. } => {
64 None
65 }
66 Self::OpenApi31 { document, .. } => Some(document),
67 }
68 }
69}
70
71pub fn load_document(input: &str) -> Result<LoadedOpenApiDocument, OpenApiDocumentLoadError> {
72 load_document_with_options(input, false)
73}
74
75pub(crate) fn load_document_with_options(
76 input: &str,
77 tolerate_invalid_openapi31: bool,
78) -> Result<LoadedOpenApiDocument, OpenApiDocumentLoadError> {
79 let raw = load_raw_document(input).map_err(OpenApiDocumentLoadError::RawLoad)?;
80 load_document_from_raw_with_options(raw, tolerate_invalid_openapi31)
81}
82
83pub fn load_document_from_source(
84 source: OpenApiSource,
85) -> Result<LoadedOpenApiDocument, OpenApiDocumentLoadError> {
86 load_document_from_source_with_options(source, false)
87}
88
89pub(crate) fn load_document_from_source_with_options(
90 source: OpenApiSource,
91 tolerate_invalid_openapi31: bool,
92) -> Result<LoadedOpenApiDocument, OpenApiDocumentLoadError> {
93 let raw = load_raw_document_from_source(source).map_err(OpenApiDocumentLoadError::RawLoad)?;
94 load_document_from_raw_with_options(raw, tolerate_invalid_openapi31)
95}
96
97pub fn load_document_from_raw(
98 raw: RawOpenApiDocument,
99) -> Result<LoadedOpenApiDocument, OpenApiDocumentLoadError> {
100 load_document_from_raw_with_options(raw, false)
101}
102
103pub(crate) fn load_document_from_raw_with_options(
104 raw: RawOpenApiDocument,
105 tolerate_invalid_openapi31: bool,
106) -> Result<LoadedOpenApiDocument, OpenApiDocumentLoadError> {
107 if matches!(
108 raw.specification_version(),
109 Ok(OpenApiSpecificationVersion::Swagger2)
110 ) {
111 return Ok(LoadedOpenApiDocument::Swagger2 { raw });
112 }
113
114 match parse_typed_document(&raw) {
115 Ok(TypedOpenApiDocument::OpenApi30(document)) => {
116 Ok(LoadedOpenApiDocument::OpenApi30 { raw, document })
117 }
118 Ok(TypedOpenApiDocument::OpenApi31(document)) => {
119 Ok(LoadedOpenApiDocument::OpenApi31 { raw, document })
120 }
121 Err(TypedOpenApiParseError::Deserialize {
122 version: OpenApiSpecificationVersion::OpenApi31,
123 ..
124 }) if should_fallback_to_raw_openapi31(&raw, tolerate_invalid_openapi31) => {
125 Ok(LoadedOpenApiDocument::OpenApi31Raw { raw })
126 }
127 Err(error) => Err(OpenApiDocumentLoadError::TypedParse(error)),
128 }
129}
130
131fn should_fallback_to_raw_openapi31(
132 raw: &RawOpenApiDocument,
133 tolerate_invalid_openapi31: bool,
134) -> bool {
135 matches!(
136 raw.specification_version(),
137 Ok(OpenApiSpecificationVersion::OpenApi31)
138 ) && (is_webhook_only_openapi31_document(raw) || tolerate_invalid_openapi31)
139}
140
141fn is_webhook_only_openapi31_document(raw: &RawOpenApiDocument) -> bool {
142 matches!(
143 raw.specification_version(),
144 Ok(OpenApiSpecificationVersion::OpenApi31)
145 ) && raw.value().get("paths").is_none()
146 && raw
147 .value()
148 .get("webhooks")
149 .and_then(serde_json::Value::as_object)
150 .is_some()
151}
152
153#[cfg(test)]
154mod tests {
155 use std::{
156 fs,
157 path::{Path, PathBuf},
158 sync::atomic::{AtomicU64, Ordering},
159 };
160
161 use crate::{OpenApiSource, OpenApiSpecificationVersion, decode_raw_document};
162
163 use super::{
164 LoadedOpenApiDocument, load_document, load_document_from_raw,
165 load_document_from_raw_with_options, load_document_from_source,
166 };
167
168 static TEST_ARTIFACT_ID: AtomicU64 = AtomicU64::new(0);
169
170 #[test]
171 fn loads_openapi_thirty_documents_from_raw_input() {
172 let raw = decode_raw_document(
173 OpenApiSource::Path(PathBuf::from("openapi.json")),
174 r#"{
175 "openapi": "3.0.2",
176 "info": { "title": "Example", "version": "1.0.0" },
177 "paths": {}
178 }"#,
179 )
180 .unwrap();
181
182 let loaded = load_document_from_raw(raw).unwrap();
183
184 assert!(matches!(loaded, LoadedOpenApiDocument::OpenApi30 { .. }));
185 assert_eq!(
186 loaded.specification_version(),
187 OpenApiSpecificationVersion::OpenApi30
188 );
189 assert!(loaded.as_openapi30().is_some());
190 assert!(loaded.as_openapi31().is_none());
191 }
192
193 #[test]
194 fn loads_openapi_thirty_one_documents_from_a_source() {
195 let file = TestFile::new(
196 "openapi.yaml",
197 "openapi: 3.1.0\ninfo:\n title: Example\n version: 1.0.0\npaths: {}\n",
198 );
199
200 let loaded =
201 load_document_from_source(OpenApiSource::Path(file.path().to_path_buf())).unwrap();
202
203 assert!(matches!(loaded, LoadedOpenApiDocument::OpenApi31 { .. }));
204 assert_eq!(
205 loaded.specification_version(),
206 OpenApiSpecificationVersion::OpenApi31
207 );
208 assert_eq!(
209 loaded.source(),
210 &OpenApiSource::Path(file.path().to_path_buf())
211 );
212 }
213
214 #[test]
215 fn loads_webhook_only_openapi_thirty_one_documents_with_a_raw_fallback() {
216 let raw = decode_raw_document(
217 OpenApiSource::Path(PathBuf::from("test/OpenAPI/v3.1/webhook-example.json")),
218 include_str!("../../../../test/OpenAPI/v3.1/webhook-example.json"),
219 )
220 .unwrap();
221
222 let loaded = load_document_from_raw(raw).unwrap();
223
224 assert!(matches!(
225 loaded,
226 LoadedOpenApiDocument::OpenApi31Raw { .. }
227 ));
228 assert_eq!(
229 loaded.specification_version(),
230 OpenApiSpecificationVersion::OpenApi31
231 );
232 assert!(loaded.as_openapi31().is_none());
233 }
234
235 #[test]
236 fn tolerant_loader_accepts_invalid_openapi_thirty_one_documents() {
237 let raw = decode_raw_document(
238 OpenApiSource::Path(PathBuf::from("test/OpenAPI/v3.1/non-oauth-scopes.json")),
239 include_str!("../../../../test/OpenAPI/v3.1/non-oauth-scopes.json"),
240 )
241 .unwrap();
242
243 let loaded = load_document_from_raw_with_options(raw, true).unwrap();
244
245 assert!(matches!(loaded, LoadedOpenApiDocument::OpenApi31Raw { .. }));
246 assert_eq!(
247 loaded.specification_version(),
248 OpenApiSpecificationVersion::OpenApi31
249 );
250 assert!(loaded.as_openapi31().is_none());
251 }
252
253 #[test]
254 fn loads_swagger_two_documents_with_a_raw_bridge() {
255 let raw = decode_raw_document(
256 OpenApiSource::Path(PathBuf::from("swagger.json")),
257 r#"{
258 "swagger": "2.0",
259 "info": { "title": "Example", "version": "1.0.0" },
260 "paths": {}
261 }"#,
262 )
263 .unwrap();
264
265 let loaded = load_document_from_raw(raw).unwrap();
266
267 assert!(matches!(loaded, LoadedOpenApiDocument::Swagger2 { .. }));
268 assert_eq!(
269 loaded.specification_version(),
270 OpenApiSpecificationVersion::Swagger2
271 );
272 assert!(loaded.as_openapi30().is_none());
273 assert!(loaded.as_openapi31().is_none());
274 }
275
276 #[test]
277 fn load_document_reads_and_parses_local_files() {
278 let file = TestFile::new(
279 "openapi.json",
280 r#"{
281 "openapi": "3.0.2",
282 "info": { "title": "Example", "version": "1.0.0" },
283 "paths": {}
284 }"#,
285 );
286
287 let loaded = load_document(file.path().to_str().unwrap()).unwrap();
288
289 assert!(matches!(loaded, LoadedOpenApiDocument::OpenApi30 { .. }));
290 assert_eq!(loaded.format(), crate::OpenApiContentFormat::Json);
291 }
292
293 fn unique_test_path(file_name: &str) -> PathBuf {
294 let artifact_id = TEST_ARTIFACT_ID.fetch_add(1, Ordering::Relaxed);
295 let directory = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
296 .join("target")
297 .join("test-data");
298
299 fs::create_dir_all(&directory).unwrap();
300
301 directory.join(format!(
302 "loader-{}-{}-{file_name}",
303 std::process::id(),
304 artifact_id
305 ))
306 }
307
308 struct TestFile {
309 path: PathBuf,
310 }
311
312 impl TestFile {
313 fn new(file_name: &str, content: &str) -> Self {
314 let path = unique_test_path(file_name);
315 fs::write(&path, content).unwrap();
316 Self { path }
317 }
318
319 fn path(&self) -> &Path {
320 &self.path
321 }
322 }
323
324 impl Drop for TestFile {
325 fn drop(&mut self) {
326 let _ = fs::remove_file(&self.path);
327 }
328 }
329}