diff --git a/CHANGELOG.md b/CHANGELOG.md index d1e6b2a..02f225f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ ##### Config -- Refactored the internals of how validation errors work. +- Refactored the internals of how merge/validation errors work. - Removed `Config::META` and `ConfigError::META`. Use `Schematic::schema_name()` instead. - Removed `url` as a default Cargo feature. - Renamed `valid_*` Cargo features to `validate_*`. diff --git a/book/src/SUMMARY.md b/book/src/SUMMARY.md index 1080737..7c8f8c3 100644 --- a/book/src/SUMMARY.md +++ b/book/src/SUMMARY.md @@ -5,7 +5,7 @@ - [Settings](./config/settings.md) - [Partials](./config/partial.md) - [Nesting](./config/nested.md) - - [Context & metadata](./config/context.md) + - [Context](./config/context.md) - [Structs & enums](./config/struct/index.md) - [Default values](./config/struct/default.md) - [Environment variables](./config/struct/env.md) diff --git a/book/src/config/context.md b/book/src/config/context.md index 6da9367..f109d87 100644 --- a/book/src/config/context.md +++ b/book/src/config/context.md @@ -43,14 +43,3 @@ let result = ConfigLoader::::new() > Refer to the [default values](./struct/default.md), [merge strategies](./struct/merge.md), and > [validation rules](./struct/validate.md) sections for more information on how to use context. - -## Metadata - -[`Config`](./index.md) supports basic metadata for use within error messages. Right now we support a -name, derived from the struct/enum name or the serde `rename` attribute field. - -Metadata can be accessed with the `META` constant. - -```rust -ExampleConfig::META.name; -``` diff --git a/book/src/config/index.md b/book/src/config/index.md index 8462789..02956cc 100644 --- a/book/src/config/index.md +++ b/book/src/config/index.md @@ -135,8 +135,11 @@ Schematic is powered entirely by [serde](https://serde.rs), and supports the fol The following Cargo features are available: - `config` (default) - Enables configuration support (all the above stuff). +- `env` (default) - Enables environment variables for settings. +- `extends` (default) - Enables configs to extend other configs. - `json` - Enables JSON. - `toml` - Enables TOML. - `tracing` - Wrap generated code in tracing instrumentations. -- `url` (default) - Enables loading, extending, and parsing configs from URLs. +- `url` - Enables loading, extending, and parsing configs from URLs. +- `validate` (default) - Enables setting value validation. - `yaml` - Enables YAML. diff --git a/crates/macros/src/config/field.rs b/crates/macros/src/config/field.rs index e28d7dc..dd012a6 100644 --- a/crates/macros/src/config/field.rs +++ b/crates/macros/src/config/field.rs @@ -31,11 +31,11 @@ impl<'l> Field<'l> { { let value = if let Some(parse_env) = &self.args.parse_env { quote! { - parse_from_env_var(#env, #parse_env)? + parse_env_value(#env, #parse_env)? } } else { quote! { - default_from_env_var(#env)? + default_env_value(#env)? } }; diff --git a/crates/macros/src/config/field_value.rs b/crates/macros/src/config/field_value.rs index fd78531..0be209e 100644 --- a/crates/macros/src/config/field_value.rs +++ b/crates/macros/src/config/field_value.rs @@ -21,11 +21,11 @@ impl<'l> FieldValue<'l> { quote! { Some(#expr) } } Expr::Path(func) => { - quote! { handle_default_fn(#func(context))? } + quote! { handle_default_result(#func(context))? } } Expr::Lit(lit) => match &lit.lit { Lit::Str(string) => quote! { - Some(handle_default_fn(#value::try_from(#string))?) + Some(handle_default_result(#value::try_from(#string))?) }, other => quote! { Some(#other) }, }, @@ -88,7 +88,7 @@ impl<'l> FieldValue<'l> { } return quote! { - self.#key = merge_partial_setting( + self.#key = merge_nested_setting( self.#key.take(), next.#key.take(), context, diff --git a/crates/schematic/src/config/error.rs b/crates/schematic/src/config/error.rs index 1973ddc..8af92d6 100644 --- a/crates/schematic/src/config/error.rs +++ b/crates/schematic/src/config/error.rs @@ -1,3 +1,4 @@ +use super::merger::MergeError; use super::parser::ParserError; #[cfg(feature = "validate")] use super::validator::ValidatorError; @@ -14,6 +15,9 @@ pub enum ConfigError { #[error(transparent)] Handler(#[from] Box), + #[error(transparent)] + Merge(#[from] Box), + #[error(transparent)] UnsupportedFormat(#[from] Box), @@ -153,6 +157,12 @@ impl From for ConfigError { } } +impl From for ConfigError { + fn from(e: MergeError) -> ConfigError { + ConfigError::Merge(Box::new(e)) + } +} + impl From for ConfigError { fn from(e: UnsupportedFormatError) -> ConfigError { ConfigError::UnsupportedFormat(Box::new(e)) diff --git a/crates/schematic/src/config/merger.rs b/crates/schematic/src/config/merger.rs new file mode 100644 index 0000000..788e6dc --- /dev/null +++ b/crates/schematic/src/config/merger.rs @@ -0,0 +1,20 @@ +use miette::Diagnostic; +use std::fmt::Display; +use thiserror::Error; + +pub type MergeResult = std::result::Result, MergeError>; + +/// A merger function that receives the previous and next values, the current +/// context, and can return a [`MergeError`] on failure. +pub type Merger = Box MergeResult>; + +/// Error for merge failures. +#[derive(Error, Debug, Diagnostic)] +#[error("{0}")] +pub struct MergeError(pub String); + +impl MergeError { + pub fn new(message: T) -> Self { + Self(message.to_string()) + } +} diff --git a/crates/schematic/src/config/mod.rs b/crates/schematic/src/config/mod.rs index e48150f..549fc19 100644 --- a/crates/schematic/src/config/mod.rs +++ b/crates/schematic/src/config/mod.rs @@ -6,6 +6,7 @@ mod extender; mod formats; mod layer; mod loader; +mod merger; mod parser; mod path; mod source; @@ -19,6 +20,7 @@ pub use error::*; pub use extender::*; pub use layer::*; pub use loader::*; +pub use merger::*; pub use parser::*; pub use path::*; pub use source::*; @@ -35,5 +37,6 @@ macro_rules! derive_enum { } pub type DefaultValueResult = std::result::Result, HandlerError>; + +#[cfg(feature = "env")] pub type ParseEnvResult = std::result::Result, HandlerError>; -pub type MergeResult = std::result::Result, HandlerError>; diff --git a/crates/schematic/src/config/validator.rs b/crates/schematic/src/config/validator.rs index 239cbe7..b2dac84 100644 --- a/crates/schematic/src/config/validator.rs +++ b/crates/schematic/src/config/validator.rs @@ -6,6 +6,11 @@ use thiserror::Error; pub type ValidateResult = std::result::Result<(), ValidateError>; +/// A validator function that receives a setting value to validate, the parent +/// configuration the setting belongs to, the current context, and can return +/// a [`ValidateError`] on failure. +pub type Validator = Box ValidateResult>; + /// Error for a single validation failure. #[derive(Clone, Debug, Diagnostic, Error)] #[error("{}{} {message}", .path.to_string().style(Style::Id), ":".style(Style::MutedLight))] diff --git a/crates/schematic/src/internal.rs b/crates/schematic/src/internal.rs index 95360fb..9e90ac2 100644 --- a/crates/schematic/src/internal.rs +++ b/crates/schematic/src/internal.rs @@ -1,27 +1,27 @@ -use crate::config::{ConfigError, HandlerError, PartialConfig}; -use crate::merge::merge_partial; -use crate::ParseEnvResult; +use crate::config::{ConfigError, HandlerError, MergeError, MergeResult, PartialConfig}; use schematic_types::Schema; -use std::{env, str::FromStr}; +use std::str::FromStr; -pub fn handle_default_fn(result: Result) -> Result { +// Handles T and Option values +pub fn handle_default_result( + result: Result, +) -> Result { result.map_err(|error| ConfigError::InvalidDefaultValue(error.to_string())) } -pub fn default_from_env_var(key: &str) -> ParseEnvResult { - parse_from_env_var(key, |var| parse_value(var).map(|v| Some(v))) +#[cfg(feature = "env")] +pub fn default_env_value(key: &str) -> crate::config::ParseEnvResult { + parse_env_value(key, |value| parse_value(value).map(|v| Some(v))) } -pub fn parse_from_env_var( +#[cfg(feature = "env")] +pub fn parse_env_value( key: &str, - parser: impl Fn(String) -> ParseEnvResult, -) -> Result, HandlerError> { - if let Ok(var) = env::var(key) { - let value = parser(var).map_err(|error| { - HandlerError(format!("Invalid environment variable {key}. {error}")) - })?; - - return Ok(value); + parser: impl Fn(String) -> crate::config::ParseEnvResult, +) -> crate::config::ParseEnvResult { + if let Ok(value) = std::env::var(key) { + return parser(value) + .map_err(|error| HandlerError(format!("Invalid environment variable {key}. {error}"))); } Ok(None) @@ -42,8 +42,8 @@ pub fn merge_setting( prev: Option, next: Option, context: &C, - merger: impl Fn(T, T, &C) -> Result, HandlerError>, -) -> Result, HandlerError> { + merger: impl Fn(T, T, &C) -> MergeResult, +) -> MergeResult { if prev.is_some() && next.is_some() { merger(prev.unwrap(), next.unwrap(), context) } else if next.is_some() { @@ -54,13 +54,19 @@ pub fn merge_setting( } #[allow(clippy::unnecessary_unwrap)] -pub fn merge_partial_setting( +pub fn merge_nested_setting( prev: Option, next: Option, context: &T::Context, -) -> Result, HandlerError> { +) -> MergeResult { if prev.is_some() && next.is_some() { - merge_partial(prev.unwrap(), next.unwrap(), context) + let mut nested = prev.unwrap(); + + nested + .merge(context, next.unwrap()) + .map_err(|error| MergeError(error.to_string()))?; + + Ok(Some(nested)) } else if next.is_some() { Ok(next) } else { diff --git a/crates/schematic/src/merge.rs b/crates/schematic/src/merge.rs index 8d0a0d3..459be06 100644 --- a/crates/schematic/src/merge.rs +++ b/crates/schematic/src/merge.rs @@ -1,16 +1,9 @@ -use crate::{ - config::{HandlerError, PartialConfig}, - MergeResult, -}; +use crate::config::MergeResult; use std::{ collections::{BTreeMap, BTreeSet, HashMap, HashSet}, hash::Hash, }; -/// A merger function that receives the previous and next values, the current -/// context, and can return a [`HandlerError`] on failure. -pub type Merger = Box MergeResult>; - /// Discard both previous and next values and return [`None`]. pub fn discard(_: T, _: T, _: &C) -> MergeResult { Ok(None) @@ -103,16 +96,3 @@ where Ok(Some(prev)) } - -/// Merge the next [`PartialConfig`] into the previous [`PartialConfig`], using the merging -/// strategies defined by the [`Config`] derive implementation. -pub fn merge_partial( - mut prev: T, - next: T, - context: &T::Context, -) -> MergeResult { - prev.merge(context, next) - .map_err(|error| HandlerError(error.to_string()))?; - - Ok(Some(prev)) -} diff --git a/crates/schematic/src/validate/mod.rs b/crates/schematic/src/validate/mod.rs index 6e44c9f..7a2ed13 100644 --- a/crates/schematic/src/validate/mod.rs +++ b/crates/schematic/src/validate/mod.rs @@ -9,7 +9,7 @@ mod string; #[cfg(feature = "validate_url")] mod url; -pub use crate::config::{ValidateError, ValidateResult}; +pub use crate::config::{ValidateError, ValidateResult, Validator}; #[cfg(feature = "validate_email")] pub use email::*; #[cfg(feature = "extends")] @@ -21,11 +21,6 @@ pub use string::*; #[cfg(feature = "validate_url")] pub use url::*; -/// A validator function that receives a setting value to validate, the parent -/// configuration the setting belongs to, the current context, and can return -/// a [`ValidateError`] on failure. -pub type Validator = Box ValidateResult>; - pub(crate) fn map_err(error: garde::Error) -> ValidateError { ValidateError::new(error.to_string()) }