httpgenerator-openapi 0.1.1

OpenAPI loading, inspection, and normalization for HTTP File Generator
Documentation
use std::fmt;

use serde_json::Value;

use crate::SpecificationVersionDetectionError;

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum OpenApiSpecificationVersion {
    Swagger2,
    OpenApi30,
    OpenApi31,
}

impl fmt::Display for OpenApiSpecificationVersion {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::Swagger2 => write!(f, "Swagger 2.0"),
            Self::OpenApi30 => write!(f, "OpenAPI 3.0.x"),
            Self::OpenApi31 => write!(f, "OpenAPI 3.1.x"),
        }
    }
}

pub fn detect_specification_version(
    value: &Value,
) -> Result<OpenApiSpecificationVersion, SpecificationVersionDetectionError> {
    if let Some(openapi_version) = value.get("openapi") {
        return classify_openapi_version(openapi_version);
    }

    if let Some(swagger_version) = value.get("swagger") {
        return classify_swagger_version(swagger_version);
    }

    Err(SpecificationVersionDetectionError::MissingVersionField)
}

fn classify_openapi_version(
    value: &Value,
) -> Result<OpenApiSpecificationVersion, SpecificationVersionDetectionError> {
    let version = version_string(value, "openapi")?;
    let (major, minor) = parse_major_minor(version).ok_or_else(|| {
        SpecificationVersionDetectionError::UnsupportedVersion {
            field: "openapi",
            value: version.to_string(),
        }
    })?;

    match (major, minor) {
        (3, 0) => Ok(OpenApiSpecificationVersion::OpenApi30),
        (3, 1) => Ok(OpenApiSpecificationVersion::OpenApi31),
        _ => Err(SpecificationVersionDetectionError::UnsupportedVersion {
            field: "openapi",
            value: version.to_string(),
        }),
    }
}

fn classify_swagger_version(
    value: &Value,
) -> Result<OpenApiSpecificationVersion, SpecificationVersionDetectionError> {
    let version = version_string(value, "swagger")?;
    let (major, minor) = parse_major_minor(version).ok_or_else(|| {
        SpecificationVersionDetectionError::UnsupportedVersion {
            field: "swagger",
            value: version.to_string(),
        }
    })?;

    match (major, minor) {
        (2, 0) => Ok(OpenApiSpecificationVersion::Swagger2),
        _ => Err(SpecificationVersionDetectionError::UnsupportedVersion {
            field: "swagger",
            value: version.to_string(),
        }),
    }
}

fn version_string<'a>(
    value: &'a Value,
    field: &'static str,
) -> Result<&'a str, SpecificationVersionDetectionError> {
    value
        .as_str()
        .map(str::trim)
        .filter(|value| !value.is_empty())
        .ok_or(SpecificationVersionDetectionError::InvalidVersionFieldType { field })
}

fn parse_major_minor(version: &str) -> Option<(u64, u64)> {
    let mut parts = version.split('.');
    let major = parse_numeric_prefix(parts.next()?)?;
    let minor = parse_numeric_prefix(parts.next()?)?;
    Some((major, minor))
}

fn parse_numeric_prefix(component: &str) -> Option<u64> {
    let digits = component
        .trim()
        .chars()
        .take_while(|character| character.is_ascii_digit())
        .collect::<String>();

    (!digits.is_empty()).then(|| digits.parse().ok()).flatten()
}

#[cfg(test)]
mod tests {
    use std::path::PathBuf;

    use serde_json::json;

    use super::{OpenApiSpecificationVersion, detect_specification_version};
    use crate::{OpenApiSource, SpecificationVersionDetectionError, decode_raw_document};

    #[test]
    fn detects_swagger_two_documents() {
        let value = json!({
            "swagger": "2.0",
            "info": { "title": "Example" }
        });

        assert_eq!(
            detect_specification_version(&value).unwrap(),
            OpenApiSpecificationVersion::Swagger2
        );
    }

    #[test]
    fn detects_openapi_thirty_documents() {
        let value = json!({
            "openapi": "3.0.2",
            "info": { "title": "Example" }
        });

        assert_eq!(
            detect_specification_version(&value).unwrap(),
            OpenApiSpecificationVersion::OpenApi30
        );
    }

    #[test]
    fn detects_openapi_thirty_one_documents() {
        let value = json!({
            "openapi": "3.1.0",
            "info": { "title": "Example" }
        });

        assert_eq!(
            detect_specification_version(&value).unwrap(),
            OpenApiSpecificationVersion::OpenApi31
        );
    }

    #[test]
    fn reports_missing_version_fields() {
        let value = json!({
            "info": { "title": "Example" }
        });

        assert_eq!(
            detect_specification_version(&value).unwrap_err(),
            SpecificationVersionDetectionError::MissingVersionField
        );
    }

    #[test]
    fn reports_invalid_version_field_types() {
        let value = json!({
            "openapi": 3.1,
            "info": { "title": "Example" }
        });

        assert_eq!(
            detect_specification_version(&value).unwrap_err(),
            SpecificationVersionDetectionError::InvalidVersionFieldType { field: "openapi" }
        );
    }

    #[test]
    fn reports_unsupported_versions() {
        let value = json!({
            "openapi": "3.2.0",
            "info": { "title": "Example" }
        });

        assert_eq!(
            detect_specification_version(&value).unwrap_err(),
            SpecificationVersionDetectionError::UnsupportedVersion {
                field: "openapi",
                value: "3.2.0".to_string(),
            }
        );
    }

    #[test]
    fn raw_documents_expose_detected_specification_versions() {
        let document = decode_raw_document(
            OpenApiSource::Path(PathBuf::from("openapi.json")),
            r#"{"openapi":"3.0.2","info":{"title":"Example"}}"#,
        )
        .unwrap();

        assert_eq!(
            document.specification_version().unwrap(),
            OpenApiSpecificationVersion::OpenApi30
        );
    }
}