1#[cfg(feature = "async")]
18use roas::loader::{AsyncResourceFetcher, FetchFuture};
19use roas::loader::{LoaderError, ResourceFetcher};
20#[cfg(feature = "yaml")]
21use serde::de::Error as _;
22use serde_json::Value;
23use std::path::PathBuf;
24use url::Url;
25
26#[derive(Clone, Debug, Default)]
29pub struct FileFetcher;
30
31impl FileFetcher {
32 pub fn new() -> Self {
34 Self
35 }
36}
37
38#[cfg(feature = "async")]
44#[derive(Clone, Debug, Default)]
45pub struct AsyncFileFetcher;
46
47#[cfg(feature = "async")]
48impl AsyncFileFetcher {
49 pub fn new() -> Self {
51 Self
52 }
53}
54
55impl ResourceFetcher for FileFetcher {
56 fn fetch(&mut self, uri: &Url) -> Result<Value, LoaderError> {
57 check_scheme(uri)?;
58 let path = uri_to_path(uri)?;
59 let bytes =
60 std::fs::read(&path).map_err(|source| LoaderError::ReadFile { path, source })?;
61 parse_body(uri, &bytes)
62 }
63}
64
65#[cfg(feature = "async")]
66impl AsyncResourceFetcher for AsyncFileFetcher {
67 fn fetch<'a>(&'a mut self, uri: &'a Url) -> FetchFuture<'a> {
68 Box::pin(async move {
69 check_scheme(uri)?;
70 let path = uri_to_path(uri)?;
71 let bytes = tokio::fs::read(&path)
72 .await
73 .map_err(|source| LoaderError::ReadFile { path, source })?;
74 parse_body(uri, &bytes)
75 })
76 }
77}
78
79fn check_scheme(uri: &Url) -> Result<(), LoaderError> {
80 if uri.scheme() == "file" {
81 Ok(())
82 } else {
83 Err(LoaderError::UnsupportedFetcherUri(uri.as_str().to_string()))
84 }
85}
86
87fn uri_to_path(uri: &Url) -> Result<PathBuf, LoaderError> {
88 uri.to_file_path()
89 .map_err(|()| LoaderError::InvalidFileUri(uri.as_str().to_string()))
90}
91
92fn parse_body(uri: &Url, bytes: &[u8]) -> Result<Value, LoaderError> {
93 if is_yaml(uri) {
94 parse_yaml(uri, bytes)
95 } else {
96 serde_json::from_slice(bytes).map_err(|source| LoaderError::Parse {
97 uri: uri.as_str().to_string(),
98 source,
99 })
100 }
101}
102
103#[allow(unused_variables)]
108fn is_yaml(uri: &Url) -> bool {
109 #[cfg(feature = "yaml")]
110 {
111 let path = uri.path().to_ascii_lowercase();
112 path.ends_with(".yaml") || path.ends_with(".yml")
113 }
114 #[cfg(not(feature = "yaml"))]
115 {
116 false
117 }
118}
119
120#[cfg(feature = "yaml")]
121fn parse_yaml(uri: &Url, bytes: &[u8]) -> Result<Value, LoaderError> {
122 serde_yaml_ng::from_slice(bytes).map_err(|yaml_err| LoaderError::Parse {
123 uri: uri.as_str().to_string(),
124 source: serde_json::Error::custom(yaml_err.to_string()),
125 })
126}
127
128#[cfg(not(feature = "yaml"))]
129#[allow(dead_code)]
130fn parse_yaml(_uri: &Url, _bytes: &[u8]) -> Result<Value, LoaderError> {
131 unreachable!("parse_yaml is only reached when the `yaml` feature is enabled")
132}
133
134#[cfg(test)]
135mod tests {
136 use super::*;
137 use std::path::Path;
138
139 fn read_file_path(err: &LoaderError) -> Option<&Path> {
142 match err {
143 LoaderError::ReadFile { path, .. } => Some(path.as_path()),
144 _ => None,
145 }
146 }
147
148 #[test]
149 fn file_fetcher_default_constructs() {
150 let _: FileFetcher = Default::default();
151 let _ = FileFetcher::new();
152 }
153
154 #[cfg(feature = "async")]
155 #[test]
156 fn async_file_fetcher_default_constructs() {
157 let _: AsyncFileFetcher = Default::default();
158 let _ = AsyncFileFetcher::new();
159 }
160
161 #[test]
162 fn check_scheme_accepts_only_file() {
163 check_scheme(&Url::parse("file:///tmp/x.json").unwrap()).unwrap();
164 let err = check_scheme(&Url::parse("http://example.test/x.json").unwrap())
165 .expect_err("http must be rejected");
166 assert!(matches!(err, LoaderError::UnsupportedFetcherUri(s) if s.starts_with("http://")));
167 }
168
169 #[test]
170 fn read_file_path_extracts_path_from_read_file_variant() {
171 let err = LoaderError::ReadFile {
172 path: PathBuf::from("/nope"),
173 source: std::io::Error::other("missing"),
174 };
175 assert_eq!(read_file_path(&err), Some(Path::new("/nope")));
176 let parse_err = LoaderError::Parse {
177 uri: "x".into(),
178 source: serde_json::from_str::<Value>("@").unwrap_err(),
179 };
180 assert_eq!(read_file_path(&parse_err), None);
181 }
182}