httpgenerator_openapi/
source.rs1use 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}