mrapids 0.1.31

Your OpenAPI, but executable
Documentation
/// OpenAPI specification version detection
use crate::core::api::ApiError;
use anyhow::Result;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::fmt;

/// Supported OpenAPI/Swagger specification versions
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum SpecVersion {
    /// Swagger 2.0
    Swagger2_0,
    /// OpenAPI 3.0.x (stores exact version)
    OpenAPI3_0(String),
    /// OpenAPI 3.1.x (stores exact version)
    OpenAPI3_1(String),
}

impl fmt::Display for SpecVersion {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            SpecVersion::Swagger2_0 => write!(f, "Swagger 2.0"),
            SpecVersion::OpenAPI3_0(v) => write!(f, "OpenAPI {}", v),
            SpecVersion::OpenAPI3_1(v) => write!(f, "OpenAPI {}", v),
        }
    }
}

impl SpecVersion {}

/// Detect the specification version from the content
pub fn detect_spec_version(content: &str) -> Result<SpecVersion> {
    // Try to parse as JSON first, then YAML
    let value: Value = serde_json::from_str(content)
        .or_else(|_| serde_yaml::from_str(content))
        .map_err(|e| ApiError::ValidationError(format!("Failed to parse spec: {}", e)))?;

    detect_version_from_value(&value)
}

/// Detect version from a parsed JSON value
pub fn detect_version_from_value(value: &Value) -> Result<SpecVersion> {
    // Check for Swagger 2.0
    if let Some(swagger_version) = value.get("swagger").and_then(|v| v.as_str()) {
        if swagger_version == "2.0" {
            return Ok(SpecVersion::Swagger2_0);
        } else {
            return Err(ApiError::ValidationError(format!(
                "Unsupported Swagger version: {}. Only 2.0 is supported.",
                swagger_version
            ))
            .into());
        }
    }

    // Check for OpenAPI 3.x
    if let Some(openapi_version) = value.get("openapi").and_then(|v| v.as_str()) {
        // Parse version string
        let parts: Vec<&str> = openapi_version.split('.').collect();
        if parts.len() < 2 {
            return Err(ApiError::ValidationError(format!(
                "Invalid OpenAPI version format: {}",
                openapi_version
            ))
            .into());
        }

        match parts[0] {
            "3" => {
                match parts[1] {
                    "0" => {
                        // Accept any 3.0.x version (they are all backward compatible)
                        // Validate it follows the 3.0.x pattern
                        if parts.len() == 3 && parts[2].chars().all(|c| c.is_ascii_digit()) {
                            Ok(SpecVersion::OpenAPI3_0(openapi_version.to_string()))
                        } else {
                            Err(ApiError::ValidationError(format!(
                                "Invalid OpenAPI 3.0.x version format: {}. Expected format: 3.0.X where X is a number",
                                openapi_version
                            )).into())
                        }
                    }
                    "1" => {
                        // OpenAPI 3.1.x - validate format like 3.0.x
                        if parts.len() == 3 && parts[2].chars().all(|c| c.is_ascii_digit()) {
                            Ok(SpecVersion::OpenAPI3_1(openapi_version.to_string()))
                        } else {
                            Err(ApiError::ValidationError(format!(
                                "Invalid OpenAPI 3.1.x version format: {}. Expected format: 3.1.X where X is a number",
                                openapi_version
                            )).into())
                        }
                    }
                    _ => Err(ApiError::ValidationError(format!(
                        "Unsupported OpenAPI version: {}. Supported: 3.0.x and 3.1.x",
                        openapi_version
                    ))
                    .into()),
                }
            }
            _ => Err(ApiError::ValidationError(format!(
                "Unsupported OpenAPI major version: {}. Only version 3.x is supported.",
                openapi_version
            ))
            .into()),
        }
    } else {
        Err(ApiError::ValidationError(
            "No 'swagger' or 'openapi' field found. This doesn't appear to be a valid OpenAPI/Swagger specification.".to_string()
        ).into())
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use serde_json::json;

    #[test]
    fn test_detect_swagger_2_0() {
        let spec = json!({
            "swagger": "2.0",
            "info": {
                "title": "Test API",
                "version": "1.0.0"
            }
        });

        let version = detect_version_from_value(&spec).unwrap();
        assert_eq!(version, SpecVersion::Swagger2_0);
    }

    #[test]
    fn test_detect_openapi_3_0() {
        let spec = json!({
            "openapi": "3.0.2",
            "info": {
                "title": "Test API",
                "version": "1.0.0"
            }
        });

        let version = detect_version_from_value(&spec).unwrap();
        assert_eq!(version, SpecVersion::OpenAPI3_0("3.0.2".to_string()));
    }

    #[test]
    fn test_detect_openapi_3_0_4() {
        let spec = json!({
            "openapi": "3.0.4",
            "info": {
                "title": "Test API",
                "version": "1.0.0"
            }
        });

        let version = detect_version_from_value(&spec).unwrap();
        assert_eq!(version, SpecVersion::OpenAPI3_0("3.0.4".to_string()));
    }

    #[test]
    fn test_detect_openapi_3_0_10() {
        // Test with double-digit patch version
        let spec = json!({
            "openapi": "3.0.10",
            "info": {
                "title": "Test API",
                "version": "1.0.0"
            }
        });

        let version = detect_version_from_value(&spec).unwrap();
        assert_eq!(version, SpecVersion::OpenAPI3_0("3.0.10".to_string()));
    }

    #[test]
    fn test_detect_openapi_3_1() {
        let spec = json!({
            "openapi": "3.1.0",
            "info": {
                "title": "Test API",
                "version": "1.0.0"
            }
        });

        let version = detect_version_from_value(&spec).unwrap();
        assert_eq!(version, SpecVersion::OpenAPI3_1("3.1.0".to_string()));
    }

    #[test]
    fn test_detect_openapi_3_1_1() {
        let spec = json!({
            "openapi": "3.1.1",
            "info": {
                "title": "Test API",
                "version": "1.0.0"
            }
        });

        let version = detect_version_from_value(&spec).unwrap();
        assert_eq!(version, SpecVersion::OpenAPI3_1("3.1.1".to_string()));
    }

    #[test]
    fn test_reject_invalid_3_0_format() {
        // Missing patch version
        let spec = json!({"openapi": "3.0"});
        assert!(detect_version_from_value(&spec).is_err());

        // Non-numeric patch
        let spec = json!({"openapi": "3.0.x"});
        assert!(detect_version_from_value(&spec).is_err());

        // Too many parts
        let spec = json!({"openapi": "3.0.1.2"});
        assert!(detect_version_from_value(&spec).is_err());
    }

    #[test]
    fn test_reject_invalid_3_1_format() {
        // Missing patch version
        let spec = json!({"openapi": "3.1"});
        assert!(detect_version_from_value(&spec).is_err());

        // Non-numeric patch
        let spec = json!({"openapi": "3.1.x"});
        assert!(detect_version_from_value(&spec).is_err());

        // Invalid characters
        let spec = json!({"openapi": "3.1.0-beta"});
        assert!(detect_version_from_value(&spec).is_err());
    }

    #[test]
    fn test_invalid_version() {
        let spec = json!({
            "openapi": "2.0",
            "info": {
                "title": "Test API",
                "version": "1.0.0"
            }
        });

        let result = detect_version_from_value(&spec);
        assert!(result.is_err());
    }

    #[test]
    fn test_missing_version() {
        let spec = json!({
            "info": {
                "title": "Test API",
                "version": "1.0.0"
            }
        });

        let result = detect_version_from_value(&spec);
        assert!(result.is_err());
    }
}