Skip to main content

httpgenerator_openapi/
source.rs

1use std::{
2    fmt,
3    path::{Path, PathBuf},
4};
5
6use url::Url;
7
8use crate::{OpenApiContentFormat, SourceClassificationError};
9
10#[derive(Debug, Clone, PartialEq, Eq)]
11pub enum OpenApiSource {
12    Path(PathBuf),
13    Url(Url),
14}
15
16impl OpenApiSource {
17    pub fn is_local_path(&self) -> bool {
18        matches!(self, Self::Path(_))
19    }
20
21    pub fn is_url(&self) -> bool {
22        matches!(self, Self::Url(_))
23    }
24
25    pub fn format_hint(&self) -> Option<OpenApiContentFormat> {
26        match self {
27            Self::Path(path) => OpenApiContentFormat::from_path(path),
28            Self::Url(url) => OpenApiContentFormat::from_path(Path::new(url.path())),
29        }
30    }
31}
32
33impl fmt::Display for OpenApiSource {
34    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
35        match self {
36            Self::Path(path) => write!(f, "{}", path.display()),
37            Self::Url(url) => write!(f, "{url}"),
38        }
39    }
40}
41
42pub fn classify_source(input: &str) -> Result<OpenApiSource, SourceClassificationError> {
43    let trimmed = input.trim();
44
45    if trimmed.is_empty() {
46        return Err(SourceClassificationError::EmptyInput);
47    }
48
49    if let Some(scheme) = candidate_url_scheme(trimmed) {
50        let normalized_scheme = scheme.to_ascii_lowercase();
51
52        if normalized_scheme != "http" && normalized_scheme != "https" {
53            return Err(SourceClassificationError::UnsupportedUrlScheme(
54                normalized_scheme,
55            ));
56        }
57
58        let url = Url::parse(trimmed).map_err(|error| SourceClassificationError::InvalidUrl {
59            value: trimmed.to_string(),
60            reason: error.to_string(),
61        })?;
62
63        return Ok(OpenApiSource::Url(url));
64    }
65
66    Ok(OpenApiSource::Path(PathBuf::from(trimmed)))
67}
68
69fn candidate_url_scheme(input: &str) -> Option<&str> {
70    let (scheme, _) = input.split_once("://")?;
71    let first = scheme.chars().next()?;
72
73    if !first.is_ascii_alphabetic() {
74        return None;
75    }
76
77    scheme
78        .chars()
79        .all(|character| character.is_ascii_alphanumeric() || matches!(character, '+' | '-' | '.'))
80        .then_some(scheme)
81}
82
83#[cfg(test)]
84mod tests {
85    use std::path::PathBuf;
86
87    use super::{OpenApiSource, classify_source};
88    use crate::{OpenApiContentFormat, SourceClassificationError};
89
90    #[test]
91    fn classifies_relative_file_paths_as_local_paths() {
92        let source = classify_source("test\\OpenAPI\\v3.0\\petstore.json").unwrap();
93
94        assert_eq!(
95            source,
96            OpenApiSource::Path(PathBuf::from("test\\OpenAPI\\v3.0\\petstore.json"))
97        );
98        assert!(source.is_local_path());
99        assert_eq!(source.format_hint(), Some(OpenApiContentFormat::Json));
100    }
101
102    #[test]
103    fn classifies_windows_absolute_paths_as_local_paths() {
104        let source = classify_source("C:\\specs\\petstore.yaml").unwrap();
105
106        assert_eq!(
107            source,
108            OpenApiSource::Path(PathBuf::from("C:\\specs\\petstore.yaml"))
109        );
110        assert!(source.is_local_path());
111        assert_eq!(source.format_hint(), Some(OpenApiContentFormat::Yaml));
112    }
113
114    #[test]
115    fn classifies_https_urls() {
116        let source = classify_source("https://example.com/specs/petstore.yaml?download=1").unwrap();
117
118        assert!(source.is_url());
119        assert_eq!(source.format_hint(), Some(OpenApiContentFormat::Yaml));
120        assert!(
121            matches!(source, OpenApiSource::Url(url) if url.as_str() == "https://example.com/specs/petstore.yaml?download=1")
122        );
123    }
124
125    #[test]
126    fn rejects_unsupported_url_schemes() {
127        let error = classify_source("ftp://example.com/openapi.json").unwrap_err();
128
129        assert_eq!(
130            error,
131            SourceClassificationError::UnsupportedUrlScheme("ftp".to_string())
132        );
133    }
134
135    #[test]
136    fn rejects_invalid_http_urls() {
137        let error = classify_source("https://").unwrap_err();
138
139        assert!(matches!(
140            error,
141            SourceClassificationError::InvalidUrl { value, .. } if value == "https://"
142        ));
143    }
144
145    #[test]
146    fn rejects_empty_input() {
147        let error = classify_source("   ").unwrap_err();
148
149        assert_eq!(error, SourceClassificationError::EmptyInput);
150    }
151}