mirror of
https://github.com/OMGeeky/confique.git
synced 2026-01-03 01:56:32 +01:00
Add Source trait and implement it for files (TOML and YAML for now)
This commit is contained in:
23
Cargo.toml
23
Cargo.toml
@@ -5,16 +5,21 @@ authors = ["Lukas Kalbertodt <lukas.kalbertodt@gmail.com>"]
|
||||
edition = "2018"
|
||||
|
||||
|
||||
[dependencies]
|
||||
confique-macro = { path = "macro" }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
log = { version = "0.4", features = ["serde"], optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
log = { version = "0.4", features = ["serde"] }
|
||||
|
||||
|
||||
[features]
|
||||
default = ["toml", "yaml"]
|
||||
yaml = ["serde_yaml"]
|
||||
|
||||
# This is used to enable the `example` module. This is only useful to generate
|
||||
# the docs for this library!
|
||||
doc-example = ["log"]
|
||||
|
||||
[dependencies]
|
||||
confique-macro = { path = "macro" }
|
||||
log = { version = "0.4", features = ["serde"], optional = true }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_yaml = { version = "0.8", optional = true }
|
||||
toml = { version = "0.5", optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
log = { version = "0.4", features = ["serde"] }
|
||||
anyhow = "1"
|
||||
|
||||
2
examples/files/etc/simple.yaml
Normal file
2
examples/files/etc/simple.yaml
Normal file
@@ -0,0 +1,2 @@
|
||||
http:
|
||||
port: 4321
|
||||
2
examples/files/simple.toml
Normal file
2
examples/files/simple.toml
Normal file
@@ -0,0 +1,2 @@
|
||||
[cat]
|
||||
foo = "yes"
|
||||
@@ -1,5 +1,5 @@
|
||||
use std::net::IpAddr;
|
||||
use confique::{Config, Partial};
|
||||
use std::{net::IpAddr, path::Path};
|
||||
use confique::Config;
|
||||
|
||||
#[derive(Debug, Config)]
|
||||
struct Conf {
|
||||
@@ -26,6 +26,13 @@ struct Cat {
|
||||
}
|
||||
|
||||
|
||||
fn main() {
|
||||
println!("{:#?}", Conf::from_partial(<Conf as Config>::Partial::default_values()));
|
||||
fn main() -> Result<(), anyhow::Error> {
|
||||
let r = Conf::from_sources(&[
|
||||
&Path::new("examples/files/simple.toml"),
|
||||
&Path::new("examples/files/etc/simple.yaml"),
|
||||
])?;
|
||||
|
||||
println!("{:#?}", r);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -26,17 +26,12 @@ fn gen_config_impl(input: &ir::Input) -> TokenStream {
|
||||
if !f.is_leaf() {
|
||||
quote! {
|
||||
confique::Config::from_partial(partial.#field_name).map_err(|e| {
|
||||
match e {
|
||||
confique::Error::MissingValue(path) => {
|
||||
confique::Error::MissingValue(format!("{}.{}", #path, path))
|
||||
}
|
||||
e => e,
|
||||
}
|
||||
confique::internal::prepend_missing_value_error(e, #path)
|
||||
})?
|
||||
}
|
||||
} else if unwrap_option(&f.ty).is_none() {
|
||||
quote! {
|
||||
partial.#field_name.ok_or(confique::Error::MissingValue(#path.into()))?
|
||||
partial.#field_name.ok_or(confique::internal::missing_value_error(#path.into()))?
|
||||
}
|
||||
} else {
|
||||
quote! { partial.#field_name }
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
//! intended to be used directly. None of this is covered by semver! Do not use
|
||||
//! any of this directly.
|
||||
|
||||
use crate::{Error, ErrorInner};
|
||||
|
||||
pub fn deserialize_default<I, O>(src: I) -> Result<O, serde::de::value::Error>
|
||||
where
|
||||
I: for<'de> serde::de::IntoDeserializer<'de>,
|
||||
@@ -9,3 +11,16 @@ where
|
||||
{
|
||||
O::deserialize(src.into_deserializer())
|
||||
}
|
||||
|
||||
pub fn missing_value_error(path: String) -> Error {
|
||||
ErrorInner::MissingValue(path).into()
|
||||
}
|
||||
|
||||
pub fn prepend_missing_value_error(e: Error, prefix: &str) -> Error {
|
||||
match *e.inner {
|
||||
ErrorInner::MissingValue(path) => {
|
||||
ErrorInner::MissingValue(format!("{}.{}", prefix, path)).into()
|
||||
}
|
||||
e => e.into(),
|
||||
}
|
||||
}
|
||||
|
||||
105
src/lib.rs
105
src/lib.rs
@@ -1,4 +1,4 @@
|
||||
use std::fmt;
|
||||
use std::{ffi::OsString, fmt, path::PathBuf};
|
||||
|
||||
use serde::Deserialize;
|
||||
|
||||
@@ -13,11 +13,23 @@ pub use confique_macro::Config;
|
||||
#[doc(hidden)]
|
||||
pub mod internal;
|
||||
|
||||
pub mod source;
|
||||
|
||||
|
||||
pub trait Config: Sized {
|
||||
type Partial: Partial;
|
||||
|
||||
fn from_partial(partial: Self::Partial) -> Result<Self, Error>;
|
||||
|
||||
fn from_sources(sources: &[&dyn Source<Self>]) -> Result<Self, Error> {
|
||||
let mut partial = Self::Partial::default_values();
|
||||
for src in sources.iter().rev() {
|
||||
let layer = src.load()?;
|
||||
partial = layer.with_fallback(partial);
|
||||
}
|
||||
|
||||
Self::from_partial(partial)
|
||||
}
|
||||
}
|
||||
|
||||
pub trait Partial: for<'de> Deserialize<'de> {
|
||||
@@ -26,22 +38,99 @@ pub trait Partial: for<'de> Deserialize<'de> {
|
||||
fn with_fallback(self, fallback: Self) -> Self;
|
||||
}
|
||||
|
||||
/// A source of configuration values for the configuration `T`, e.g. a file or
|
||||
/// environment variables.
|
||||
pub trait Source<C: Config> {
|
||||
fn load(&self) -> Result<C::Partial, Error>;
|
||||
}
|
||||
|
||||
pub enum Error {
|
||||
pub struct Error {
|
||||
inner: Box<ErrorInner>,
|
||||
}
|
||||
|
||||
enum ErrorInner {
|
||||
/// Returned by `Config::from_partial` when the partial does not contain
|
||||
/// values for all required configuration values. The string is a
|
||||
/// human-readable path to the value, e.g. `http.port`.
|
||||
MissingValue(String),
|
||||
|
||||
/// An IO error occured, e.g. when reading a file.
|
||||
Io {
|
||||
path: Option<PathBuf>,
|
||||
err: std::io::Error,
|
||||
},
|
||||
|
||||
/// Returned by `Source::load` implementations when deserialization fails.
|
||||
Deserialization {
|
||||
/// A human readable description for the error message, describing from
|
||||
/// what source it was attempted to deserialize. Completes the sentence
|
||||
/// "failed to deserialize configuration from ". E.g. "file 'foo.toml'"
|
||||
/// or "environment variable 'FOO_PORT'".
|
||||
source: Option<String>,
|
||||
err: Box<dyn std::error::Error + Send + Sync>,
|
||||
},
|
||||
|
||||
/// Returned by the [`Source`] impls for `Path` and `PathBuf` if the file
|
||||
/// extension is not supported by confique or if the corresponding Cargo
|
||||
/// feature of confique was not enabled.
|
||||
UnsupportedFileFormat {
|
||||
path: PathBuf,
|
||||
extension: OsString,
|
||||
},
|
||||
|
||||
/// Returned by the [`Source`] impls for `Path` and `PathBuf` if the path
|
||||
/// does not contain a file extension.
|
||||
MissingFileExtension {
|
||||
path: PathBuf,
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for Error {}
|
||||
impl std::error::Error for Error {
|
||||
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
|
||||
match &*self.inner {
|
||||
ErrorInner::Io { err, .. } => Some(err),
|
||||
ErrorInner::Deserialization { err, .. } => Some(&**err),
|
||||
ErrorInner::MissingValue(_)
|
||||
| ErrorInner::UnsupportedFileFormat { .. }
|
||||
| ErrorInner::MissingFileExtension { .. } => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Error {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
match self {
|
||||
Self::MissingValue(path) => {
|
||||
match &*self.inner {
|
||||
ErrorInner::MissingValue(path) => {
|
||||
std::write!(f, "required configuration value is missing: '{}'", path)
|
||||
}
|
||||
ErrorInner::Io { path: Some(path), .. } => {
|
||||
std::write!(f,
|
||||
"IO error occured while reading configuration file '{}'",
|
||||
path.display(),
|
||||
)
|
||||
}
|
||||
ErrorInner::Io { path: None, .. } => {
|
||||
std::write!(f, "IO error occured while loading configuration")
|
||||
}
|
||||
ErrorInner::Deserialization { source: Some(source), .. } => {
|
||||
std::write!(f, "failed to deserialize configuration from {}", source)
|
||||
}
|
||||
ErrorInner::Deserialization { source: None, .. } => {
|
||||
std::write!(f, "failed to deserialize configuration")
|
||||
}
|
||||
ErrorInner::UnsupportedFileFormat { path, extension } => {
|
||||
std::write!(f,
|
||||
"unknown configuration file format '{}' of '{}'",
|
||||
extension.to_string_lossy(),
|
||||
path.display(),
|
||||
)
|
||||
}
|
||||
ErrorInner::MissingFileExtension { path } => {
|
||||
std::write!(f,
|
||||
"cannot guess configuration file format due to missing file extension in '{}'",
|
||||
path.display(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -51,3 +140,9 @@ impl fmt::Debug for Error {
|
||||
fmt::Display::fmt(self, f)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ErrorInner> for Error {
|
||||
fn from(inner: ErrorInner) -> Self {
|
||||
Self { inner: Box::new(inner) }
|
||||
}
|
||||
}
|
||||
|
||||
122
src/source.rs
Normal file
122
src/source.rs
Normal file
@@ -0,0 +1,122 @@
|
||||
//! Types implementing [`Source`], thus representing some source of
|
||||
//! configuration values.
|
||||
|
||||
use std::{ffi::OsStr, fs, io, path::{Path, PathBuf}};
|
||||
|
||||
use crate::{Config, Error, ErrorInner, Partial, Source};
|
||||
|
||||
|
||||
|
||||
impl<C: Config> Source<C> for &Path {
|
||||
fn load(&self) -> Result<C::Partial, Error> {
|
||||
let ext = self.extension().ok_or_else(|| {
|
||||
ErrorInner::MissingFileExtension { path: self.into() }
|
||||
})?;
|
||||
let format = FileFormat::from_extension(ext).ok_or_else(|| {
|
||||
ErrorInner::UnsupportedFileFormat { extension: ext.into(), path: self.into() }
|
||||
})?;
|
||||
|
||||
<File as Source<C>>::load(&File::new(self, format))
|
||||
}
|
||||
}
|
||||
|
||||
impl<C: Config> Source<C> for PathBuf {
|
||||
fn load(&self) -> Result<C::Partial, Error> {
|
||||
<&Path as Source<C>>::load(&&**self)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct File {
|
||||
path: PathBuf,
|
||||
format: FileFormat,
|
||||
required: bool,
|
||||
}
|
||||
|
||||
impl File {
|
||||
pub fn new(path: impl Into<PathBuf>, format: FileFormat) -> Self {
|
||||
Self {
|
||||
path: path.into(),
|
||||
format,
|
||||
required: false,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "toml")]
|
||||
pub fn toml(path: impl Into<PathBuf>) -> Self {
|
||||
Self::new(path, FileFormat::Toml)
|
||||
}
|
||||
|
||||
#[cfg(feature = "yaml")]
|
||||
pub fn yaml(path: impl Into<PathBuf>) -> Self {
|
||||
Self::new(path, FileFormat::Yaml)
|
||||
}
|
||||
|
||||
/// Marks this file as required, meaning that `<File as Source<_>>::load`
|
||||
/// will return an error if the file does not exist. Otherwise, an empty
|
||||
/// layer (all values are `None`) is returned.
|
||||
pub fn required(mut self) -> Self {
|
||||
self.required = true;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<C: Config> Source<C> for File {
|
||||
// Unfortunately, if no file format is enabled, this emits unused variable
|
||||
// warnings. This should not happen as `self`, a type containing an empty
|
||||
// enum, is in scope, meaning that the code cannot be reached.
|
||||
#[cfg_attr(
|
||||
not(any(feature = "toml", feature = "yaml")),
|
||||
allow(unused_variables),
|
||||
)]
|
||||
fn load(&self) -> Result<C::Partial, Error> {
|
||||
// Load file contents. If the file does not exist and was not marked as
|
||||
// required, we just return an empty layer.
|
||||
let file_content = match fs::read(&self.path) {
|
||||
Ok(v) => v,
|
||||
Err(e) if e.kind() == io::ErrorKind::NotFound && !self.required => {
|
||||
return Ok(C::Partial::empty());
|
||||
}
|
||||
Err(e) => {
|
||||
return Err(ErrorInner::Io {
|
||||
path: Some(self.path.clone()),
|
||||
err: e,
|
||||
}.into());
|
||||
}
|
||||
};
|
||||
|
||||
// Helper closure to create an error.
|
||||
let error = |err| Error::from(ErrorInner::Deserialization {
|
||||
err,
|
||||
source: Some(format!("file '{}'", self.path.display())),
|
||||
});
|
||||
|
||||
match self.format {
|
||||
#[cfg(feature = "toml")]
|
||||
FileFormat::Toml => toml::from_slice(&file_content)
|
||||
.map_err(|e| error(Box::new(e))),
|
||||
|
||||
#[cfg(feature = "yaml")]
|
||||
FileFormat::Yaml => serde_yaml::from_slice(&file_content)
|
||||
.map_err(|e| error(Box::new(e))),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub enum FileFormat {
|
||||
#[cfg(feature = "toml")] Toml,
|
||||
#[cfg(feature = "yaml")] Yaml,
|
||||
}
|
||||
|
||||
impl FileFormat {
|
||||
pub fn from_extension(ext: impl AsRef<OsStr>) -> Option<Self> {
|
||||
match ext.as_ref().to_str()? {
|
||||
#[cfg(feature = "toml")]
|
||||
"toml" => Some(Self::Toml),
|
||||
|
||||
#[cfg(feature = "yaml")]
|
||||
"yaml" | "yml" => Some(Self::Yaml),
|
||||
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user