Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Flexible inputs, second attempt #100

Merged
merged 1 commit into from
Apr 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
100 changes: 96 additions & 4 deletions src/figment.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use serde::de::Deserialize;

use crate::{Profile, Provider, Metadata};
use crate::error::{Kind, Result};
use crate::value::{Value, Map, Dict, Tag, ConfiguredValueDe};
use crate::value::{Value, Map, Dict, Tag, ConfiguredValueDe, DefaultInterpreter, LossyInterpreter};
use crate::coalesce::{Coalescible, Order};

/// Combiner of [`Provider`]s for configuration value extraction.
Expand Down Expand Up @@ -482,7 +482,64 @@ impl Figment {
/// });
/// ```
pub fn extract<'a, T: Deserialize<'a>>(&self) -> Result<T> {
T::deserialize(ConfiguredValueDe::from(self, &self.merged()?))
let value = self.merged()?;
T::deserialize(ConfiguredValueDe::<'_, DefaultInterpreter>::from(self, &value))
}

/// As [`extract`](Figment::extract_lossy), but interpret numbers and
/// booleans more flexibly.
///
/// See [`Value::to_bool_lossy`] and [`Value::to_num_lossy`] for a full
/// explanation of the imputs accepted.
///
///
/// # Example
///
/// ```rust
/// use serde::Deserialize;
///
/// use figment::{Figment, providers::{Format, Toml, Json, Env}};
///
/// #[derive(Debug, PartialEq, Deserialize)]
/// struct Config {
/// name: String,
/// numbers: Option<Vec<usize>>,
/// debug: bool,
/// }
///
/// figment::Jail::expect_with(|jail| {
/// jail.create_file("Config.toml", r#"
/// name = "test"
/// numbers = ["1", "2", "3", "10"]
/// "#)?;
///
/// jail.set_env("config_name", "env-test");
///
/// jail.create_file("Config.json", r#"
/// {
/// "name": "json-test",
/// "debug": "yes"
/// }
/// "#)?;
///
/// let config: Config = Figment::new()
/// .merge(Toml::file("Config.toml"))
/// .merge(Env::prefixed("CONFIG_"))
/// .join(Json::file("Config.json"))
/// .extract_lossy()?;
///
/// assert_eq!(config, Config {
/// name: "env-test".into(),
/// numbers: vec![1, 2, 3, 10].into(),
/// debug: true
/// });
///
/// Ok(())
/// });
/// ```
pub fn extract_lossy<'a, T: Deserialize<'a>>(&self) -> Result<T> {
let value = self.merged()?;
T::deserialize(ConfiguredValueDe::<'_, LossyInterpreter>::from(self, &value))
}

/// Deserializes the value at the `key` path in the collected value into
Expand Down Expand Up @@ -511,8 +568,43 @@ impl Figment {
/// });
/// ```
pub fn extract_inner<'a, T: Deserialize<'a>>(&self, path: &str) -> Result<T> {
T::deserialize(ConfiguredValueDe::from(self, &self.find_value(path)?))
.map_err(|e| e.with_path(path))
let value = self.find_value(path)?;
let de = ConfiguredValueDe::<'_, DefaultInterpreter>::from(self, &value);
T::deserialize(de).map_err(|e| e.with_path(path))
}

/// As [`extract`](Figment::extract_lossy), but interpret numbers and
/// booleans more flexibly.
///
/// See [`Value::to_bool_lossy`] and [`Value::to_num_lossy`] for a full
/// explanation of the imputs accepted.
///
/// # Example
///
/// ```rust
/// use figment::{Figment, providers::{Format, Toml, Json}};
///
/// figment::Jail::expect_with(|jail| {
/// jail.create_file("Config.toml", r#"
/// numbers = ["1", "2", "3", "10"]
/// "#)?;
///
/// jail.create_file("Config.json", r#"{ "debug": true } "#)?;
///
/// let numbers: Vec<usize> = Figment::new()
/// .merge(Toml::file("Config.toml"))
/// .join(Json::file("Config.json"))
/// .extract_inner_lossy("numbers")?;
///
/// assert_eq!(numbers, vec![1, 2, 3, 10]);
///
/// Ok(())
/// });
/// ```
pub fn extract_inner_lossy<'a, T: Deserialize<'a>>(&self, path: &str) -> Result<T> {
let value = self.find_value(path)?;
let de = ConfiguredValueDe::<'_, LossyInterpreter>::from(self, &value);
T::deserialize(de).map_err(|e| e.with_path(path))
}

/// Returns an iterator over the metadata for all of the collected values in
Expand Down
79 changes: 69 additions & 10 deletions src/value/de.rs
Original file line number Diff line number Diff line change
@@ -1,28 +1,76 @@
use std::fmt;
use std::result;
use std::cell::Cell;
use std::marker::PhantomData;
use std::borrow::Cow;

use serde::Deserialize;
use serde::de::{self, Deserializer, IntoDeserializer};
use serde::de::{Visitor, SeqAccess, MapAccess, VariantAccess};
use serde::de::{self, Deserializer, IntoDeserializer, Visitor};
use serde::de::{SeqAccess, MapAccess, VariantAccess};

use crate::Figment;
use crate::error::{Error, Kind, Result};
use crate::value::{Value, Num, Empty, Dict, Tag};

pub struct ConfiguredValueDe<'c> {
pub trait Interpreter {
fn interpret_as_bool(v: &Value) -> Cow<'_, Value> {
Cow::Borrowed(v)
}

fn interpret_as_num(v: &Value) -> Cow<'_, Value> {
Cow::Borrowed(v)
}
}

pub struct DefaultInterpreter;
impl Interpreter for DefaultInterpreter { }

pub struct LossyInterpreter;
impl Interpreter for LossyInterpreter {
fn interpret_as_bool(v: &Value) -> Cow<'_, Value> {
v.to_bool_lossy()
.map(|b| Cow::Owned(Value::Bool(v.tag(), b)))
.unwrap_or(Cow::Borrowed(v))
}

fn interpret_as_num(v: &Value) -> Cow<'_, Value> {
v.to_num_lossy()
.map(|n| Cow::Owned(Value::Num(v.tag(), n)))
.unwrap_or(Cow::Borrowed(v))
}
}

pub struct ConfiguredValueDe<'c, I = DefaultInterpreter> {
pub config: &'c Figment,
pub value: &'c Value,
pub readable: Cell<bool>,
_phantom: PhantomData<I>
}

impl<'c> ConfiguredValueDe<'c> {
impl<'c, I: Interpreter> ConfiguredValueDe<'c, I> {
pub fn from(config: &'c Figment, value: &'c Value) -> Self {
Self { config, value, readable: Cell::from(true) }
Self { config, value, readable: Cell::from(true), _phantom: PhantomData }
}
}

/// Like [`serde::forward_to_deserialize_any`] but applies `$apply` to
/// `&self` first, then calls `deserialize_any()` on the returned value, and
/// finally maps any error produced using `$errmap`:
/// - $apply(&self).deserialize_any(visitor).map_err($errmap)
macro_rules! apply_then_forward_to_deserialize_any {
( $( $($f:ident),+ => |$this:pat| $apply:expr, $errmap:expr),* $(,)? ) => {
$(
$(
fn $f<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
let $this = &self;
$apply.deserialize_any(visitor).map_err($errmap)
}
)+
)*
}
}

impl<'de: 'c, 'c> Deserializer<'de> for ConfiguredValueDe<'c> {
impl<'de: 'c, 'c, I: Interpreter> Deserializer<'de> for ConfiguredValueDe<'c, I> {
type Error = Error;

fn deserialize_any<V>(self, v: V) -> Result<V::Value>
Expand Down Expand Up @@ -114,8 +162,19 @@ impl<'de: 'c, 'c> Deserializer<'de> for ConfiguredValueDe<'c> {
val
}

apply_then_forward_to_deserialize_any! {
deserialize_bool =>
|de| I::interpret_as_bool(de.value),
|e| e.retagged(de.value.tag()).resolved(de.config),
deserialize_u8, deserialize_u16, deserialize_u32, deserialize_u64,
deserialize_i8, deserialize_i16, deserialize_i32, deserialize_i64,
deserialize_f32, deserialize_f64 =>
|de| I::interpret_as_num(de.value),
|e| e.retagged(de.value.tag()).resolved(de.config),
}

serde::forward_to_deserialize_any! {
bool u8 u16 u32 u64 i8 i16 i32 i64 f32 f64 char str
char str
string seq bytes byte_buf map unit
ignored_any unit_struct tuple_struct tuple identifier
}
Expand Down Expand Up @@ -348,14 +407,14 @@ impl Value {
"___figment_value_id", "___figment_value_value"
];

fn deserialize_from<'de: 'c, 'c, V: de::Visitor<'de>>(
de: ConfiguredValueDe<'c>,
fn deserialize_from<'de: 'c, 'c, V: de::Visitor<'de>, I: Interpreter>(
de: ConfiguredValueDe<'c, I>,
visitor: V
) -> Result<V::Value> {
let mut map = Dict::new();
map.insert(Self::FIELDS[0].into(), de.value.tag().into());
map.insert(Self::FIELDS[1].into(), de.value.clone());
visitor.visit_map(MapDe::new(&map, |v| ConfiguredValueDe::from(de.config, v)))
visitor.visit_map(MapDe::new(&map, |v| ConfiguredValueDe::<'_, I>::from(de.config, v)))
}
}

Expand Down
25 changes: 13 additions & 12 deletions src/value/magic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ use std::path::{PathBuf, Path};

use serde::{Deserialize, Serialize, de};

use crate::{Error, value::{ConfiguredValueDe, MapDe, Tag}};
use crate::{Error, value::{ConfiguredValueDe, Interpreter, MapDe, Tag}};

/// Marker trait for "magic" values. Primarily for use with [`Either`].
pub trait Magic: for<'de> Deserialize<'de> {
Expand All @@ -16,8 +16,8 @@ pub trait Magic: for<'de> Deserialize<'de> {
/// The fields of the pseudo-structure. The last one should be the value.
#[doc(hidden)] const FIELDS: &'static [&'static str];

#[doc(hidden)] fn deserialize_from<'de: 'c, 'c, V: de::Visitor<'de>>(
de: ConfiguredValueDe<'c>,
#[doc(hidden)] fn deserialize_from<'de: 'c, 'c, V: de::Visitor<'de>, I: Interpreter>(
de: ConfiguredValueDe<'c, I>,
visitor: V
) -> Result<V::Value, Error>;
}
Expand Down Expand Up @@ -177,16 +177,17 @@ impl Magic for RelativePathBuf {
"___figment_relative_path"
];

fn deserialize_from<'de: 'c, 'c, V: de::Visitor<'de>>(
de: ConfiguredValueDe<'c>,
fn deserialize_from<'de: 'c, 'c, V: de::Visitor<'de>, I: Interpreter>(
de: ConfiguredValueDe<'c, I>,
visitor: V
) -> Result<V::Value, Error> {
// If we have this struct with a non-empty metadata_path, use it.
let config = de.config;
if let Some(d) = de.value.as_dict() {
if let Some(mpv) = d.get(Self::FIELDS[0]) {
if mpv.to_empty().is_none() {
return visitor.visit_map(MapDe::new(d, |v| ConfiguredValueDe::from(config, v)));
let map_de = MapDe::new(d, |v| ConfiguredValueDe::<I>::from(config, v));
return visitor.visit_map(map_de);
}
}
}
Expand All @@ -204,7 +205,7 @@ impl Magic for RelativePathBuf {
// If we have this struct with no metadata_path, still use the value.
let value = de.value.find_ref(Self::FIELDS[1]).unwrap_or(&de.value);
map.insert(Self::FIELDS[1].into(), value.clone());
visitor.visit_map(MapDe::new(&map, |v| ConfiguredValueDe::from(config, v)))
visitor.visit_map(MapDe::new(&map, |v| ConfiguredValueDe::<I>::from(config, v)))
}
}

Expand Down Expand Up @@ -406,7 +407,7 @@ impl RelativePathBuf {
// ) -> Result<V::Value, Error>{
// let mut map = crate::value::Map::new();
// map.insert(Self::FIELDS[0].into(), de.config.profile().to_string().into());
// visitor.visit_map(MapDe::new(&map, |v| ConfiguredValueDe::from(de.config, v)))
// visitor.visit_map(MapDe::new(&map, |v| ConfiguredValueDe::<I>::from(de.config, v)))
// }
// }
//
Expand Down Expand Up @@ -572,8 +573,8 @@ impl<T: for<'de> Deserialize<'de>> Magic for Tagged<T> {
"___figment_tagged_tag" , "___figment_tagged_value"
];

fn deserialize_from<'de: 'c, 'c, V: de::Visitor<'de>>(
de: ConfiguredValueDe<'c>,
fn deserialize_from<'de: 'c, 'c, V: de::Visitor<'de>, I: Interpreter>(
de: ConfiguredValueDe<'c, I>,
visitor: V
) -> Result<V::Value, Error>{
let config = de.config;
Expand All @@ -584,7 +585,7 @@ impl<T: for<'de> Deserialize<'de>> Magic for Tagged<T> {
if let Some(tagv) = dict.get(Self::FIELDS[0]) {
if let Ok(false) = tagv.deserialize::<Tag>().map(|t| t.is_default()) {
return visitor.visit_map(MapDe::new(dict, |v| {
ConfiguredValueDe::from(config, v)
ConfiguredValueDe::<I>::from(config, v)
}));
}
}
Expand All @@ -594,7 +595,7 @@ impl<T: for<'de> Deserialize<'de>> Magic for Tagged<T> {
let value = de.value.find_ref(Self::FIELDS[1]).unwrap_or(&de.value);
map.insert(Self::FIELDS[0].into(), de.value.tag().into());
map.insert(Self::FIELDS[1].into(), value.clone());
visitor.visit_map(MapDe::new(&map, |v| ConfiguredValueDe::from(config, v)))
visitor.visit_map(MapDe::new(&map, |v| ConfiguredValueDe::<I>::from(config, v)))
}
}

Expand Down
1 change: 1 addition & 0 deletions src/value/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ mod escape;
pub mod magic;

pub(crate) use {self::ser::*, self::de::*};

pub use tag::Tag;
pub use value::{Value, Map, Num, Dict, Empty};
pub use uncased::{Uncased, UncasedStr};
Loading
Loading