use std::marker::PhantomData;
use std::path::{Path, PathBuf};
use serde::de::{self, DeserializeOwned};
use crate::value::{Map, Dict};
use crate::{Error, Profile, Provider, Metadata};
use crate::error::Kind;
#[derive(Debug, Clone)]
pub struct Data<F: Format> {
source: Source,
pub profile: Option<Profile>,
_format: PhantomData<F>,
}
#[derive(Debug, Clone)]
enum Source {
File { path: PathBuf, required: bool, search: bool, },
String(String),
}
impl<F: Format> Data<F> {
fn new(profile: Option<Profile>, source: Source) -> Self {
Data { source, profile, _format: PhantomData }
}
pub fn file<P: AsRef<Path>>(path: P) -> Self {
Data::new(Some(Profile::Default), Source::File {
path: path.as_ref().to_path_buf(),
required: false,
search: true,
})
}
pub fn string(string: &str) -> Self {
Data::new(Some(Profile::Default), Source::String(string.into()))
}
#[doc(hidden)]
#[deprecated(since = "0.10.20", note = "use `::file(path).search(false)` instead")]
pub fn file_exact<P: AsRef<Path>>(path: P) -> Self {
Data::file(path.as_ref()).search(false)
}
pub fn nested(mut self) -> Self {
self.profile = None;
self
}
pub fn required(mut self, yes: bool) -> Self {
if let Source::File { required, .. } = &mut self.source {
*required = yes;
}
self
}
pub fn search(mut self, enabled: bool) -> Self {
if let Source::File { search, .. } = &mut self.source {
*search = enabled;
}
self
}
pub fn profile<P: Into<Profile>>(mut self, profile: P) -> Self {
self.profile = Some(profile.into());
self
}
fn resolve(path: &Path, search: bool) -> Option<PathBuf> {
if path.is_absolute() || !search {
return path.is_file().then(|| path.to_path_buf());
}
let cwd = std::env::current_dir().ok()?;
let mut cwd = cwd.as_path();
loop {
let file_path = cwd.join(path);
if file_path.is_file() {
return Some(file_path.into());
}
cwd = cwd.parent()?;
}
}
}
impl<F: Format> Provider for Data<F> {
fn metadata(&self) -> Metadata {
use Source::*;
match &self.source {
String(_) => Metadata::named(format!("{} source string", F::NAME)),
File { path, search, required: _ } => {
let path = Self::resolve(path, *search).unwrap_or_else(|| path.clone());
Metadata::from(format!("{} file", F::NAME), path.as_path())
}
}
}
fn data(&self) -> Result<Map<Profile, Dict>, Error> {
use Source as S;
let map: Result<Map<Profile, Dict>, _> = match (&self.source, &self.profile) {
(S::File { path, required, search }, profile) => {
match Self::resolve(path, *search) {
Some(path) => match profile {
Some(prof) => F::from_path(&path).map(|v| prof.collect(v)),
None => F::from_path(&path),
},
None if !required => Ok(Map::new()),
None => {
let msg = format!("required file `{}` not found", path.display());
return Err(Kind::Message(msg).into());
}
}
},
(S::String(s), None) => F::from_str(s),
(S::String(s), Some(prof)) => F::from_str(s).map(|v| prof.collect(v)),
};
Ok(map.map_err(|e| e.to_string())?)
}
}
pub trait Format: Sized {
type Error: de::Error;
const NAME: &'static str;
fn file<P: AsRef<Path>>(path: P) -> Data<Self> {
Data::file(path)
}
fn string(string: &str) -> Data<Self> {
Data::string(string)
}
#[doc(hidden)]
#[deprecated(since = "0.10.20", note = "use `::file(path).search(false)` instead")]
fn file_exact<P: AsRef<Path>>(path: P) -> Data<Self> {
Data::file(path.as_ref()).search(false)
}
fn from_str<'de, T: DeserializeOwned>(string: &'de str) -> Result<T, Self::Error>;
fn from_path<T: DeserializeOwned>(path: &Path) -> Result<T, Self::Error> {
let source = std::fs::read_to_string(path).map_err(de::Error::custom)?;
Self::from_str(&source)
}
}
#[allow(unused_macros)]
macro_rules! impl_format {
($name:ident $NAME:literal/$string:literal: $func:expr, $E:ty, $doc:expr) => (
#[cfg(feature = $string)]
#[cfg_attr(nightly, doc(cfg(feature = $string)))]
#[doc = $doc]
pub struct $name;
#[cfg(feature = $string)]
impl Format for $name {
type Error = $E;
const NAME: &'static str = $NAME;
fn from_str<'de, T: DeserializeOwned>(s: &'de str) -> Result<T, $E> {
$func(s)
}
}
);
($name:ident $NAME:literal/$string:literal: $func:expr, $E:ty) => (
impl_format!($name $NAME/$string: $func, $E, concat!(
"A ", $NAME, " [`Format`] [`Data`] provider.",
"\n\n",
"Static constructor methods on `", stringify!($name), "` return a
[`Data`] value with a generic marker of [`", stringify!($name), "`].
Thus, further use occurs via methods on [`Data`].",
"\n```\n",
"use figment2::providers::{Format, ", stringify!($name), "};",
"\n\n// Source directly from a source string...",
"\nlet provider = ", stringify!($name), r#"::string("source-string");"#,
"\n\n// Or read from a file on disk.",
"\nlet provider = ", stringify!($name), r#"::file("path-to-file");"#,
"\n\n// Or configured as nested (via Data::nested()):",
"\nlet provider = ", stringify!($name), r#"::file("path-to-file").nested();"#,
"\n```",
"\n\nSee also [`", stringify!($func), "`] for parsing details."
));
)
}
#[cfg(feature = "yaml")]
#[cfg_attr(nightly, doc(cfg(feature = "yaml")))]
impl YamlExtended {
pub fn from_str<'de, T: DeserializeOwned>(s: &'de str) -> serde_norway::Result<T> {
let mut value: serde_norway::Value = serde_norway::from_str(s)?;
value.apply_merge()?;
T::deserialize(value)
}
}
impl_format!(Toml "TOML"/"toml": toml_edit::de::from_str, toml_edit::de::Error);
impl_format!(Yaml "YAML"/"yaml": serde_norway::from_str, serde_norway::Error);
impl_format!(Json "JSON"/"json": serde_json::from_str, serde_json::error::Error);
impl_format!(YamlExtended "YAML Extended"/"yaml": YamlExtended::from_str, serde_norway::Error);