use crate::config::cacher::BoxedCacher;
use crate::config::errors::ConfigError;
use crate::config::format::Format;
use serde::Deserialize;
use serde::{de::DeserializeOwned, Serialize};
use std::fs;
use std::path::PathBuf;
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(tag = "type", rename_all = "kebab-case")]
pub enum Source {
Code { code: String, format: Format },
File {
path: PathBuf,
format: Format,
required: bool,
},
Url { url: String, format: Format },
}
impl Source {
pub fn new(value: &str, parent_source: Option<&Source>) -> Result<Source, ConfigError> {
if is_url_like(value) {
return Source::url(value);
}
if is_file_like(value) {
let value = if let Some(stripped) = value.strip_prefix("file://") {
stripped
} else {
value
};
if parent_source.is_none() {
return Source::file(value, true);
}
if let Source::File {
path: parent_path, ..
} = parent_source.unwrap()
{
let mut path = PathBuf::from(value);
if !path.has_root() {
path = parent_path.parent().unwrap().join(path);
}
return Source::file(path, true);
} else {
return Err(ConfigError::ExtendsFromParentFileOnly);
}
}
Err(ConfigError::ExtendsFromNoCode)
}
pub fn code<T: TryInto<String>>(code: T, format: Format) -> Result<Source, ConfigError> {
let code: String = code.try_into().map_err(|_| ConfigError::InvalidCode)?;
Ok(Source::Code { code, format })
}
pub fn file<T: TryInto<PathBuf>>(path: T, required: bool) -> Result<Source, ConfigError> {
let path: PathBuf = path.try_into().map_err(|_| ConfigError::InvalidFile)?;
Ok(Source::File {
format: Format::detect(path.to_str().unwrap_or_default())?,
path,
required,
})
}
pub fn url<T: TryInto<String>>(url: T) -> Result<Source, ConfigError> {
let url: String = url.try_into().map_err(|_| ConfigError::InvalidUrl)?;
Ok(Source::Url {
format: Format::detect(&url)?,
url,
})
}
pub fn parse<D>(
&self,
location: &str,
cacher: &mut BoxedCacher,
help: Option<&str>,
) -> Result<D, ConfigError>
where
D: DeserializeOwned,
{
let handle_error = |error: crate::config::ParserError| ConfigError::Parser {
config: location.to_owned(),
error,
help: help.map(|h| h.to_owned()),
};
match self {
Source::Code { code, format } => format
.parse(code.to_owned(), location)
.map_err(handle_error),
Source::File {
path,
format,
required,
} => {
let content = if path.exists() {
fs::read_to_string(path).map_err(|error| ConfigError::ReadFileFailed {
path: path.to_path_buf(),
error,
})?
} else {
if *required {
return Err(ConfigError::MissingFile(path.to_path_buf()));
}
"".into()
};
format.parse(content, location).map_err(handle_error)
}
Source::Url { url, format } => {
if !is_secure_url(url) {
return Err(ConfigError::HttpsOnly(url.to_owned()));
}
#[cfg(feature = "url")]
{
let handle_reqwest_error = |error: reqwest::Error| ConfigError::ReadUrlFailed {
url: url.to_owned(),
error,
};
let content = if let Some(cache) = cacher.read(url)? {
cache
} else {
let body = reqwest::blocking::get(url)
.map_err(handle_reqwest_error)?
.text()
.map_err(handle_reqwest_error)?;
cacher.write(url, &body)?;
body
};
format.parse(content, location).map_err(handle_error)
}
#[cfg(not(feature = "url"))]
{
panic!("Parsing a URL requires the `url` feature!");
}
}
}
}
pub fn as_str(&self) -> &str {
match self {
Source::Code { .. } => "<code>",
Source::File { path, .. } => path.to_str().unwrap_or_default(),
Source::Url { url, .. } => url,
}
}
}
pub fn is_source_format(value: &str) -> bool {
value.ends_with(".json")
|| value.ends_with(".toml")
|| value.ends_with(".yaml")
|| value.ends_with(".yml")
}
pub fn is_file_like(value: &str) -> bool {
value.starts_with("file://")
|| value.starts_with('/')
|| value.starts_with('\\')
|| value.starts_with('.')
|| value.contains('/')
|| value.contains('\\')
|| value.contains('.')
}
pub fn is_url_like(value: &str) -> bool {
value.starts_with("https://") || value.starts_with("http://") || value.starts_with("www")
}
pub fn is_secure_url(value: &str) -> bool {
if value.contains("127.0.0.1") || value.contains("//localhost") {
return true;
}
value.starts_with("https://")
}