httpgenerator_openapi/
version.rs1use std::fmt;
2
3use serde_json::Value;
4
5use crate::SpecificationVersionDetectionError;
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq)]
8pub enum OpenApiSpecificationVersion {
9 Swagger2,
10 OpenApi30,
11 OpenApi31,
12}
13
14impl fmt::Display for OpenApiSpecificationVersion {
15 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
16 match self {
17 Self::Swagger2 => write!(f, "Swagger 2.0"),
18 Self::OpenApi30 => write!(f, "OpenAPI 3.0.x"),
19 Self::OpenApi31 => write!(f, "OpenAPI 3.1.x"),
20 }
21 }
22}
23
24pub fn detect_specification_version(
25 value: &Value,
26) -> Result<OpenApiSpecificationVersion, SpecificationVersionDetectionError> {
27 if let Some(openapi_version) = value.get("openapi") {
28 return classify_openapi_version(openapi_version);
29 }
30
31 if let Some(swagger_version) = value.get("swagger") {
32 return classify_swagger_version(swagger_version);
33 }
34
35 Err(SpecificationVersionDetectionError::MissingVersionField)
36}
37
38fn classify_openapi_version(
39 value: &Value,
40) -> Result<OpenApiSpecificationVersion, SpecificationVersionDetectionError> {
41 let version = version_string(value, "openapi")?;
42 let (major, minor) = parse_major_minor(version).ok_or_else(|| {
43 SpecificationVersionDetectionError::UnsupportedVersion {
44 field: "openapi",
45 value: version.to_string(),
46 }
47 })?;
48
49 match (major, minor) {
50 (3, 0) => Ok(OpenApiSpecificationVersion::OpenApi30),
51 (3, 1) => Ok(OpenApiSpecificationVersion::OpenApi31),
52 _ => Err(SpecificationVersionDetectionError::UnsupportedVersion {
53 field: "openapi",
54 value: version.to_string(),
55 }),
56 }
57}
58
59fn classify_swagger_version(
60 value: &Value,
61) -> Result<OpenApiSpecificationVersion, SpecificationVersionDetectionError> {
62 let version = version_string(value, "swagger")?;
63 let (major, minor) = parse_major_minor(version).ok_or_else(|| {
64 SpecificationVersionDetectionError::UnsupportedVersion {
65 field: "swagger",
66 value: version.to_string(),
67 }
68 })?;
69
70 match (major, minor) {
71 (2, 0) => Ok(OpenApiSpecificationVersion::Swagger2),
72 _ => Err(SpecificationVersionDetectionError::UnsupportedVersion {
73 field: "swagger",
74 value: version.to_string(),
75 }),
76 }
77}
78
79fn version_string<'a>(
80 value: &'a Value,
81 field: &'static str,
82) -> Result<&'a str, SpecificationVersionDetectionError> {
83 value
84 .as_str()
85 .map(str::trim)
86 .filter(|value| !value.is_empty())
87 .ok_or(SpecificationVersionDetectionError::InvalidVersionFieldType { field })
88}
89
90fn parse_major_minor(version: &str) -> Option<(u64, u64)> {
91 let mut parts = version.split('.');
92 let major = parse_numeric_prefix(parts.next()?)?;
93 let minor = parse_numeric_prefix(parts.next()?)?;
94 Some((major, minor))
95}
96
97fn parse_numeric_prefix(component: &str) -> Option<u64> {
98 let digits = component
99 .trim()
100 .chars()
101 .take_while(|character| character.is_ascii_digit())
102 .collect::<String>();
103
104 (!digits.is_empty()).then(|| digits.parse().ok()).flatten()
105}
106
107#[cfg(test)]
108mod tests {
109 use std::path::PathBuf;
110
111 use serde_json::json;
112
113 use super::{OpenApiSpecificationVersion, detect_specification_version};
114 use crate::{OpenApiSource, SpecificationVersionDetectionError, decode_raw_document};
115
116 #[test]
117 fn detects_swagger_two_documents() {
118 let value = json!({
119 "swagger": "2.0",
120 "info": { "title": "Example" }
121 });
122
123 assert_eq!(
124 detect_specification_version(&value).unwrap(),
125 OpenApiSpecificationVersion::Swagger2
126 );
127 }
128
129 #[test]
130 fn detects_openapi_thirty_documents() {
131 let value = json!({
132 "openapi": "3.0.2",
133 "info": { "title": "Example" }
134 });
135
136 assert_eq!(
137 detect_specification_version(&value).unwrap(),
138 OpenApiSpecificationVersion::OpenApi30
139 );
140 }
141
142 #[test]
143 fn detects_openapi_thirty_one_documents() {
144 let value = json!({
145 "openapi": "3.1.0",
146 "info": { "title": "Example" }
147 });
148
149 assert_eq!(
150 detect_specification_version(&value).unwrap(),
151 OpenApiSpecificationVersion::OpenApi31
152 );
153 }
154
155 #[test]
156 fn reports_missing_version_fields() {
157 let value = json!({
158 "info": { "title": "Example" }
159 });
160
161 assert_eq!(
162 detect_specification_version(&value).unwrap_err(),
163 SpecificationVersionDetectionError::MissingVersionField
164 );
165 }
166
167 #[test]
168 fn reports_invalid_version_field_types() {
169 let value = json!({
170 "openapi": 3.1,
171 "info": { "title": "Example" }
172 });
173
174 assert_eq!(
175 detect_specification_version(&value).unwrap_err(),
176 SpecificationVersionDetectionError::InvalidVersionFieldType { field: "openapi" }
177 );
178 }
179
180 #[test]
181 fn reports_unsupported_versions() {
182 let value = json!({
183 "openapi": "3.2.0",
184 "info": { "title": "Example" }
185 });
186
187 assert_eq!(
188 detect_specification_version(&value).unwrap_err(),
189 SpecificationVersionDetectionError::UnsupportedVersion {
190 field: "openapi",
191 value: "3.2.0".to_string(),
192 }
193 );
194 }
195
196 #[test]
197 fn raw_documents_expose_detected_specification_versions() {
198 let document = decode_raw_document(
199 OpenApiSource::Path(PathBuf::from("openapi.json")),
200 r#"{"openapi":"3.0.2","info":{"title":"Example"}}"#,
201 )
202 .unwrap();
203
204 assert_eq!(
205 document.specification_version().unwrap(),
206 OpenApiSpecificationVersion::OpenApi30
207 );
208 }
209}