Skip to main content

httpgenerator_openapi/
version.rs

1use 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}