#[cfg(feature = "async")]
use roas::loader::{AsyncResourceFetcher, FetchFuture};
use roas::loader::{LoaderError, ResourceFetcher};
#[cfg(feature = "yaml")]
use serde::de::Error as _;
use serde_json::Value;
use std::path::PathBuf;
use url::Url;
#[derive(Clone, Debug, Default)]
pub struct FileFetcher;
impl FileFetcher {
pub fn new() -> Self {
Self
}
}
#[cfg(feature = "async")]
#[derive(Clone, Debug, Default)]
pub struct AsyncFileFetcher;
#[cfg(feature = "async")]
impl AsyncFileFetcher {
pub fn new() -> Self {
Self
}
}
impl ResourceFetcher for FileFetcher {
fn fetch(&mut self, uri: &Url) -> Result<Value, LoaderError> {
check_scheme(uri)?;
let path = uri_to_path(uri)?;
let bytes =
std::fs::read(&path).map_err(|source| LoaderError::ReadFile { path, source })?;
parse_body(uri, &bytes)
}
}
#[cfg(feature = "async")]
impl AsyncResourceFetcher for AsyncFileFetcher {
fn fetch<'a>(&'a mut self, uri: &'a Url) -> FetchFuture<'a> {
Box::pin(async move {
check_scheme(uri)?;
let path = uri_to_path(uri)?;
let bytes = tokio::fs::read(&path)
.await
.map_err(|source| LoaderError::ReadFile { path, source })?;
parse_body(uri, &bytes)
})
}
}
fn check_scheme(uri: &Url) -> Result<(), LoaderError> {
if uri.scheme() == "file" {
Ok(())
} else {
Err(LoaderError::UnsupportedFetcherUri(uri.as_str().to_string()))
}
}
fn uri_to_path(uri: &Url) -> Result<PathBuf, LoaderError> {
uri.to_file_path()
.map_err(|()| LoaderError::InvalidFileUri(uri.as_str().to_string()))
}
fn parse_body(uri: &Url, bytes: &[u8]) -> Result<Value, LoaderError> {
if is_yaml(uri) {
parse_yaml(uri, bytes)
} else {
serde_json::from_slice(bytes).map_err(|source| LoaderError::Parse {
uri: uri.as_str().to_string(),
source,
})
}
}
#[allow(unused_variables)]
fn is_yaml(uri: &Url) -> bool {
#[cfg(feature = "yaml")]
{
let path = uri.path().to_ascii_lowercase();
path.ends_with(".yaml") || path.ends_with(".yml")
}
#[cfg(not(feature = "yaml"))]
{
false
}
}
#[cfg(feature = "yaml")]
fn parse_yaml(uri: &Url, bytes: &[u8]) -> Result<Value, LoaderError> {
serde_yaml_ng::from_slice(bytes).map_err(|yaml_err| LoaderError::Parse {
uri: uri.as_str().to_string(),
source: serde_json::Error::custom(yaml_err.to_string()),
})
}
#[cfg(not(feature = "yaml"))]
#[allow(dead_code)]
fn parse_yaml(_uri: &Url, _bytes: &[u8]) -> Result<Value, LoaderError> {
unreachable!("parse_yaml is only reached when the `yaml` feature is enabled")
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::Path;
fn read_file_path(err: &LoaderError) -> Option<&Path> {
match err {
LoaderError::ReadFile { path, .. } => Some(path.as_path()),
_ => None,
}
}
#[test]
fn file_fetcher_default_constructs() {
let _: FileFetcher = Default::default();
let _ = FileFetcher::new();
}
#[cfg(feature = "async")]
#[test]
fn async_file_fetcher_default_constructs() {
let _: AsyncFileFetcher = Default::default();
let _ = AsyncFileFetcher::new();
}
#[test]
fn check_scheme_accepts_only_file() {
check_scheme(&Url::parse("file:///tmp/x.json").unwrap()).unwrap();
let err = check_scheme(&Url::parse("http://example.test/x.json").unwrap())
.expect_err("http must be rejected");
assert!(matches!(err, LoaderError::UnsupportedFetcherUri(s) if s.starts_with("http://")));
}
#[test]
fn read_file_path_extracts_path_from_read_file_variant() {
let err = LoaderError::ReadFile {
path: PathBuf::from("/nope"),
source: std::io::Error::other("missing"),
};
assert_eq!(read_file_path(&err), Some(Path::new("/nope")));
let parse_err = LoaderError::Parse {
uri: "x".into(),
source: serde_json::from_str::<Value>("@").unwrap_err(),
};
assert_eq!(read_file_path(&parse_err), None);
}
}