1use std::path::{Path, PathBuf};
10use std::sync::Arc;
11
12use query_flow::asset_key;
13use url::Url;
14
15use super::error::EureQueryError;
16
17#[asset_key(asset = TextFileContent)]
19pub enum TextFile {
20 Local(Arc<PathBuf>),
22 Remote(Url),
24}
25
26impl TextFile {
27 pub fn from_path(path: PathBuf) -> Self {
29 Self::Local(Arc::new(path))
30 }
31
32 pub fn from_url(url: Url) -> Self {
34 Self::Remote(url)
35 }
36
37 pub fn parse(s: &str) -> Result<Self, EureQueryError> {
39 if s.starts_with("https://") {
40 Url::parse(s)
41 .map(Self::from_url)
42 .map_err(|e| EureQueryError::InvalidUrl {
43 url: s.to_string(),
44 reason: e.to_string(),
45 })
46 } else {
47 Ok(Self::from_path(PathBuf::from(s)))
48 }
49 }
50
51 pub fn resolve(target: &str, base_dir: &Path) -> Result<Self, EureQueryError> {
56 if target.starts_with("https://") {
57 Self::parse(target)
58 } else {
59 Ok(Self::from_path(base_dir.join(target)))
60 }
61 }
62
63 pub fn new(path: Arc<PathBuf>) -> Self {
65 Self::Local(path)
66 }
67
68 pub fn as_local_path(&self) -> Option<&Path> {
70 match self {
71 Self::Local(p) => Some(p),
72 Self::Remote(_) => None,
73 }
74 }
75
76 pub fn as_url(&self) -> Option<&Url> {
78 match self {
79 Self::Local(_) => None,
80 Self::Remote(url) => Some(url),
81 }
82 }
83
84 pub fn is_local(&self) -> bool {
86 matches!(self, Self::Local(_))
87 }
88
89 pub fn ends_with(&self, suffix: &str) -> bool {
91 match self {
92 Self::Local(path) => path
93 .file_name()
94 .is_some_and(|name| name.to_string_lossy().ends_with(suffix)),
95 Self::Remote(url) => url.path().ends_with(suffix),
96 }
97 }
98}
99
100impl std::fmt::Display for TextFile {
101 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
102 match self {
103 Self::Local(path) => write!(f, "{}", path.display()),
104 Self::Remote(url) => write!(f, "{}", url),
105 }
106 }
107}
108
109#[derive(Clone, PartialEq, Debug)]
111pub struct TextFileContent(pub String);
112
113impl TextFileContent {
114 pub fn get(&self) -> &str {
115 &self.0
116 }
117}
118
119#[asset_key(asset = Workspace)]
121pub struct WorkspaceId(pub String);
122
123#[derive(Clone, PartialEq)]
125pub struct Workspace {
126 pub path: PathBuf,
127 pub config_path: PathBuf,
128}
129
130#[asset_key(asset = GlobResult)]
138pub struct Glob {
139 pub base_dir: PathBuf,
140 pub pattern: String,
141}
142
143impl Glob {
144 pub fn new(base_dir: impl Into<PathBuf>, pattern: impl Into<String>) -> Self {
145 Self {
146 base_dir: base_dir.into(),
147 pattern: pattern.into(),
148 }
149 }
150
151 pub fn full_pattern(&self) -> PathBuf {
153 self.base_dir.join(&self.pattern)
154 }
155}
156
157#[derive(Clone, PartialEq, Debug)]
159pub struct GlobResult(pub Vec<TextFile>);
160
161#[asset_key(asset = OpenDocumentsList)]
166pub struct OpenDocuments;
167
168#[derive(Clone, PartialEq, Debug)]
170pub struct OpenDocumentsList(pub Vec<TextFile>);
171
172#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
174pub enum DecorStyle {
175 #[default]
177 Unicode,
178 Ascii,
180}
181
182#[asset_key(asset = DecorStyle)]
187pub struct DecorStyleKey;
188
189#[cfg(test)]
190mod tests {
191 use super::*;
192
193 mod text_file_parse {
194 use super::*;
195
196 #[test]
197 fn parses_https_url() {
198 let file = TextFile::parse("https://example.com/schema.eure").unwrap();
199 assert!(file.as_url().is_some());
200 assert!(file.as_local_path().is_none());
201 assert_eq!(
202 file.as_url().unwrap().as_str(),
203 "https://example.com/schema.eure"
204 );
205 }
206
207 #[test]
208 fn parses_local_path() {
209 let file = TextFile::parse("/path/to/file.eure").unwrap();
210 assert!(file.as_local_path().is_some());
211 assert!(file.as_url().is_none());
212 assert_eq!(
213 file.as_local_path().unwrap(),
214 Path::new("/path/to/file.eure")
215 );
216 }
217
218 #[test]
219 fn parses_relative_path() {
220 let file = TextFile::parse("relative/path.eure").unwrap();
221 assert!(file.as_local_path().is_some());
222 assert_eq!(
223 file.as_local_path().unwrap(),
224 Path::new("relative/path.eure")
225 );
226 }
227
228 #[test]
229 fn http_without_s_is_local_path() {
230 let file = TextFile::parse("http://example.com").unwrap();
232 assert!(file.as_local_path().is_some());
233 }
234
235 #[test]
236 fn invalid_url_returns_error() {
237 let result = TextFile::parse("https://");
239 assert!(result.is_err());
240
241 let result = TextFile::parse("https://[invalid");
243 assert!(result.is_err());
244 }
245 }
246
247 mod text_file_ends_with {
248 use super::*;
249
250 #[test]
251 fn local_file_ends_with_extension() {
252 let file = TextFile::from_path(PathBuf::from("/path/to/file.schema.eure"));
253 assert!(file.ends_with(".schema.eure"));
254 assert!(file.ends_with(".eure"));
255 assert!(!file.ends_with(".json"));
256 }
257
258 #[test]
259 fn local_file_ends_with_filename() {
260 let file = TextFile::from_path(PathBuf::from("/path/to/config.eure"));
261 assert!(file.ends_with("config.eure"));
262 assert!(!file.ends_with("other.eure"));
263 }
264
265 #[test]
266 fn remote_url_ends_with_extension() {
267 let file = TextFile::parse("https://example.com/schemas/user.schema.eure").unwrap();
268 assert!(file.ends_with(".schema.eure"));
269 assert!(file.ends_with(".eure"));
270 assert!(!file.ends_with(".json"));
271 }
272
273 #[test]
274 fn remote_url_ignores_query_params() {
275 let file = TextFile::parse("https://example.com/file.eure?version=1").unwrap();
278 assert!(file.ends_with(".eure"));
279 assert!(!file.ends_with("?version=1"));
280 }
281
282 #[test]
283 fn remote_url_ignores_fragment() {
284 let file = TextFile::parse("https://example.com/file.eure#section").unwrap();
285 assert!(file.ends_with(".eure"));
286 assert!(!file.ends_with("#section"));
287 }
288 }
289}