smart_config/
lib.rs

1//! `smart-config` – schema-driven layered configuration system with support of multiple configuration formats.
2//!
3//! # Overview
4//!
5//! *See [extra docs](_docs) for deep dive into library features.*
6//!
7//! The task solved by the library is merging configuration input from a variety of prioritized [sources](ConfigSource)
8//! (JSON and YAML files, env variables, command-line args etc.) and converting this input to strongly typed
9//! representation (i.e., config structs or enums). As with other config systems, config input follows the JSON object model
10//! (see [`Value`](value::Value)), with each value enriched with its [origin](value::ValueOrigin) (e.g., a path in a specific JSON file,
11//! or a specific env var). This allows attributing errors during deserialization.
12//!
13//! The defining feature of `smart-config` is its schema-driven design. Each config type has associated [metadata](ConfigMetadata)
14//! defined with the help of the [`DescribeConfig`](macro@DescribeConfig) derive macro; deserialization is handled
15//! by the accompanying [`DeserializeConfig`](macro@DeserializeConfig) macro.
16//! Metadata includes a variety of info extracted from the config type:
17//!
18//! - [Parameter info](metadata::ParamMetadata): name (including aliases and renaming), help (extracted from doc comments),
19//!   type, [deserializer for the param](de::DeserializeParam) etc.
20//! - [Nested configurations](metadata::NestedConfigMetadata).
21//!
22//! Multiple configurations are collected into a global [`ConfigSchema`]. Each configuration is *mounted* at a specific path.
23//! E.g., if a large app has an HTTP server component, it may be mounted at `api.http`. Multiple config types may be mounted
24//! at the same path (e.g., flattened configs); conversely, a single config type may be mounted at multiple places.
25//! 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
26//! inside relevant components.
27//!
28//! This information provides rich human-readable info about configs. It also assists when preprocessing and merging config inputs.
29//! For example, env vars are a flat string -> string map; with the help of a schema, it's possible to:
30//!
31//! - Correctly nest vars (e.g., transform the `API_HTTP_PORT` var into a `port` var inside `http` object inside `api` object)
32//! - Transform value types from strings to expected types.
33//!
34//! Preprocessing and merging config sources is encapsulated in [`ConfigRepository`].
35//!
36//! # TL;DR
37//!
38//! - Rich, self-documenting configuration schema.
39//! - Utilizes the schema to enrich configuration sources and intelligently merge them.
40//! - Doesn't require a god object uniting all configs in the app; they may be dynamically collected and deserialized
41//!   inside relevant components.
42//! - Supports lazy parsing for complex / multi-component apps (only the used configs are parsed; other configs are not required).
43//! - Supports multiple configuration formats and programmable source priorities (e.g., `base.yml` + overrides from the
44//!   `overrides/` dir in the alphabetic order + env vars).
45//! - Rich and complete deserialization errors including locations and value origins.
46//! - [Built-in support for secret params](de#secrets).
47//!
48//! # Crate features
49//!
50//! ## `primitive-types`
51//!
52//! *(Off by default)*
53//!
54//! Implements deserialization for basic Ethereum types like [`H256`](primitive_types::H256) (32-byte hash)
55//! and [`U256`](primitive_types::U256) (256-bit unsigned integer).
56//!
57//! ## `alloy`
58//!
59//! *(Off by default)*
60//!
61//! Implements deserialization for basic alloy primitive types like [`B256`](alloy::primitives::B256) (32-byte hash)
62//! and [`U256`](alloy::primitives::U256) (256-bit unsigned integer).
63//!
64//! # Examples
65//!
66//! ## Basic workflow
67//!
68//! ```
69//! use smart_config::{
70//!     config, ConfigSchema, ConfigRepository, DescribeConfig, DeserializeConfig, Yaml, Environment,
71//! };
72//!
73//! #[derive(Debug, DescribeConfig, DeserializeConfig)]
74//! pub struct TestConfig {
75//!     pub port: u16,
76//!     #[config(default_t = "test".into())]
77//!     pub name: String,
78//!     #[config(default_t = true)]
79//!     pub tracing: bool,
80//! }
81//!
82//! let schema = ConfigSchema::new(&TestConfig::DESCRIPTION, "test");
83//! // Assume we use two config sources: a YAML file and env vars,
84//! // the latter having higher priority.
85//! let yaml = r"
86//! test:
87//!   port: 4000
88//!   name: app
89//! ";
90//! let yaml = Yaml::new("test.yml", serde_yaml::from_str(yaml)?)?;
91//! let env = Environment::from_iter("APP_", [("APP_TEST_PORT", "8000")]);
92//! // Add both sources to a repo.
93//! let repo = ConfigRepository::new(&schema).with(yaml).with(env);
94//! // Get the parser for the config.
95//! let parser = repo.single::<TestConfig>()?;
96//! let config = parser.parse()?;
97//! assert_eq!(config.port, 8_000); // from the env var
98//! assert_eq!(config.name, "app"); // from YAML
99//! assert!(config.tracing); // from the default value
100//! # anyhow::Ok(())
101//! ```
102//!
103//! ## Declaring type as well-known
104//!
105//! ```
106//! use std::collections::HashMap;
107//! use smart_config::{
108//!     de::{Serde, WellKnown, WellKnownOption}, metadata::BasicTypes,
109//!     DescribeConfig, DeserializeConfig,
110//! };
111//!
112//! #[derive(Debug, serde::Serialize, serde::Deserialize)]
113//! enum CustomEnum {
114//!     First,
115//!     Second,
116//! }
117//!
118//! impl WellKnown for CustomEnum {
119//!     // signals that the type should be deserialized via `serde`
120//!     // and the expected input is a string
121//!     type Deserializer = Serde![str];
122//!     const DE: Self::Deserializer = Serde![str];
123//! }
124//!
125//! // Signals that the type can be used with an `Option<_>`
126//! impl WellKnownOption for CustomEnum {}
127//!
128//! // Then, the type can be used in configs basically everywhere:
129//! #[derive(Debug, DescribeConfig, DeserializeConfig)]
130//! struct TestConfig {
131//!     value: CustomEnum,
132//!     optional: Option<CustomEnum>,
133//!     repeated: Vec<CustomEnum>,
134//!     map: HashMap<String, CustomEnum>,
135//! }
136//! ```
137
138// Documentation settings
139#![doc(html_root_url = "https://docs.rs/smart-config/0.4.0-pre.2")] // x-release-please-version
140#![cfg_attr(docsrs, feature(doc_cfg))]
141// TODO: restore the attribute once supported by the nightly toolchain used by ZKsync OS
142//   #![cfg_attr(docsrs, doc(auto_cfg(hide(feature = "_docs"))))]
143// Linter settings
144#![warn(missing_docs)]
145
146pub use smart_config_derive::{DescribeConfig, DeserializeConfig, ExampleConfig};
147
148pub use self::{
149    de::DeserializeConfig,
150    error::{DeserializeConfigError, ErrorWithOrigin, ParseError, ParseErrorCategory, ParseErrors},
151    schema::{ConfigMut, ConfigRef, ConfigSchema},
152    source::{
153        ConfigParser, ConfigRepository, ConfigSource, ConfigSourceKind, ConfigSources, Environment,
154        Flat, Hierarchical, Json, Prefixed, SerializerOptions, SourceInfo, Yaml,
155    },
156    types::{ByteSize, EtherAmount},
157};
158use self::{metadata::ConfigMetadata, visit::VisitConfig};
159
160#[cfg(feature = "_docs")]
161pub mod _docs;
162pub mod de;
163mod error;
164pub mod fallback;
165pub mod metadata;
166pub mod pat;
167mod schema;
168mod source;
169pub mod testing;
170#[cfg(test)]
171mod testonly;
172mod types;
173mod utils;
174pub mod validation;
175pub mod value;
176pub mod visit;
177
178/// Describes a configuration (i.e., a group of related parameters).
179pub trait DescribeConfig: 'static + VisitConfig {
180    /// Provides the config description.
181    const DESCRIPTION: ConfigMetadata;
182}
183
184/// Provides an example for this configuration. The produced config can be used in tests etc.
185///
186/// For struct configs, this can be derived via [the corresponding proc macro](macro@ExampleConfig).
187pub trait ExampleConfig {
188    /// Constructs an example configuration.
189    fn example_config() -> Self;
190}
191
192impl<T: ExampleConfig> ExampleConfig for Option<T> {
193    fn example_config() -> Self {
194        Some(T::example_config())
195    }
196}
197
198#[cfg(doctest)]
199doc_comment::doctest!("../README.md");