Skip to content

Commit

Permalink
Add lossy extraction.
Browse files Browse the repository at this point in the history
This commit adds `Figment::extract_{inner_}lossy()`, variants of the
existing methods that convert string representations of booleans and
integers into their boolean and integer forms. The original string form
is lost and is not directly recoverable.

Methods that performs the same conversion are added to `Value` types:

  * `Value::to_num_lossy()`
  * `Value::to_bool_lossy()`
  * `Num::to_u128_lossy()`
  * `Num::from_str()`

Closes #97.

Co-authored-by: Sergio Benitez <sb@sergio.bz>
  • Loading branch information
nmathewson and SergioBenitez committed Apr 18, 2024
1 parent d2a31e8 commit 2c83ffa
Show file tree
Hide file tree
Showing 6 changed files with 361 additions and 28 deletions.
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

0 comments on commit 2c83ffa

Please sign in to comment.