smart_config/
lib.rs

1//! `smart-config` – schema-driven layered configuration system with support of multiple configuration formats.
2//!
3//! # Overview
4//!
5//! The task solved by the library is merging configuration input from a variety of prioritized [sources](ConfigSource)
6//! (JSON and YAML files, env variables, command-line args etc.) and converting this input to strongly typed
7//! representation (i.e., config structs or enums). As with other config systems, config input follows the JSON object model
8//! (see [`Value`](value::Value)), with each value enriched with its [origin](value::ValueOrigin) (e.g., a path in a specific JSON file,
9//! or a specific env var). This allows attributing errors during deserialization.
10//!
11//! The defining feature of `smart-config` is its schema-driven design. Each config type has associated [metadata](ConfigMetadata)
12//! defined with the help of the [`DescribeConfig`] derive macro; deserialization is handled by the accompanying [`DeserializeConfig`] macro.
13//! Metadata includes a variety of info extracted from the config type:
14//!
15//! - [Parameter info](metadata::ParamMetadata): name (including aliases and renaming), help (extracted from doc comments),
16//!   type, [deserializer for the param](de::DeserializeParam) etc.
17//! - [Nested configurations](metadata::NestedConfigMetadata).
18//!
19//! Multiple configurations are collected into a global [`ConfigSchema`]. Each configuration is *mounted* at a specific path.
20//! E.g., if a large app has an HTTP server component, it may be mounted at `api.http`. Multiple config types may be mounted
21//! at the same path (e.g., flattened configs); conversely, a single config type may be mounted at multiple places.
22//! As a result, there doesn't need to be a god object uniting all configs in the app; they may be dynamically collected and deserialized
23//! inside relevant components.
24//!
25//! This information provides rich human-readable info about configs. It also assists when preprocessing and merging config inputs.
26//! For example, env vars are a flat string -> string map; with the help of a schema, it's possible to:
27//!
28//! - Correctly nest vars (e.g., transform the `API_HTTP_PORT` var into a `port` var inside `http` object inside `api` object)
29//! - Transform value types from strings to expected types.
30//!
31//! Preprocessing and merging config sources is encapsulated in [`ConfigRepository`].
32//!
33//! # TL;DR
34//!
35//! - Rich, self-documenting configuration schema.
36//! - Utilizes the schema to enrich configuration sources and intelligently merge them.
37//! - Doesn't require a god object uniting all configs in the app; they may be dynamically collected and deserialized
38//!   inside relevant components.
39//! - Supports lazy parsing for complex / multi-component apps (only the used configs are parsed; other configs are not required).
40//! - Supports multiple configuration formats and programmable source priorities (e.g., `base.yml` + overrides from the
41//!   `overrides/` dir in the alphabetic order + env vars).
42//! - Rich and complete deserialization errors including locations and value origins.
43//! - [Built-in support for secret params](de#secrets).
44//!
45//! # Crate features
46//!
47//! ## `primitive-types`
48//!
49//! *(Off by default)*
50//!
51//! Implements deserialization for basic Ethereum types like [`H256`](primitive_types::H256) (32-byte hash)
52//! and [`U256`](primitive_types::U256) (256-bit unsigned integer).
53//!
54//! ## `alloy`
55//!
56//! *(Off by default)*
57//!
58//! Implements deserialization for basic alloy primitive types like [`B256`](alloy::primitives::B256) (32-byte hash)
59//! and [`U256`](alloy::primitives::U256) (256-bit unsigned integer).
60//!
61//! # Examples
62//!
63//! ## Basic workflow
64//!
65//! ```
66//! use smart_config::{
67//!     config, ConfigSchema, ConfigRepository, DescribeConfig, DeserializeConfig, Yaml, Environment,
68//! };
69//!
70//! #[derive(Debug, DescribeConfig, DeserializeConfig)]
71//! pub struct TestConfig {
72//!     pub port: u16,
73//!     #[config(default_t = "test".into())]
74//!     pub name: String,
75//!     #[config(default_t = true)]
76//!     pub tracing: bool,
77//! }
78//!
79//! let schema = ConfigSchema::new(&TestConfig::DESCRIPTION, "test");
80//! // Assume we use two config sources: a YAML file and env vars,
81//! // the latter having higher priority.
82//! let yaml = r"
83//! test:
84//!   port: 4000
85//!   name: app
86//! ";
87//! let yaml = Yaml::new("test.yml", serde_yaml::from_str(yaml)?)?;
88//! let env = Environment::from_iter("APP_", [("APP_TEST_PORT", "8000")]);
89//! // Add both sources to a repo.
90//! let repo = ConfigRepository::new(&schema).with(yaml).with(env);
91//! // Get the parser for the config.
92//! let parser = repo.single::<TestConfig>()?;
93//! let config = parser.parse()?;
94//! assert_eq!(config.port, 8_000); // from the env var
95//! assert_eq!(config.name, "app"); // from YAML
96//! assert!(config.tracing); // from the default value
97//! # anyhow::Ok(())
98//! ```
99//!
100//! ## Declaring type as well-known
101//!
102//! ```
103//! use std::collections::HashMap;
104//! use smart_config::{
105//!     de::{Serde, WellKnown, WellKnownOption}, metadata::BasicTypes,
106//!     DescribeConfig, DeserializeConfig,
107//! };
108//!
109//! #[derive(Debug, serde::Serialize, serde::Deserialize)]
110//! enum CustomEnum {
111//!     First,
112//!     Second,
113//! }
114//!
115//! impl WellKnown for CustomEnum {
116//!     // signals that the type should be deserialized via `serde`
117//!     // and the expected input is a string
118//!     type Deserializer = Serde![str];
119//!     const DE: Self::Deserializer = Serde![str];
120//! }
121//!
122//! // Signals that the type can be used with an `Option<_>`
123//! impl WellKnownOption for CustomEnum {}
124//!
125//! // Then, the type can be used in configs basically everywhere:
126//! #[derive(Debug, DescribeConfig, DeserializeConfig)]
127//! struct TestConfig {
128//!     value: CustomEnum,
129//!     optional: Option<CustomEnum>,
130//!     repeated: Vec<CustomEnum>,
131//!     map: HashMap<String, CustomEnum>,
132//! }
133//! ```
134
135// Documentation settings
136#![doc(html_root_url = "https://docs.rs/smart-config/0.4.0-pre.1")] // x-release-please-version
137#![cfg_attr(docsrs, feature(doc_cfg))]
138// Linter settings
139#![warn(missing_docs)]
140
141/// Derives the [`DescribeConfig`](trait@DescribeConfig) trait for a type.
142///
143/// This macro supports both structs and enums. It is conceptually similar to `Deserialize` macro from `serde`.
144/// Macro behavior can be configured with `#[config(_)]` attributes. Multiple `#[config(_)]` attributes
145/// on a single item are supported.
146///
147/// Each field in the struct / each enum variant is considered a configuration param (by default),
148/// or a sub-config (if `#[config(nest)]` or `#[config(flatten)]` is present for the field).
149///
150/// # Container attributes
151///
152/// ## `validate`
153///
154/// **Type:** One of the following:
155///
156/// - Expression evaluating to a [`Validate`](validation::Validate) implementation (e.g., a [`Range`](std::ops::Range); see the `Validate` docs
157///   for implementations). An optional human-readable string validation description may be provided delimited by the comma (e.g., to make the description
158///   more domain-specific).
159/// - Pointer to a function with the `fn(&_) -> Result<(), ErrorWithOrigin>` signature and the validation description separated by a comma.
160/// - Pointer to a function with the `fn(&_) -> bool` signature and the validation description separated by a comma. Validation fails
161///   if the function returns `false`.
162///
163/// See the examples in the [`validation`] module.
164///
165/// Specifies a post-deserialization validation for the config. This is useful to check invariants involving multiple params.
166/// Multiple validations are supported by specifying the attribute multiple times.
167///
168/// ## `tag`
169///
170/// **Type:** string
171///
172/// Specifies the param name holding the enum tag, similar to the corresponding attribute in `serde`.
173/// Unlike `serde`, this attribute is *required* for enums; this is to ensure that source merging is well-defined.
174///
175/// ## `rename_all`
176///
177/// **Type:** string; one of `lowercase`, `UPPERCASE`, `camelCase`, `snake_case`, `SCREAMING_SNAKE_CASE`,
178/// `kebab-case`, `SCREAMING-KEBAB-CASE`
179///
180/// Renames all variants in an enum config according to the provided transform. Unlike in `serde`, this attribute
181/// *only* works on enum variants. Params / sub-configs are always expected to have `snake_case` naming.
182///
183/// Caveats:
184///
185/// - `rename_all` assumes that original variant names are in `PascalCase` (i.e., follow Rust naming conventions).
186/// - `rename_all` requires original variant names to consist of ASCII chars.
187/// - Each letter of capitalized acronyms (e.g., "HTTP" in `HTTPServer`) is treated as a separate word.
188///   E.g., `rename_all = "snake_case"` will rename `HTTPServer` to `h_t_t_p_server`.
189///   Note that [it is recommended][clippy-acronyms] to not capitalize acronyms (i.e., use `HttpServer`).
190/// - No spacing is inserted before numbers or other non-letter chars. E.g., `rename_all = "snake_case"`
191///   will rename `Status500` to `status500`, not to `status_500`.
192///
193/// [clippy-acronyms]: https://rust-lang.github.io/rust-clippy/master/index.html#/upper_case_acronyms
194///
195/// ## `derive(Default)`
196///
197/// Derives `Default` according to the default values of params (+ the default variant for enum configs).
198/// To work, all params must have a default value specified.
199///
200/// # Variant attributes
201///
202/// ## `rename`, `alias`
203///
204/// **Type:** string
205///
206/// Have the same meaning as in `serde`; i.e. allow to rename / specify additional names for the tag(s)
207/// corresponding to the variant. `alias` can be specified multiple times.
208///
209/// ## `default`
210///
211/// If specified, marks the variant as default – one which will be used if the tag param is not set in the input.
212/// At most one variant can be marked as default.
213///
214/// # Field attributes
215///
216/// ## `rename`, `alias`
217///
218/// **Type:** string
219///
220/// Have the same meaning as in `serde`; i.e. allow to rename / specify additional names for the param or a nested config.
221/// Names are [validated](#validations) in compile time.
222///
223/// In addition to simple names, *path* aliases are supported as well. A path alias starts with `.` and consists of dot-separated segments,
224/// e.g. `.experimental.value` or `..value`. The paths are resolved relative to the config prefix. As in Python, more than one dot
225/// at the start of the path signals that the path is relative to the parent(s) of the config.
226///
227/// - `alias = ".experimental.value"` with config prefix `test` resolves to the absolute path `test.experimental.value`.
228/// - `alias = "..value"` with config prefix `test.experimental` resolves to the absolute path `test.value`.
229///
230/// If an alias requires more parents than is present in the config prefix, the alias is not applicable.
231/// (E.g., `alias = "...value"` with config prefix `test`.)
232///
233/// Path aliases are somewhat difficult to reason about, so avoid using them unless necessary.
234///
235/// ## `deprecated`
236///
237/// **Type:** string
238///
239/// Similar to `alias`, with the difference that the alias is marked as deprecated in the schema docs,
240/// and its usages are logged on the `WARN` level.
241///
242/// ## `default`
243///
244/// **Type:** path to function (optional)
245///
246/// Has the same meaning as in `serde`, i.e. allows to specify a constructor of the default value for the param.
247/// Without a value, [`Default`] is used for this purpose. Unlike `serde`, the path shouldn't be quoted.
248///
249/// ## `default_t`
250///
251/// **Type:** expression with param type
252///
253/// Allows to specify the default typed value for the param. The provided expression doesn't need to be constant.
254///
255/// ## `example`
256///
257/// **Type:** expression with field type
258///
259/// Allows to specify the example value for the param. The example value can be specified together with the `default` / `default_t`
260/// attribute. In this case, the example value can be more "complex" than the default, to better illustrate how the configuration works.
261///
262/// ## `fallback`
263///
264/// **Type:** constant expression evaluating to `&'static dyn `[`FallbackSource`](fallback::FallbackSource)
265///
266/// Allows to provide a fallback source for the param. See the [`fallback`] module docs for the discussion of fallbacks
267/// and intended use cases.
268///
269/// ## `with`
270///
271/// **Type:** const expression implementing [`DeserializeParam`]
272///
273/// Allows changing the param deserializer. See [`de`] module docs for the overview of available deserializers.
274/// For `Option`s, `with` refers to the *internal* type deserializer; it will be wrapped into an [`Optional`](crate::de::Optional) automatically.
275///
276/// Note that there is an alternative: implementing [`WellKnown`](de::WellKnown) for the param type.
277///
278/// ## `nest`
279///
280/// If specified, the field is treated as a nested sub-config rather than a param. Correspondingly, its type must
281/// implement `DescribeConfig`, or wrap such a type in an `Option`.
282///
283/// ## `flatten`
284///
285/// If specified, the field is treated as a *flattened* sub-config rather than a param. Unlike `nest`, its params
286/// will be added to the containing config instead of a separate object. The sub-config type must implement `DescribeConfig`.
287///
288/// ## `validate`
289///
290/// Has same semantics as [config validations](#validate), but applies to a specific config parameter.
291///
292/// ## `deserialize_if`
293///
294/// **Type:** same as [config validations](#validate)
295///
296/// Filters an `Option`al value. This is useful to coerce semantically invalid values (e.g., empty strings for URLs)
297/// to `None` in the case [automated null coercion](crate::de::Optional#encoding-nulls) doesn't apply.
298/// See the [`validation`] module for examples of usage.
299///
300/// # Validations
301///
302/// The following validations are performed by the macro in compile time:
303///
304/// - Param / sub-config names and aliases must be non-empty, consist of lowercase ASCII alphanumeric chars or underscore
305///   and not start with a digit (i.e., follow the `[a-z_][a-z0-9_]*` regex).
306/// - Param names / aliases cannot coincide with nested config names.
307///
308/// [`DeserializeParam`]: de::DeserializeParam
309///
310/// # Examples
311///
312/// ```
313/// # use std::{collections::HashSet, num::NonZeroUsize, time::Duration};
314/// # use smart_config::{DescribeConfig, DeserializeConfig};
315/// use smart_config::metadata::TimeUnit;
316///
317/// #[derive(DescribeConfig, DeserializeConfig)]
318/// struct TestConfig {
319///     /// Doc comments are parsed as a description.
320///     #[config(default_t = 3)]
321///     int: u32,
322///     #[config(default)] // multiple `config` attrs are supported
323///     #[config(rename = "str", alias = "string")]
324///     renamed: String,
325///     /// Nested sub-config. E.g., the tag will be read from path `nested.version`.
326///     #[config(nest)]
327///     nested: NestedConfig,
328///     /// Flattened sub-config. E.g., `array` param will be read from `array`, not `flat.array`.
329///     #[config(flatten)]
330///     flat: FlattenedConfig,
331/// }
332///
333/// #[derive(DescribeConfig, DeserializeConfig)]
334/// #[config(tag = "version", rename_all = "snake_case", derive(Default))]
335/// enum NestedConfig {
336///     #[config(default)]
337///     V0,
338///     #[config(alias = "latest")]
339///     V1 {
340///         /// Param with a custom deserializer. In this case, it will deserialize
341///         /// a duration from a number with milliseconds unit of measurement.
342///         #[config(default_t = Duration::from_millis(50), with = TimeUnit::Millis)]
343///         latency_ms: Duration,
344///         /// `Vec`s, sets and other containers are supported out of the box.
345///         set: HashSet<NonZeroUsize>,
346///     },
347/// }
348///
349/// #[derive(DescribeConfig, DeserializeConfig)]
350/// struct FlattenedConfig {
351///     #[config(default = FlattenedConfig::default_array)]
352///     array: [f32; 2],
353/// }
354///
355/// impl FlattenedConfig {
356///     const fn default_array() -> [f32; 2] { [1.0, 2.0] }
357/// }
358/// ```
359pub use smart_config_derive::DescribeConfig;
360/// Derives the [`DeserializeConfig`](trait@DeserializeConfig) trait for a type.
361///
362/// This macro is intended to be used together with [`DescribeConfig`](macro@DescribeConfig). It reuses
363/// the same attributes, so see `DescribeConfig` docs for details and examples of usage.
364pub use smart_config_derive::DeserializeConfig;
365/// Derives the [`ExampleConfig`](trait@ExampleConfig) trait for a type.
366///
367/// This macro is intended to be used together with [`DescribeConfig`](macro@DescribeConfig); it reuses
368/// the same attributes. Specifically, for each config field, the default value is assigned from the following sources
369/// in the decreasing priority order:
370///
371/// 1. `example`
372/// 2. `default` / `default_t`, including implied ones for `Option`al fields
373/// 3. From [`ExampleConfig`](trait@ExampleConfig) implementation (only for nested / flattened configs)
374///
375/// # Examples
376///
377/// ```
378/// # use std::collections::HashSet;
379/// # use smart_config::{DescribeConfig, ExampleConfig, SerializerOptions};
380/// #[derive(DescribeConfig, ExampleConfig)]
381/// struct TestConfig {
382///     /// Required param that still has an example value.
383///     #[config(example = 42)]
384///     required: u32,
385///     optional: Option<String>,
386///     #[config(default_t = true)]
387///     with_default: bool,
388///     #[config(default, example = vec![5, 8])]
389///     values: Vec<u32>,
390///     #[config(nest)]
391///     nested: NestedConfig,
392/// }
393///
394/// #[derive(DescribeConfig, ExampleConfig)]
395/// struct NestedConfig {
396///     #[config(default, example = ["eth_call".into()].into())]
397///     methods: HashSet<String>,
398/// }
399///
400/// let example: TestConfig = TestConfig::example_config();
401/// let json = SerializerOptions::default().serialize(&example);
402/// assert_eq!(
403///     serde_json::Value::from(json),
404///     serde_json::json!({
405///         "required": 42,
406///         "optional": null,
407///         "with_default": true,
408///         "values": [5, 8],
409///         "nested": {
410///             "methods": ["eth_call"],
411///         },
412///     })
413/// );
414/// ```
415pub use smart_config_derive::ExampleConfig;
416
417pub use self::{
418    de::DeserializeConfig,
419    error::{DeserializeConfigError, ErrorWithOrigin, ParseError, ParseErrorCategory, ParseErrors},
420    schema::{ConfigMut, ConfigRef, ConfigSchema},
421    source::{
422        ConfigParser, ConfigRepository, ConfigSource, ConfigSourceKind, ConfigSources, Environment,
423        Flat, Hierarchical, Json, Prefixed, SerializerOptions, SourceInfo, Yaml,
424    },
425    types::{ByteSize, EtherAmount},
426};
427use self::{metadata::ConfigMetadata, visit::VisitConfig};
428
429pub mod de;
430mod error;
431pub mod fallback;
432pub mod metadata;
433mod schema;
434mod source;
435pub mod testing;
436#[cfg(test)]
437mod testonly;
438mod types;
439mod utils;
440pub mod validation;
441pub mod value;
442pub mod visit;
443
444/// Describes a configuration (i.e., a group of related parameters).
445pub trait DescribeConfig: 'static + VisitConfig {
446    /// Provides the config description.
447    const DESCRIPTION: ConfigMetadata;
448}
449
450/// Provides an example for this configuration. The produced config can be used in tests etc.
451///
452/// For struct configs, this can be derived via [the corresponding proc macro](macro@ExampleConfig).
453pub trait ExampleConfig {
454    /// Constructs an example configuration.
455    fn example_config() -> Self;
456}
457
458impl<T: ExampleConfig> ExampleConfig for Option<T> {
459    fn example_config() -> Self {
460        Some(T::example_config())
461    }
462}
463
464#[cfg(doctest)]
465doc_comment::doctest!("../README.md");