From 896fa6d3fa477e6e0bb9ad8d052a4ab9a321c151 Mon Sep 17 00:00:00 2001 From: Troy Benson Date: Fri, 7 Jul 2023 15:10:35 +0000 Subject: [PATCH] feat: new errors --- Cargo.lock | 21 ++ common/src/config.rs | 10 +- config/config/Cargo.toml | 4 +- config/config/example/derive.rs | 29 +- config/config/src/lib.rs | 390 +++++++++++++++++++++---- config/config/src/sources/cli.rs | 56 +++- config/config/src/sources/env.rs | 91 ++++-- config/config/src/sources/file/json.rs | 2 +- config/config/src/sources/file/mod.rs | 59 ++-- config/config/src/sources/file/toml.rs | 2 +- config/config/src/sources/utils.rs | 8 +- 11 files changed, 540 insertions(+), 132 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index bb89e55d..fae6bb0e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -870,7 +870,9 @@ dependencies = [ "convert_case", "serde", "serde-value", + "serde_ignored", "serde_json", + "serde_path_to_error", "serde_yaml", "thiserror", "toml", @@ -3333,6 +3335,15 @@ dependencies = [ "syn 2.0.23", ] +[[package]] +name = "serde_ignored" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6de9d103529a9ba50008099785839df1e6f40b4576ed4c000cbfdb051182b827" +dependencies = [ + "serde", +] + [[package]] name = "serde_json" version = "1.0.100" @@ -3344,6 +3355,16 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_path_to_error" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8acc4422959dd87a76cb117c191dcbffc20467f06c9100b76721dab370f24d3a" +dependencies = [ + "itoa", + "serde", +] + [[package]] name = "serde_spanned" version = "0.6.3" diff --git a/common/src/config.rs b/common/src/config.rs index 52cbd544..475530ce 100644 --- a/common/src/config.rs +++ b/common/src/config.rs @@ -137,13 +137,13 @@ pub fn parse( } builder.add_source_with_priority(source, 1); } - Err(config::ConfigError::File(e)) => { - if key_provided { - return Err(e.into()); + Err(err) => { + if key_provided || !err.is_io() { + return Err(err.into()); } - tracing::debug!("failed to load config file: {}", e); + + tracing::debug!("failed to load config file: {}", err); } - Err(e) => return Err(e.into()), } } diff --git a/config/config/Cargo.toml b/config/config/Cargo.toml index ab59e5a4..6ec87145 100644 --- a/config/config/Cargo.toml +++ b/config/config/Cargo.toml @@ -15,8 +15,10 @@ toml = "0" clap = { version = "4", features = ["cargo", "string"] } convert_case = "0" serde = { version = "1", features = ["derive"] } -serde-value = "0" tracing = { version = "0" } +serde_ignored = "0" +serde-value = "0" +serde_path_to_error = "0" # Derive macro config_derive = { path = "../config_derive" } diff --git a/config/config/example/derive.rs b/config/config/example/derive.rs index 98862cc6..7c9990cf 100644 --- a/config/config/example/derive.rs +++ b/config/config/example/derive.rs @@ -1,7 +1,7 @@ //! Run with: `cargo run --example derive` //! Look at the generated code with: `cargo expand --example derive` -use config::{sources, ConfigBuilder}; +use config::{sources, ConfigBuilder, ConfigError}; type TypeAlias = bool; @@ -14,7 +14,7 @@ struct AppConfig { } #[derive(config::Config, Debug, PartialEq, serde::Deserialize, serde::Serialize)] -#[serde(default)] +// #[serde(default)] struct LoggingConfig { level: String, json: bool, @@ -30,11 +30,26 @@ impl Default for LoggingConfig { } fn main() { - let mut builder: ConfigBuilder = ConfigBuilder::new(); - builder.add_source(sources::CliSource::new().unwrap()); + match parse() { + Ok(config) => println!("{:#?}", config), + Err(err) => println!("{:#}", err), + } +} - match builder.build() { - Ok(config) => println!("{:?}", config), - Err(e) => eprintln!("error: {:#}", e), +fn parse() -> Result { + let mut builder = ConfigBuilder::new(); + builder.add_source(sources::CliSource::new()?); + builder.add_source(sources::EnvSource::with_prefix("TEST")?); + builder.add_source(sources::FileSource::json( + br#" + { + "optional": null } + "# + .as_slice(), + )?); + + let config: AppConfig = builder.build()?; + + Ok(config) } diff --git a/config/config/src/lib.rs b/config/config/src/lib.rs index 09948d2c..818d2c90 100644 --- a/config/config/src/lib.rs +++ b/config/config/src/lib.rs @@ -1,10 +1,11 @@ //! TODO: write docs -use std::collections::{BTreeMap, HashSet}; +use std::collections::{BTreeMap, BTreeSet, HashSet}; use std::io; use std::num::{ParseFloatError, ParseIntError}; use std::ops::Deref; use std::str::ParseBoolError; +use std::sync::Arc; pub use config_derive::Config; @@ -14,59 +15,253 @@ mod tests; mod key; pub mod sources; pub use key::*; -use serde_value::Value; +use serde_ignored::Path; +use serde_value::DeserializerError; +pub use serde_value::Value; pub type Result = std::result::Result; #[derive(Debug, thiserror::Error)] -pub enum ConfigError { - #[error("failed to parse file: {0}")] - File(#[from] FileError), - #[error("failed to parse value: {0}")] - Parse(#[from] ParseError), - #[error("unsupported file format/extension: {0}")] +pub enum ConfigErrorEnum { + #[error("unsupported file format: {0}")] UnsupportedFileFormat(String), - #[error("type mismatch{}, expected: {expected:?}, got: {got:?}", path.as_ref().map(|p| format!(" for {}", p)).unwrap_or_default())] - TypeMismatch { - path: Option, - expected: KeyType, - got: Value, - }, - // TODO: Add path to MissingKey error - #[error("missing key")] - MissingKey, - #[error("can't parse value from string: {0}")] - FromStr(Box), - #[error("can't call, not a primitive")] - NotAPrimitive, - #[error("can't call on a primitive")] - IsAPrimitive, - #[error("cannot deserialize into a struct: {0}")] - Struct(#[from] serde_value::DeserializerError), -} - -#[derive(Debug, thiserror::Error)] -pub enum FileError { - #[error("{0}")] + #[error("deserialize: {0}")] + Deserialize(#[from] serde_value::DeserializerError), + #[error("parse int: {0}")] + Integer(#[from] ParseIntError), + #[error("parse float: {0}")] + Float(#[from] ParseFloatError), + #[error("parse bool: {0}")] + Bool(#[from] ParseBoolError), + #[error("unsupported type: {0:?}")] + UnsupportedType(KeyType), + #[error("io: {0}")] Io(#[from] io::Error), - #[error("{0}")] + #[error("toml: {0}")] Toml(#[from] toml::de::Error), - #[error("{0}")] + #[error("yaml: {0}")] Yaml(#[from] serde_yaml::Error), - #[error("{0}")] + #[error("json: {0}")] Json(#[from] serde_json::Error), + #[error("multiple errors: {0}")] + Multiple(MultiError), + #[error("subkey on non-map: {0:?}")] + SubkeyOnNonMap(Value), } -#[derive(Debug, thiserror::Error)] -pub enum ParseError { - #[error("{0}")] - Integer(#[from] ParseIntError), - #[error("{0}")] - Float(#[from] ParseFloatError), - #[error("{0}")] - Bool(#[from] ParseBoolError), - #[error("unsupported type: {1:?} for key: {0}")] - UnsupportedType(KeyPath, KeyType), +#[derive(Debug)] +pub struct MultiError(Vec); + +impl MultiError { + pub fn into_inner(self) -> Vec { + self.0 + } + + pub fn inner(&self) -> &[ConfigError] { + &self.0 + } +} + +impl std::fmt::Display for MultiError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let errors = self + .0 + .iter() + .map(|e| format!("{}", e)) + .collect::>() + .join(", "); + + write!(f, "{}", errors) + } +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum ErrorSource { + Cli, + File, + Env, +} + +#[derive(Debug, Clone)] +pub struct ConfigError { + error: Arc, + path: Option, + source: Option, +} + +impl ConfigError { + pub(crate) fn new(error: ConfigErrorEnum) -> Self { + Self { + error: Arc::new(error), + path: None, + source: None, + } + } + + pub(crate) fn with_path(mut self, path: KeyPath) -> Self { + self.path = Some(path); + self + } + + pub(crate) fn with_source(mut self, source: ErrorSource) -> Self { + self.source = Some(source); + self + } + + pub fn is_io(&self) -> bool { + match self.error() { + ConfigErrorEnum::Io(_) => true, + ConfigErrorEnum::Multiple(MultiError(errors)) => errors.iter().any(|e| e.is_io()), + _ => false, + } + } + + pub fn path(&self) -> Option<&KeyPath> { + self.path.as_ref() + } + + pub fn source(&self) -> Option { + self.source + } + + pub fn error(&self) -> &ConfigErrorEnum { + &self.error + } + + pub fn multi(self, other: Self) -> Self { + let mut errors = Vec::new(); + match self.error() { + ConfigErrorEnum::Multiple(MultiError(errors1)) => { + errors.extend(errors1.clone()); + } + _ => { + errors.push(self); + } + } + + match other.error() { + ConfigErrorEnum::Multiple(MultiError(errors2)) => { + errors.extend(errors2.clone()); + } + _ => { + errors.push(other); + } + } + + Self::new(ConfigErrorEnum::Multiple(MultiError(errors))) + } +} + +impl From for ConfigError { + fn from(value: ConfigErrorEnum) -> Self { + Self::new(value) + } +} + +impl std::fmt::Display for ConfigError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + // Some errors will have a path and or source, so we should include those in the output. + // We should also write better errors for the enum variants. + let mut path = self.path.clone(); + + if let Some(source) = self.source() { + write!(f, "{:?}: ", source)?; + } + + match self.error() { + ConfigErrorEnum::UnsupportedFileFormat(format) => { + write!(f, "unsupported file format: {}", format) + } + ConfigErrorEnum::Deserialize(DeserializerError::DuplicateField(field)) => { + if let Some(path) = path.take() { + write!(f, "duplicate field: {}", path.push_child(field)) + } else { + write!(f, "duplicate field: {}", field) + } + } + ConfigErrorEnum::Deserialize(DeserializerError::MissingField(field)) => { + if let Some(path) = path.take() { + write!(f, "missing field: {}", path.push_child(field)) + } else { + write!(f, "missing field: {}", field) + } + } + ConfigErrorEnum::Deserialize(DeserializerError::InvalidType(unexpected, expected)) => { + write!( + f, + "invalid type: expected {}, found {:?}", + expected, unexpected + ) + } + ConfigErrorEnum::Deserialize(DeserializerError::InvalidValue(unexpected, expected)) => { + write!( + f, + "invalid value: expected {}, found {:?}", + expected, unexpected + ) + } + ConfigErrorEnum::Deserialize(DeserializerError::UnknownVariant(field, expected)) => { + if let Some(path) = path.take() { + write!( + f, + "unknown variant: expected one of {:?}, found {}", + expected, + path.push_child(field) + ) + } else { + write!( + f, + "unknown variant: expected one of {:?}, found {}", + expected, field + ) + } + } + ConfigErrorEnum::Deserialize(DeserializerError::Custom(custom)) => { + write!(f, "unknown deserialize error: {}", custom) + } + ConfigErrorEnum::Deserialize(DeserializerError::InvalidLength(len, unexpected)) => { + write!(f, "invalid length: expected {}, found {}", len, unexpected) + } + ConfigErrorEnum::Deserialize(DeserializerError::UnknownField(field, expected)) => { + if let Some(path) = path.take() { + write!( + f, + "unknown field: expected one of {:?}, found {}", + expected, + path.push_child(field) + ) + } else { + write!( + f, + "unknown field: expected one of {:?}, found {}", + expected, field + ) + } + } + ConfigErrorEnum::Integer(error) => write!(f, "failed to parse int: {}", error), + ConfigErrorEnum::Float(error) => write!(f, "failed to parse float: {}", error), + ConfigErrorEnum::Bool(error) => write!(f, "failed to parse bool: {}", error), + ConfigErrorEnum::UnsupportedType(ty) => write!(f, "unsupported type: {:?}", ty), + ConfigErrorEnum::Io(error) => write!(f, "io error: {}", error), + ConfigErrorEnum::Toml(error) => write!(f, "toml decode error: {}", error), + ConfigErrorEnum::Yaml(error) => write!(f, "yaml decode error: {}", error), + ConfigErrorEnum::Json(error) => write!(f, "json decode error: {}", error), + ConfigErrorEnum::Multiple(error) => write!(f, "multiple errors: {}", error), + ConfigErrorEnum::SubkeyOnNonMap(ty) => write!(f, "subkey on non-map: {:?}", ty), + }?; + + if let Some(path) = path { + write!(f, " ({})", path)?; + } + + Ok(()) + } +} + +impl std::error::Error for ConfigError { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + Some(&*self.error) + } } pub trait Source { @@ -75,10 +270,7 @@ pub trait Source { /// You don't need to implemented this trait manually. /// Please use the provided derive macro to generate the implementation. -pub trait Config: Sized + 'static + serde::de::DeserializeOwned + serde::ser::Serialize -where - Self: Sized, -{ +pub trait Config: serde::de::Deserialize<'static> + serde::ser::Serialize { const PKG_NAME: Option<&'static str> = None; const ABOUT: Option<&'static str> = None; const VERSION: Option<&'static str> = None; @@ -120,13 +312,13 @@ impl Default for ConfigBuilder { } } -fn merge(first: Value, second: Value) -> Value { +fn merge(first: Value, second: Value) -> (Value, Option) { let Value::Map(mut first) = first else { - return first; + return (first, None) }; let Value::Map(mut second) = second else { - return Value::Map(first); + return (Value::Map(first), None) }; let mut merged = BTreeMap::new(); @@ -136,21 +328,39 @@ fn merge(first: Value, second: Value) -> Value { .chain(second.keys()) .cloned() .collect::>(); + + let mut error: Option = None; + for key in keys { let first = first.remove(&key); let second = second.remove(&key); - let value = match (first, second) { + let (value, new_error) = match (first, second) { (Some(first), Some(second)) => merge(first, second), - (Some(first), None) => first, - (None, Some(second)) => second, + (Some(first), None) => (first, None), + (None, Some(second)) => (second, None), (None, None) => unreachable!(), }; + match (error, new_error) { + (Some(err), Some(new_err)) => { + error = Some(err.multi(new_err)); + } + (Some(err), None) => { + error = Some(err); + } + (None, Some(new_err)) => { + error = Some(new_err); + } + (None, None) => { + error = None; + } + } + merged.insert(key, value); } - Value::Map(merged) + (Value::Map(merged), error) } impl ConfigBuilder { @@ -183,21 +393,79 @@ impl ConfigBuilder { pub fn get_key(&self, path: impl Into) -> Result> { let key_path = path.into(); - Ok(self + let mut iter = self .sources .iter() .map(|s| s.get_key(&key_path)) .collect::>>()? .into_iter() - .flatten() - .reduce(merge)) + .flatten(); + + let Some(mut value) = iter.next() else { + return Ok(None) + }; + + let mut error: Option = None; + for v in iter { + let (merged, new_error) = merge(value, v); + + match (error, new_error) { + (Some(err), Some(new_err)) => { + error = Some(err.multi(new_err)); + } + (Some(err), None) => { + error = Some(err); + } + (None, Some(new_err)) => { + error = Some(new_err); + } + (None, None) => { + error = None; + } + } + + value = merged; + } + + if let Some(error) = error { + Err(error) + } else { + Ok(Some(value)) + } } /// Parse a single key. - pub fn parse_key(&self, path: impl Into) -> Result { - Ok(T::deserialize( - self.get_key(path)?.unwrap_or(Value::Option(None)), - )?) + pub fn parse_key<'de, T: serde::de::Deserialize<'de>>( + &self, + path: impl Into, + ) -> Result { + // We can use serde_ignored to find more information about the state of our struct. + let d = self.get_key(path)?.unwrap_or(Value::Option(None)); + let mut paths = BTreeSet::new(); + + let mut cb = |path: Path| { + paths.insert(path.to_string()); + }; + + let ignored_de = serde_ignored::Deserializer::new(d, &mut cb); + + let value = match serde_path_to_error::deserialize(ignored_de) { + Ok(t) => Ok(t), + Err(e) => { + let path = e.path().to_string(); + let error = e.into_inner(); + Err(ConfigError::new(error.into()).with_path(path.as_str().into())) + } + }; + + if !paths.is_empty() { + tracing::warn!( + "fields ignored while deserializing, {}", + paths.into_iter().collect::>().join(", ") + ); + } + + value } /// This function iterates all added sources and builds a config for all `C::keys`. diff --git a/config/config/src/sources/cli.rs b/config/config/src/sources/cli.rs index 4cd38067..5fb07fee 100644 --- a/config/config/src/sources/cli.rs +++ b/config/config/src/sources/cli.rs @@ -3,7 +3,9 @@ use std::marker::PhantomData; use clap::{command, Arg, ArgAction, ArgMatches, Command}; use convert_case::{Case, Casing}; -use crate::{Config, KeyPath, KeyTree, ParseError, Result, Source, Value}; +use crate::{ + Config, ConfigError, ConfigErrorEnum, ErrorSource, KeyPath, KeyTree, Result, Source, Value, +}; use super::utils; @@ -93,11 +95,14 @@ fn extend_cmd( } KeyTree::Seq(t) => { if sequenced { - return Err(ParseError::UnsupportedType(path.clone(), t.key_type()).into()); + return Err( + ConfigError::new(ConfigErrorEnum::UnsupportedType(t.key_type())) + .with_path(path.clone()), + ); } else { let (arg, cmd) = extend_cmd(cmd, t, arg, path, true)?; let Some(arg) = arg else { - return Err(ParseError::UnsupportedType(path.clone(), t.key_type()).into()) + return Err(ConfigError::new(ConfigErrorEnum::UnsupportedType(t.key_type())).with_path(path.clone())); }; let num_args = arg.get_num_args().unwrap(); @@ -106,7 +111,10 @@ fn extend_cmd( } KeyTree::Option(t) => { if sequenced { - return Err(ParseError::UnsupportedType(path.clone(), t.key_type()).into()); + return Err( + ConfigError::new(ConfigErrorEnum::UnsupportedType(t.key_type())) + .with_path(path.clone()), + ); } let (arg, cmd) = extend_cmd(cmd, t, arg, path, false)?; @@ -119,7 +127,10 @@ fn extend_cmd( } KeyTree::Map(map) => { if sequenced { - return Err(ParseError::UnsupportedType(path.clone(), tree.key_type()).into()); + return Err( + ConfigError::new(ConfigErrorEnum::UnsupportedType(tree.key_type())) + .with_path(path.clone()), + ); } let mut cmd = cmd; @@ -198,7 +209,12 @@ pub fn generate_command() -> Result { let map = match C::tree() { KeyTree::Map(map) => map, - r => return Err(ParseError::UnsupportedType(KeyPath::new(), r.key_type()).into()), + r => { + return Err( + ConfigError::new(ConfigErrorEnum::UnsupportedType(r.key_type())) + .with_path(KeyPath::new()), + ) + } }; let (arg, mut command) = extend_cmd(command, &KeyTree::Map(map), None, &KeyPath::new(), false)?; @@ -372,14 +388,20 @@ fn matches_to_value( } KeyTree::Seq(t) => { if sequenced { - return Err(ParseError::UnsupportedType(path, t.key_type()).into()); + return Err( + ConfigError::new(ConfigErrorEnum::UnsupportedType(t.key_type())) + .with_path(path), + ); } matches_to_value(path, t, matches, true) } KeyTree::Option(t) => { if sequenced { - return Err(ParseError::UnsupportedType(path, t.key_type()).into()); + return Err( + ConfigError::new(ConfigErrorEnum::UnsupportedType(t.key_type())) + .with_path(path), + ); } let value = matches_to_value(path, t, matches, false)?; @@ -396,7 +418,10 @@ fn matches_to_value( } KeyTree::Map(map) => { if sequenced { - return Err(ParseError::UnsupportedType(path, tree.key_type()).into()); + return Err( + ConfigError::new(ConfigErrorEnum::UnsupportedType(tree.key_type())) + .with_path(path), + ); } let mut hashmap = std::collections::BTreeMap::new(); @@ -426,13 +451,18 @@ fn matches_to_value( impl CliSource { pub fn new() -> Result { - Self::with_matches(generate_command::()?.get_matches()) + Self::with_matches( + generate_command::() + .map_err(|e| e.with_source(ErrorSource::Cli))? + .get_matches(), + ) } pub fn with_matches(matches: ArgMatches) -> Result { Ok(Self { - value: matches_to_value(KeyPath::new(), &C::tree(), &matches, false)? - .unwrap_or(Value::Option(None)), + value: matches_to_value(KeyPath::new(), &C::tree(), &matches, false) + .map_err(|e| e.with_source(ErrorSource::Cli))? + .unwrap_or(Value::Map(Default::default())), _phantom: PhantomData, }) } @@ -440,6 +470,6 @@ impl CliSource { impl Source for CliSource { fn get_key(&self, path: &KeyPath) -> Result> { - utils::get_key(&self.value, path) + utils::get_key(&self.value, path).map_err(|e| e.with_source(ErrorSource::Cli)) } } diff --git a/config/config/src/sources/env.rs b/config/config/src/sources/env.rs index 7abb2456..2d912658 100644 --- a/config/config/src/sources/env.rs +++ b/config/config/src/sources/env.rs @@ -1,6 +1,9 @@ use std::{collections::BTreeMap, marker::PhantomData}; -use crate::{Config, KeyPath, KeyTree, KeyType, ParseError, Result, Source, Value}; +use crate::{ + Config, ConfigError, ConfigErrorEnum, ErrorSource, KeyPath, KeyTree, KeyType, Result, Source, + Value, +}; use super::utils; @@ -30,7 +33,8 @@ impl EnvSource { joiner, false, false, - )? + ) + .map_err(|e| e.with_source(ErrorSource::Env))? .unwrap_or(Value::Option(None)), }) } @@ -78,7 +82,10 @@ fn extract_keys( } KeyTree::Map(map) => { if seq { - return Err(ParseError::UnsupportedType(path, tree.key_type()).into()); + return Err( + ConfigError::new(ConfigErrorEnum::UnsupportedType(tree.key_type())) + .with_path(path), + ); } let result = map @@ -107,14 +114,20 @@ fn extract_keys( } KeyTree::Option(tree) => { if seq { - return Err(ParseError::UnsupportedType(path, tree.key_type()).into()); + return Err( + ConfigError::new(ConfigErrorEnum::UnsupportedType(tree.key_type())) + .with_path(path), + ); } extract_keys(tree, prefix, path, joiner, seq, true) } KeyTree::Seq(tree) => { if seq { - return Err(ParseError::UnsupportedType(path, tree.key_type()).into()); + return Err( + ConfigError::new(ConfigErrorEnum::UnsupportedType(tree.key_type())) + .with_path(path), + ); } extract_keys(tree, prefix, path, joiner, true, false) @@ -125,17 +138,61 @@ fn extract_keys( fn parse_to_value(key_type: KeyType, s: &str) -> Result { match key_type { KeyType::String => Ok(Value::String(s.to_string())), - KeyType::I64 => Ok(Value::I64(s.parse::().map_err(ParseError::Integer)?)), - KeyType::U64 => Ok(Value::U64(s.parse::().map_err(ParseError::Integer)?)), - KeyType::I32 => Ok(Value::I32(s.parse::().map_err(ParseError::Integer)?)), - KeyType::U32 => Ok(Value::U32(s.parse::().map_err(ParseError::Integer)?)), - KeyType::I16 => Ok(Value::I16(s.parse::().map_err(ParseError::Integer)?)), - KeyType::U16 => Ok(Value::U16(s.parse::().map_err(ParseError::Integer)?)), - KeyType::I8 => Ok(Value::I8(s.parse::().map_err(ParseError::Integer)?)), - KeyType::U8 => Ok(Value::U8(s.parse::().map_err(ParseError::Integer)?)), - KeyType::F32 => Ok(Value::F32(s.parse::().map_err(ParseError::Float)?)), - KeyType::F64 => Ok(Value::F64(s.parse::().map_err(ParseError::Float)?)), - KeyType::Bool => Ok(Value::Bool(s.parse::().map_err(ParseError::Bool)?)), + KeyType::I64 => Ok(Value::I64( + s.parse::() + .map_err(ConfigErrorEnum::Integer) + .map_err(ConfigError::new)?, + )), + KeyType::U64 => Ok(Value::U64( + s.parse::() + .map_err(ConfigErrorEnum::Integer) + .map_err(ConfigError::new)?, + )), + KeyType::I32 => Ok(Value::I32( + s.parse::() + .map_err(ConfigErrorEnum::Integer) + .map_err(ConfigError::new)?, + )), + KeyType::U32 => Ok(Value::U32( + s.parse::() + .map_err(ConfigErrorEnum::Integer) + .map_err(ConfigError::new)?, + )), + KeyType::I16 => Ok(Value::I16( + s.parse::() + .map_err(ConfigErrorEnum::Integer) + .map_err(ConfigError::new)?, + )), + KeyType::U16 => Ok(Value::U16( + s.parse::() + .map_err(ConfigErrorEnum::Integer) + .map_err(ConfigError::new)?, + )), + KeyType::I8 => Ok(Value::I8( + s.parse::() + .map_err(ConfigErrorEnum::Integer) + .map_err(ConfigError::new)?, + )), + KeyType::U8 => Ok(Value::U8( + s.parse::() + .map_err(ConfigErrorEnum::Integer) + .map_err(ConfigError::new)?, + )), + KeyType::F32 => Ok(Value::F32( + s.parse::() + .map_err(ConfigErrorEnum::Float) + .map_err(ConfigError::new)?, + )), + KeyType::F64 => Ok(Value::F64( + s.parse::() + .map_err(ConfigErrorEnum::Float) + .map_err(ConfigError::new)?, + )), + KeyType::Bool => Ok(Value::Bool( + s.parse::() + .map_err(ConfigErrorEnum::Bool) + .map_err(ConfigError::new)?, + )), KeyType::Unit => Ok(Value::Unit), _ => unreachable!(), } @@ -143,6 +200,6 @@ fn parse_to_value(key_type: KeyType, s: &str) -> Result { impl Source for EnvSource { fn get_key(&self, path: &KeyPath) -> Result> { - utils::get_key(&self.value, path) + utils::get_key(&self.value, path).map_err(|e| e.with_source(ErrorSource::Cli)) } } diff --git a/config/config/src/sources/file/json.rs b/config/config/src/sources/file/json.rs index 1a661c6e..60a9b3e7 100644 --- a/config/config/src/sources/file/json.rs +++ b/config/config/src/sources/file/json.rs @@ -1,6 +1,6 @@ use std::collections::BTreeMap; -use serde_value::Value; +use crate::Value; pub fn convert_value(value: serde_json::Value) -> Value { match value { diff --git a/config/config/src/sources/file/mod.rs b/config/config/src/sources/file/mod.rs index 71f56f7d..3da79ca7 100644 --- a/config/config/src/sources/file/mod.rs +++ b/config/config/src/sources/file/mod.rs @@ -5,7 +5,7 @@ use std::{ path::{Path, PathBuf}, }; -use crate::{Config, ConfigError, FileError, KeyPath, Result, Source, Value}; +use crate::{Config, ConfigError, ConfigErrorEnum, ErrorSource, KeyPath, Result, Source, Value}; use super::utils; @@ -21,27 +21,41 @@ pub struct FileSource { impl FileSource { pub fn with_path>(path: P) -> Result { - let mut fs = match path.as_ref().extension().and_then(|s| s.to_str()) { - Some("json") => Self::json(fs::File::open(&path).map_err(FileError::Io)?)?, - Some("toml") => Self::toml(fs::File::open(&path).map_err(FileError::Io)?)?, - Some("yaml") | Some("yml") => { - Self::yaml(fs::File::open(&path).map_err(FileError::Io)?)? - } + let fs_fn = match path.as_ref().extension().and_then(|s| s.to_str()) { + Some("json") => Self::json, + Some("toml") => Self::toml, + Some("yaml") | Some("yml") => Self::yaml, None => { // We should try to parse the file as all supported formats // and return the first one that succeeds - return Self::with_path(path.as_ref().with_extension("json")) - .or_else(|_| Self::with_path(path.as_ref().with_extension("toml"))) - .or_else(|_| Self::with_path(path.as_ref().with_extension("yaml"))) - .or_else(|_| Self::with_path(path.as_ref().with_extension("yml"))); + let fn_mut = |e: Option, ext: &str| { + if e.as_ref().map(|e| e.is_io()).unwrap_or(true) { + Self::with_path(path.as_ref().with_extension(ext)) + } else { + Err(e.unwrap()) + } + }; + + return fn_mut(None, "json") + .or_else(|err| fn_mut(Some(err), "toml")) + .or_else(|err| fn_mut(Some(err), "yaml")) + .or_else(|err| fn_mut(Some(err), "yml")); } ext => { - return Err(ConfigError::UnsupportedFileFormat( + return Err(ConfigErrorEnum::UnsupportedFileFormat( ext.map(|s| s.to_string()).unwrap_or_default(), - )) + ) + .into()) } }; + let mut fs = match fs::File::open(&path).map(fs_fn) { + Ok(Ok(fs)) => Ok(fs), + Ok(Err(err)) => Err(err), + Err(err) => Err(ConfigErrorEnum::Io(err).into()), + } + .map_err(|e| e.with_source(ErrorSource::File))?; + fs.path = Some(path.as_ref().to_path_buf()); Ok(fs) @@ -52,8 +66,9 @@ impl FileSource { } pub fn json(reader: R) -> Result { - let content: serde_json::Value = - serde_json::from_reader(reader).map_err(FileError::Json)?; + let content: serde_json::Value = serde_json::from_reader(reader).map_err(|e| { + ConfigError::new(ConfigErrorEnum::Json(e)).with_source(ErrorSource::File) + })?; Ok(Self { content: json::convert_value(content), _phantom: PhantomData, @@ -62,8 +77,11 @@ impl FileSource { } pub fn toml(reader: R) -> Result { - let content = io::read_to_string(reader).map_err(FileError::Io)?; - let value: ::toml::Value = ::toml::from_str(&content).map_err(FileError::Toml)?; + let content = io::read_to_string(reader) + .map_err(|e| ConfigError::new(ConfigErrorEnum::Io(e)).with_source(ErrorSource::File))?; + let value: ::toml::Value = ::toml::from_str(&content).map_err(|e| { + ConfigError::new(ConfigErrorEnum::Toml(e)).with_source(ErrorSource::File) + })?; Ok(Self { content: toml::convert_value(value), _phantom: PhantomData, @@ -72,8 +90,9 @@ impl FileSource { } pub fn yaml(reader: R) -> Result { - let content: serde_yaml::Value = - serde_yaml::from_reader(reader).map_err(FileError::Yaml)?; + let content: serde_yaml::Value = serde_yaml::from_reader(reader).map_err(|e| { + ConfigError::new(ConfigErrorEnum::Yaml(e)).with_source(ErrorSource::File) + })?; Ok(Self { content: yaml::convert_value(content).unwrap_or_else(|| Value::Map(Default::default())), _phantom: PhantomData, @@ -84,6 +103,6 @@ impl FileSource { impl Source for FileSource { fn get_key(&self, path: &KeyPath) -> Result> { - utils::get_key(&self.content, path) + utils::get_key(&self.content, path).map_err(|e| e.with_source(ErrorSource::File)) } } diff --git a/config/config/src/sources/file/toml.rs b/config/config/src/sources/file/toml.rs index ab41c668..806da8c5 100644 --- a/config/config/src/sources/file/toml.rs +++ b/config/config/src/sources/file/toml.rs @@ -1,6 +1,6 @@ use std::collections::BTreeMap; -use serde_value::Value; +use crate::Value; pub fn convert_value(value: toml::Value) -> Value { match value { diff --git a/config/config/src/sources/utils.rs b/config/config/src/sources/utils.rs index 21cb982f..00d87a2e 100644 --- a/config/config/src/sources/utils.rs +++ b/config/config/src/sources/utils.rs @@ -1,13 +1,9 @@ -use serde_value::Value; - -use crate::{KeyPath, Result}; +use crate::{ConfigError, ConfigErrorEnum, KeyPath, Result, Value}; pub fn get_key(mut current: &Value, path: &KeyPath) -> Result> { for segment in path { let Value::Map(map) = current else { - // Trying to access a field on a non-map type - // I'm not sure if we should return an error here - panic!("Trying to access a field on a non-map type: {}, {:#?}", path, current); + return Err(ConfigError::new(ConfigErrorEnum::SubkeyOnNonMap(current.clone())).with_path(path.clone())); }; let Some(value) = map.get(&Value::String(segment.clone())) else { return Ok(None);