smart_config/de/
mod.rs

1//! Configuration deserialization logic.
2//!
3//! # How it works
4//!
5//! [`DeserializeConfig`] derive macro visits all config parameters invoking associated [`DeserializeParam`]
6//! implementations. Unlike `serde` deserialization, deserialization does not stop early on error (we want to get
7//! errors for all params). Nested / flattened configs do not use `serde` either for a couple of reasons:
8//!
9//! - To reach all params regardless of encountered errors as mentioned above
10//! - `serde` sometimes collects params in intermediate containers (e.g., in structs with `#[serde(flatten)]`
11//!   or in tagged enums), which leads to param deserialization potentially getting broken in unpredictable ways.
12//!
13//! So, each config param is deserialized in isolation from an optional [`Value`] [`WithOrigin`]
14//! encapsulated in [`DeserializeContext`].
15//!
16//! # Deserializers
17//!
18//! The default deserializer is extracted from the param type with the help of [`WellKnown`] and [`WellKnownOption`] traits.
19//! If you have a custom type defined locally which you want to use in configs, the easiest solution
20//! would be to implement `WellKnown` (+ maybe `WellKnownOption`) for it.
21//! Alternatively, it's possible to specify a custom deserializer using `#[config(with = _)]` attribute.
22//!
23//! ## Universal deserializers
24//!
25//! [`Serde`](struct@Serde) (usually instantiated via [the eponymous macro](macro@Serde)) can deserialize
26//! any param implementing [`serde::Deserialize`]. An important caveat is that these deserializers require
27//! the input `Value` to be present; otherwise, they'll fail with a "missing value" error. As such,
28//! for [`Option`]al types, it's necessary to wrap a deserializer in the [`Optional`] decorator.
29//!
30//! ## Durations and byte sizes
31//!
32//! [`Duration`]s and [`ByteSize`]s can be deserialized in two ways:
33//!
34//! - By default, they are deserialized from an integer + unit either encapsulated in a string like "200ms" or
35//!   in a single-key object like `{ "mb": 4 }`. See [`WithUnit`] for more details.
36//! - Alternatively, [`TimeUnit`](crate::metadata::TimeUnit) and [`SizeUnit`](crate::metadata::SizeUnit) can be used
37//!   on `Duration`s and `ByteSize`s, respectively.
38//!
39//! ## Secrets
40//!
41//! A param is secret iff it uses a [`Secret`] deserializer (perhaps, with decorators on top, like
42//! [`Optional`] / [`WithDefault`]). Secret params must be deserializable from a string; this is because
43//! strings are the only type of secret values currently supported.
44//!
45//! Secret values are wrapped in opaque, zero-on-drop wrappers during source preprocessing so that
46//! they do not get accidentally exposed via debug logs etc. See [`ConfigRepository`](crate::ConfigRepository)
47//! for details.
48//!
49//! [`Duration`]: std::time::Duration
50//! [`ByteSize`]: crate::ByteSize
51
52use std::any;
53
54use serde::de::Error as DeError;
55
56use self::deserializer::ValueDeserializer;
57pub use self::{
58    deserializer::DeserializerOptions,
59    macros::Serde,
60    param::{
61        CustomKnownOption, DeserializeParam, Optional, OrString, Qualified, Serde, WellKnown,
62        WellKnownOption, WithDefault,
63    },
64    repeated::{Delimited, Entries, NamedEntries, Repeated, ToEntries},
65    secret::{FromSecretString, Secret},
66    units::WithUnit,
67};
68use crate::{
69    DescribeConfig, DeserializeConfigError, ParseError, ParseErrorCategory, ParseErrors,
70    error::{ErrorWithOrigin, LocationInConfig, LowLevelError},
71    metadata::{BasicTypes, ConfigMetadata, ParamMetadata},
72    value::{Pointer, StrValue, Value, ValueOrigin, WithOrigin},
73};
74
75#[doc(hidden)]
76pub mod _private;
77#[cfg(feature = "alloy")]
78mod alloy_impl;
79mod deserializer;
80mod macros;
81mod param;
82#[cfg(feature = "primitive-types")]
83mod primitive_types_impl;
84mod repeated;
85mod secret;
86#[cfg(test)]
87mod tests;
88mod units;
89
90/// Context for deserializing a configuration.
91#[derive(Debug)]
92pub struct DeserializeContext<'a> {
93    de_options: &'a DeserializerOptions,
94    root_value: &'a WithOrigin,
95    path: String,
96    patched_current_value: Option<&'a WithOrigin>,
97    current_config: &'static ConfigMetadata,
98    location_in_config: Option<LocationInConfig>,
99    errors: &'a mut ParseErrors,
100}
101
102impl<'a> DeserializeContext<'a> {
103    pub(crate) fn new(
104        de_options: &'a DeserializerOptions,
105        root_value: &'a WithOrigin,
106        path: String,
107        current_config: &'static ConfigMetadata,
108        errors: &'a mut ParseErrors,
109    ) -> Self {
110        Self {
111            de_options,
112            root_value,
113            path,
114            patched_current_value: None,
115            current_config,
116            location_in_config: None,
117            errors,
118        }
119    }
120
121    fn child(
122        &mut self,
123        path: &str,
124        location_in_config: Option<LocationInConfig>,
125    ) -> DeserializeContext<'_> {
126        DeserializeContext {
127            de_options: self.de_options,
128            root_value: self.root_value,
129            path: Pointer(&self.path).join(path),
130            patched_current_value: self.patched_current_value.and_then(|val| {
131                if path.is_empty() {
132                    Some(val)
133                } else if let Value::Object(object) = &val.inner {
134                    object.get(path)
135                } else {
136                    None
137                }
138            }),
139            current_config: self.current_config,
140            location_in_config,
141            errors: self.errors,
142        }
143    }
144
145    /// Mutably borrows this context with a shorter lifetime.
146    pub fn borrow(&mut self) -> DeserializeContext<'_> {
147        DeserializeContext {
148            de_options: self.de_options,
149            root_value: self.root_value,
150            path: self.path.clone(),
151            patched_current_value: self.patched_current_value,
152            current_config: self.current_config,
153            location_in_config: self.location_in_config,
154            errors: self.errors,
155        }
156    }
157
158    /// Allows to pretend that `current_value` is as supplied.
159    fn patched<'s>(&'s mut self, current_value: &'s WithOrigin) -> DeserializeContext<'s> {
160        DeserializeContext {
161            de_options: self.de_options,
162            root_value: self.root_value,
163            path: self.path.clone(),
164            patched_current_value: Some(current_value),
165            current_config: self.current_config,
166            location_in_config: self.location_in_config,
167            errors: self.errors,
168        }
169    }
170
171    pub(crate) fn current_value(&self) -> Option<&'a WithOrigin> {
172        self.patched_current_value
173            .or_else(|| self.root_value.get(Pointer(&self.path)))
174    }
175
176    /// Returns a `serde` deserializer for the current value.
177    ///
178    /// # Errors
179    ///
180    /// Returns an error if the current value is missing.
181    pub fn current_value_deserializer(
182        &self,
183        name: &'static str,
184    ) -> Result<ValueDeserializer<'a>, ErrorWithOrigin> {
185        if let Some(value) = self.current_value() {
186            Ok(ValueDeserializer::new(value, self.de_options))
187        } else {
188            Err(DeError::missing_field(name))
189        }
190    }
191
192    /// Returns context for a nested configuration.
193    fn for_nested_config(&mut self, index: usize) -> DeserializeContext<'_> {
194        let nested_meta = self
195            .current_config
196            .nested_configs
197            .get(index)
198            .unwrap_or_else(|| {
199                panic!(
200                    "Internal error: called `for_nested_config()` with missing config index {index}"
201                )
202            });
203        let path = nested_meta.name;
204        DeserializeContext {
205            current_config: nested_meta.meta,
206            ..self.child(path, None)
207        }
208    }
209
210    fn for_param(&mut self, index: usize) -> (DeserializeContext<'_>, &'static ParamMetadata) {
211        let param = self.current_config.params.get(index).unwrap_or_else(|| {
212            panic!("Internal error: called `for_param()` with missing param index {index}")
213        });
214        (
215            self.child(param.name, Some(LocationInConfig::Param(index))),
216            param,
217        )
218    }
219
220    /// Pushes a deserialization error into the context.
221    pub fn push_error(&mut self, err: ErrorWithOrigin) {
222        self.push_generic_error(err, None);
223    }
224
225    #[cold]
226    fn push_generic_error(&mut self, err: ErrorWithOrigin, validation: Option<String>) {
227        let (inner, category) = match err.inner {
228            LowLevelError::Json { err, category } => (err, category),
229            LowLevelError::InvalidArray
230            | LowLevelError::InvalidObject
231            | LowLevelError::Validation => return,
232        };
233
234        let mut origin = err.origin;
235        if matches!(origin.as_ref(), ValueOrigin::Unknown) {
236            if let Some(val) = self.current_value() {
237                origin = val.origin.clone();
238            }
239        }
240
241        self.errors.push(ParseError {
242            inner,
243            category,
244            path: self.path.clone(),
245            origin,
246            config: self.current_config,
247            location_in_config: self.location_in_config,
248            validation,
249        });
250    }
251
252    #[tracing::instrument(
253        level = "trace",
254        skip_all,
255        fields(path = self.path, config = ?self.current_config.ty)
256    )]
257    pub(crate) fn deserialize_any_config(
258        mut self,
259    ) -> Result<Box<dyn any::Any>, DeserializeConfigError> {
260        // It is technically possible to coerce a value to an object here, but this would make merging sources not obvious:
261        // should a config specified as a string override / be overridden atomically? (Probably not, but if so, it needs to be coerced to an object
262        // before the merge, potentially recursively.)
263
264        if let Some(val) = self.current_value() {
265            if !matches!(&val.inner, Value::Object(_)) {
266                self.push_error(val.invalid_type("config object"));
267                return Err(DeserializeConfigError::new());
268            }
269        }
270        let config = (self.current_config.deserializer)(self.borrow())?;
271
272        let mut has_errors = false;
273        for &validation in self.current_config.validations {
274            let _span = tracing::trace_span!("validation", %validation).entered();
275            if let Err(err) = validation.validate(config.as_ref()) {
276                tracing::info!(%validation, origin = %err.origin, "config validation failed: {}", err.inner);
277                self.push_generic_error(err, Some(validation.to_string()));
278                has_errors = true;
279            }
280        }
281
282        if has_errors {
283            Err(DeserializeConfigError::new())
284        } else {
285            Ok(config)
286        }
287    }
288
289    /// Caller is responsible to downcast the config to the correct type.
290    pub(crate) fn deserialize_config<C: 'static>(self) -> Result<C, DeserializeConfigError> {
291        Ok(*self
292            .deserialize_any_config()?
293            .downcast::<C>()
294            .expect("Internal error: config deserializer output has wrong type"))
295    }
296
297    pub(crate) fn deserialize_any_config_opt(
298        mut self,
299    ) -> Result<Option<Box<dyn any::Any>>, DeserializeConfigError> {
300        if self.current_value().is_none() {
301            return Ok(None);
302        }
303
304        let error_count = self.errors.len();
305        self.borrow()
306            .deserialize_any_config()
307            .map(Some)
308            .or_else(|err| {
309                let only_missing_field_errors = self
310                    .errors
311                    .iter()
312                    .skip(error_count)
313                    .all(|err| matches!(err.category, ParseErrorCategory::MissingField));
314                if only_missing_field_errors {
315                    tracing::trace!(
316                        "optional config misses required params and no other errors; coercing it to `None`"
317                    );
318                    self.errors.truncate(error_count);
319                    Ok(None)
320                } else {
321                    Err(err)
322                }
323            })
324    }
325
326    pub(crate) fn deserialize_config_opt<C: 'static>(
327        self,
328    ) -> Result<Option<C>, DeserializeConfigError> {
329        let config = self.deserialize_any_config_opt()?.map(|boxed| {
330            *boxed
331                .downcast::<C>()
332                .expect("Internal error: config deserializer output has wrong type")
333        });
334        Ok(config)
335    }
336}
337
338/// Methods used in proc macros. Not a part of public API.
339#[doc(hidden)]
340impl DeserializeContext<'_> {
341    pub fn deserialize_nested_config<C: DeserializeConfig>(
342        &mut self,
343        index: usize,
344        default_fn: Option<fn() -> C>,
345    ) -> Result<C, DeserializeConfigError> {
346        let child_ctx = self.for_nested_config(index);
347        if child_ctx.current_value().is_none() {
348            if let Some(default) = default_fn {
349                return Ok(default());
350            }
351        }
352        child_ctx.deserialize_config()
353    }
354
355    pub fn deserialize_nested_config_opt<C: DeserializeConfig>(
356        &mut self,
357        index: usize,
358    ) -> Result<Option<C>, DeserializeConfigError> {
359        self.for_nested_config(index).deserialize_config_opt()
360    }
361
362    #[tracing::instrument(
363        level = "trace",
364        name = "deserialize_param",
365        skip_all,
366        fields(path = self.path, config = ?self.current_config.ty, param)
367    )]
368    pub(crate) fn deserialize_any_param(
369        &mut self,
370        index: usize,
371    ) -> Result<Box<dyn any::Any>, DeserializeConfigError> {
372        let (mut child_ctx, param) = self.for_param(index);
373        tracing::Span::current().record("param", param.rust_field_name);
374
375        // Coerce value to the expected type.
376        let maybe_coerced = child_ctx
377            .current_value()
378            .and_then(|val| val.coerce_value_type(param.expecting));
379        let mut child_ctx = if let Some(coerced) = &maybe_coerced {
380            child_ctx.patched(coerced)
381        } else {
382            child_ctx
383        };
384        tracing::trace!(
385            deserializer = ?param.deserializer,
386            value = ?child_ctx.current_value(),
387            "deserializing param"
388        );
389
390        match param
391            .deserializer
392            .deserialize_param(child_ctx.borrow(), param)
393        {
394            Ok(param) => Ok(param),
395            Err(err) => {
396                tracing::info!(origin = %err.origin, "deserialization failed: {}", err.inner);
397                child_ctx.push_error(err);
398                Err(DeserializeConfigError::new())
399            }
400        }
401    }
402
403    pub fn deserialize_param<T: 'static>(
404        &mut self,
405        index: usize,
406    ) -> Result<T, DeserializeConfigError> {
407        self.deserialize_any_param(index).map(|val| {
408            *val.downcast()
409                .expect("Internal error: deserializer output has wrong type")
410        })
411    }
412}
413
414impl WithOrigin {
415    #[tracing::instrument(level = "trace", skip(self))]
416    fn coerce_value_type(&self, expecting: BasicTypes) -> Option<Self> {
417        let Value::String(StrValue::Plain(str)) = &self.inner else {
418            return None; // we only know how to coerce strings so far
419        };
420
421        // Coerce string values representing `null`, provided that the original deserializer doesn't expect a string
422        // (if it does, there would be an ambiguity doing this).
423        if !expecting.contains(BasicTypes::STRING) && (str.is_empty() || str == "null") {
424            return Some(Self::new(Value::Null, self.origin.clone()));
425        }
426
427        // Attempt to transform the type to the expected type
428        match expecting {
429            // We intentionally use exact comparisons; if a type supports multiple primitive representations,
430            // we do nothing.
431            BasicTypes::BOOL => match str.parse::<bool>() {
432                Ok(bool_value) => {
433                    return Some(Self::new(bool_value.into(), self.origin.clone()));
434                }
435                Err(err) => {
436                    tracing::info!(%expecting, "failed coercing value: {err}");
437                }
438            },
439            BasicTypes::INTEGER | BasicTypes::FLOAT => match str.parse::<serde_json::Number>() {
440                Ok(number) => {
441                    return Some(Self::new(number.into(), self.origin.clone()));
442                }
443                Err(err) => {
444                    tracing::info!(%expecting, "failed coercing value: {err}");
445                }
446            },
447            _ => { /* do nothing */ }
448        }
449        None
450    }
451}
452
453/// Deserializes this configuration from the provided context.
454pub trait DeserializeConfig: DescribeConfig + Sized {
455    /// Performs deserialization.
456    ///
457    /// # Errors
458    ///
459    /// Returns an error marker if deserialization fails for at least one of recursively contained params.
460    /// Error info should is contained in the context.
461    fn deserialize_config(ctx: DeserializeContext<'_>) -> Result<Self, DeserializeConfigError>;
462}