httpgenerator-openapi 0.1.1

OpenAPI loading, inspection, and normalization for HTTP File Generator
Documentation
use std::{
    fmt,
    path::{Path, PathBuf},
};

use url::Url;

use crate::{OpenApiContentFormat, SourceClassificationError};

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum OpenApiSource {
    Path(PathBuf),
    Url(Url),
}

impl OpenApiSource {
    pub fn is_local_path(&self) -> bool {
        matches!(self, Self::Path(_))
    }

    pub fn is_url(&self) -> bool {
        matches!(self, Self::Url(_))
    }

    pub fn format_hint(&self) -> Option<OpenApiContentFormat> {
        match self {
            Self::Path(path) => OpenApiContentFormat::from_path(path),
            Self::Url(url) => OpenApiContentFormat::from_path(Path::new(url.path())),
        }
    }
}

impl fmt::Display for OpenApiSource {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::Path(path) => write!(f, "{}", path.display()),
            Self::Url(url) => write!(f, "{url}"),
        }
    }
}

pub fn classify_source(input: &str) -> Result<OpenApiSource, SourceClassificationError> {
    let trimmed = input.trim();

    if trimmed.is_empty() {
        return Err(SourceClassificationError::EmptyInput);
    }

    if let Some(scheme) = candidate_url_scheme(trimmed) {
        let normalized_scheme = scheme.to_ascii_lowercase();

        if normalized_scheme != "http" && normalized_scheme != "https" {
            return Err(SourceClassificationError::UnsupportedUrlScheme(
                normalized_scheme,
            ));
        }

        let url = Url::parse(trimmed).map_err(|error| SourceClassificationError::InvalidUrl {
            value: trimmed.to_string(),
            reason: error.to_string(),
        })?;

        return Ok(OpenApiSource::Url(url));
    }

    Ok(OpenApiSource::Path(PathBuf::from(trimmed)))
}

fn candidate_url_scheme(input: &str) -> Option<&str> {
    let (scheme, _) = input.split_once("://")?;
    let first = scheme.chars().next()?;

    if !first.is_ascii_alphabetic() {
        return None;
    }

    scheme
        .chars()
        .all(|character| character.is_ascii_alphanumeric() || matches!(character, '+' | '-' | '.'))
        .then_some(scheme)
}

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

    use super::{OpenApiSource, classify_source};
    use crate::{OpenApiContentFormat, SourceClassificationError};

    #[test]
    fn classifies_relative_file_paths_as_local_paths() {
        let source = classify_source("test\\OpenAPI\\v3.0\\petstore.json").unwrap();

        assert_eq!(
            source,
            OpenApiSource::Path(PathBuf::from("test\\OpenAPI\\v3.0\\petstore.json"))
        );
        assert!(source.is_local_path());
        assert_eq!(source.format_hint(), Some(OpenApiContentFormat::Json));
    }

    #[test]
    fn classifies_windows_absolute_paths_as_local_paths() {
        let source = classify_source("C:\\specs\\petstore.yaml").unwrap();

        assert_eq!(
            source,
            OpenApiSource::Path(PathBuf::from("C:\\specs\\petstore.yaml"))
        );
        assert!(source.is_local_path());
        assert_eq!(source.format_hint(), Some(OpenApiContentFormat::Yaml));
    }

    #[test]
    fn classifies_https_urls() {
        let source = classify_source("https://example.com/specs/petstore.yaml?download=1").unwrap();

        assert!(source.is_url());
        assert_eq!(source.format_hint(), Some(OpenApiContentFormat::Yaml));
        assert!(
            matches!(source, OpenApiSource::Url(url) if url.as_str() == "https://example.com/specs/petstore.yaml?download=1")
        );
    }

    #[test]
    fn rejects_unsupported_url_schemes() {
        let error = classify_source("ftp://example.com/openapi.json").unwrap_err();

        assert_eq!(
            error,
            SourceClassificationError::UnsupportedUrlScheme("ftp".to_string())
        );
    }

    #[test]
    fn rejects_invalid_http_urls() {
        let error = classify_source("https://").unwrap_err();

        assert!(matches!(
            error,
            SourceClassificationError::InvalidUrl { value, .. } if value == "https://"
        ));
    }

    #[test]
    fn rejects_empty_input() {
        let error = classify_source("   ").unwrap_err();

        assert_eq!(error, SourceClassificationError::EmptyInput);
    }
}