1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
//! `smart-config` – schema-driven layered configuration system with support of multiple configuration formats.
//!
//! # Overview
//!
//! The task solved by the library is merging configuration input from a variety of prioritized [sources](ConfigSource)
//! (JSON and YAML files, env variables, command-line args etc.) and converting this input to strongly typed
//! representation (i.e., config structs or enums). As with other config systems, config input follows the JSON object model
//! (see [`Value`](value::Value)), with each value enriched with its [origin](value::ValueOrigin) (e.g., a path in a specific JSON file,
//! or a specific env var). This allows attributing errors during deserialization.
//!
//! The defining feature of `smart-config` is its schema-driven design. Each config type has associated [metadata](ConfigMetadata)
//! defined with the help of the [`DescribeConfig`] derive macro; deserialization is handled by the accompanying [`DeserializeConfig`] macro.
//! Metadata includes a variety of info extracted from the config type:
//!
//! - [Parameter info](metadata::ParamMetadata): name (including aliases and renaming), help (extracted from doc comments),
//!   type, [deserializer for the param](de::DeserializeParam) etc.
//! - [Nested configurations](metadata::NestedConfigMetadata).
//!
//! Multiple configurations are collected into a global [`ConfigSchema`]. Each configuration is *mounted* at a specific path.
//! E.g., if a large app has an HTTP server component, it may be mounted at `api.http`. Multiple config types may be mounted
//! at the same path (e.g., flattened configs); conversely, a single config type may be mounted at multiple places.
//! 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
//! inside relevant components.
//!
//! This information provides rich human-readable info about configs. It also assists when preprocessing and merging config inputs.
//! For example, env vars are a flat string -> string map; with the help of a schema, it's possible to:
//!
//! - Correctly nest vars (e.g., transform the `API_HTTP_PORT` var into a `port` var inside `http` object inside `api` object)
//! - Transform value types from strings to expected types.
//!
//! Preprocessing and merging config sources is encapsulated in [`ConfigRepository`].
//!
//! # TL;DR
//!
//! - Rich, self-documenting configuration schema.
//! - Utilizes the schema to enrich configuration sources and intelligently merge them.
//! - Doesn't require a god object uniting all configs in the app; they may be dynamically collected and deserialized
//!   inside relevant components.
//! - Supports lazy parsing for complex / multi-component apps (only the used configs are parsed; other configs are not required).
//! - Supports multiple configuration formats and programmable source priorities (e.g., `base.yml` + overrides from the
//!   `overrides/` dir in the alphabetic order + env vars).
//! - Rich and complete deserialization errors including locations and value origins.
//! - [Built-in support for secret params](de#secrets).
//!
//! # Crate features
//!
//! ## `primitive-types`
//!
//! *(Off by default)*
//!
//! Implements deserialization for basic Ethereum types like [`H256`](primitive_types::H256) (32-byte hash)
//! and [`U256`](primitive_types::U256) (256-bit unsigned integer).
//!
//! # Examples
//!
//! ## Basic workflow
//!
//! ```
//! use smart_config::{
//!     config, ConfigSchema, ConfigRepository, DescribeConfig, DeserializeConfig, Yaml, Environment,
//! };
//!
//! #[derive(Debug, DescribeConfig, DeserializeConfig)]
//! pub struct TestConfig {
//!     pub port: u16,
//!     #[config(default_t = "test".into())]
//!     pub name: String,
//!     #[config(default_t = true)]
//!     pub tracing: bool,
//! }
//!
//! let schema = ConfigSchema::new(&TestConfig::DESCRIPTION, "test");
//! // Assume we use two config sources: a YAML file and env vars,
//! // the latter having higher priority.
//! let yaml = r"
//! test:
//!   port: 4000
//!   name: app
//! ";
//! let yaml = Yaml::new("test.yml", serde_yaml::from_str(yaml)?)?;
//! let env = Environment::from_iter("APP_", [("APP_TEST_PORT", "8000")]);
//! // Add both sources to a repo.
//! let repo = ConfigRepository::new(&schema).with(yaml).with(env);
//! // Get the parser for the config.
//! let parser = repo.single::<TestConfig>()?;
//! let config = parser.parse()?;
//! assert_eq!(config.port, 8_000); // from the env var
//! assert_eq!(config.name, "app"); // from YAML
//! assert!(config.tracing); // from the default value
//! # anyhow::Ok(())
//! ```
//!
//! ## Declaring type as well-known
//!
//! ```
//! use std::collections::HashMap;
//! use smart_config::{
//!     de::{Serde, WellKnown, WellKnownOption}, metadata::BasicTypes,
//!     DescribeConfig, DeserializeConfig,
//! };
//!
//! #[derive(Debug, serde::Serialize, serde::Deserialize)]
//! enum CustomEnum {
//!     First,
//!     Second,
//! }
//!
//! impl WellKnown for CustomEnum {
//!     // signals that the type should be deserialized via `serde`
//!     // and the expected input is a string
//!     type Deserializer = Serde![str];
//!     const DE: Self::Deserializer = Serde![str];
//! }
//!
//! // Signals that the type can be used with an `Option<_>`
//! impl WellKnownOption for CustomEnum {}
//!
//! // Then, the type can be used in configs basically everywhere:
//! #[derive(Debug, DescribeConfig, DeserializeConfig)]
//! struct TestConfig {
//!     value: CustomEnum,
//!     optional: Option<CustomEnum>,
//!     repeated: Vec<CustomEnum>,
//!     map: HashMap<String, CustomEnum>,
//! }
//! ```

// Documentation settings
#![doc(html_root_url = "https://docs.rs/smart-config/0.3.0-pre")]
#![cfg_attr(docsrs, feature(doc_cfg))]
// Linter settings
#![warn(missing_docs)]

/// Derives the [`DescribeConfig`](trait@DescribeConfig) trait for a type.
///
/// This macro supports both structs and enums. It is conceptually similar to `Deserialize` macro from `serde`.
/// Macro behavior can be configured with `#[config(_)]` attributes. Multiple `#[config(_)]` attributes
/// on a single item are supported.
///
/// Each field in the struct / each enum variant is considered a configuration param (by default),
/// or a sub-config (if `#[config(nest)]` or `#[config(flatten)]` is present for the field).
///
/// # Container attributes
///
/// ## `validate`
///
/// **Type:** One of the following:
///
/// - Expression evaluating to a [`Validate`](validation::Validate) implementation (e.g., a [`Range`](std::ops::Range); see the `Validate` docs
///   for implementations). An optional human-readable string validation description may be provided delimited by the comma (e.g., to make the description
///   more domain-specific).
/// - Pointer to a function with the `fn(&_) -> Result<(), ErrorWithOrigin>` signature and the validation description separated by a comma.
/// - Pointer to a function with the `fn(&_) -> bool` signature and the validation description separated by a comma. Validation fails
///   if the function returns `false`.
///
/// See the examples in the [`validation`] module.
///
/// Specifies a post-deserialization validation for the config. This is useful to check invariants involving multiple params.
/// Multiple validations are supported by specifying the attribute multiple times.
///
/// ## `tag`
///
/// **Type:** string
///
/// Specifies the param name holding the enum tag, similar to the corresponding attribute in `serde`.
/// Unlike `serde`, this attribute is *required* for enums; this is to ensure that source merging is well-defined.
///
/// ## `rename_all`
///
/// **Type:** string; one of `lowercase`, `UPPERCASE`, `camelCase`, `snake_case`, `SCREAMING_SNAKE_CASE`,
/// `kebab-case`, `SCREAMING-KEBAB-CASE`
///
/// Renames all variants in an enum config according to the provided transform. Unlike in `serde`, this attribute
/// *only* works on enum variants. Params / sub-configs are always expected to have `snake_case` naming.
///
/// Caveats:
///
/// - `rename_all` assumes that original variant names are in `PascalCase` (i.e., follow Rust naming conventions).
/// - `rename_all` requires original variant names to consist of ASCII chars.
/// - Each letter of capitalized acronyms (e.g., "HTTP" in `HTTPServer`) is treated as a separate word.
///   E.g., `rename_all = "snake_case"` will rename `HTTPServer` to `h_t_t_p_server`.
///   Note that [it is recommended][clippy-acronyms] to not capitalize acronyms (i.e., use `HttpServer`).
/// - No spacing is inserted before numbers or other non-letter chars. E.g., `rename_all = "snake_case"`
///   will rename `Status500` to `status500`, not to `status_500`.
///
/// [clippy-acronyms]: https://rust-lang.github.io/rust-clippy/master/index.html#/upper_case_acronyms
///
/// ## `derive(Default)`
///
/// Derives `Default` according to the default values of params (+ the default variant for enum configs).
/// To work, all params must have a default value specified.
///
/// # Variant attributes
///
/// ## `rename`, `alias`
///
/// **Type:** string
///
/// Have the same meaning as in `serde`; i.e. allow to rename / specify additional names for the tag(s)
/// corresponding to the variant. `alias` can be specified multiple times.
///
/// ## `default`
///
/// If specified, marks the variant as default – one which will be used if the tag param is not set in the input.
/// At most one variant can be marked as default.
///
/// # Field attributes
///
/// ## `rename`, `alias`
///
/// **Type:** string
///
/// Have the same meaning as in `serde`; i.e. allow to rename / specify additional names for the param or a nested config.
/// Names are [validated](#validations) in compile time.
///
/// In addition to simple names, *path* aliases are supported as well. A path alias starts with `.` and consists of dot-separated segments,
/// e.g. `.experimental.value` or `..value`. The paths are resolved relative to the config prefix. As in Python, more than one dot
/// at the start of the path signals that the path is relative to the parent(s) of the config.
///
/// - `alias = ".experimental.value"` with config prefix `test` resolves to the absolute path `test.experimental.value`.
/// - `alias = "..value"` with config prefix `test.experimental` resolves to the absolute path `test.value`.
///
/// If an alias requires more parents than is present in the config prefix, the alias is not applicable.
/// (E.g., `alias = "...value"` with config prefix `test`.)
///
/// Path aliases are somewhat difficult to reason about, so avoid using them unless necessary.
///
/// ## `deprecated`
///
/// **Type:** string
///
/// Similar to `alias`, with the difference that the alias is marked as deprecated in the schema docs,
/// and its usages are logged on the `WARN` level.
///
/// ## `default`
///
/// **Type:** path to function (optional)
///
/// Has the same meaning as in `serde`, i.e. allows to specify a constructor of the default value for the param.
/// Without a value, [`Default`] is used for this purpose. Unlike `serde`, the path shouldn't be quoted.
///
/// ## `default_t`
///
/// **Type:** expression with param type
///
/// Allows to specify the default typed value for the param. The provided expression doesn't need to be constant.
///
/// ## `example`
///
/// **Type:** expression with field type
///
/// Allows to specify the example value for the param. The example value can be specified together with the `default` / `default_t`
/// attribute. In this case, the example value can be more "complex" than the default, to better illustrate how the configuration works.
///
/// ## `fallback`
///
/// **Type:** constant expression evaluating to `&'static dyn `[`FallbackSource`](fallback::FallbackSource)
///
/// Allows to provide a fallback source for the param. See the [`fallback`] module docs for the discussion of fallbacks
/// and intended use cases.
///
/// ## `with`
///
/// **Type:** const expression implementing [`DeserializeParam`]
///
/// Allows changing the param deserializer. See [`de`] module docs for the overview of available deserializers.
/// For `Option`s, `with` refers to the *internal* type deserializer; it will be wrapped into an [`Optional`](crate::de::Optional) automatically.
///
/// Note that there is an alternative: implementing [`WellKnown`](de::WellKnown) for the param type.
///
/// ## `nest`
///
/// If specified, the field is treated as a nested sub-config rather than a param. Correspondingly, its type must
/// implement `DescribeConfig`, or wrap such a type in an `Option`.
///
/// ## `flatten`
///
/// If specified, the field is treated as a *flattened* sub-config rather than a param. Unlike `nest`, its params
/// will be added to the containing config instead of a separate object. The sub-config type must implement `DescribeConfig`.
///
/// ## `validate`
///
/// Has same semantics as [config validations](#validate), but applies to a specific config parameter.
///
/// ## `deserialize_if`
///
/// **Type:** same as [config validations](#validate)
///
/// Filters an `Option`al value. This is useful to coerce semantically invalid values (e.g., empty strings for URLs)
/// to `None` in the case [automated null coercion](crate::de::Optional#encoding-nulls) doesn't apply.
/// See the [`validation`] module for examples of usage.
///
/// # Validations
///
/// The following validations are performed by the macro in compile time:
///
/// - Param / sub-config names and aliases must be non-empty, consist of lowercase ASCII alphanumeric chars or underscore
///   and not start with a digit (i.e., follow the `[a-z_][a-z0-9_]*` regex).
/// - Param names / aliases cannot coincide with nested config names.
///
/// [`DeserializeParam`]: de::DeserializeParam
///
/// # Examples
///
/// ```
/// # use std::{collections::HashSet, num::NonZeroUsize, time::Duration};
/// # use smart_config::{DescribeConfig, DeserializeConfig};
/// use smart_config::metadata::TimeUnit;
///
/// #[derive(DescribeConfig, DeserializeConfig)]
/// struct TestConfig {
///     /// Doc comments are parsed as a description.
///     #[config(default_t = 3)]
///     int: u32,
///     #[config(default)] // multiple `config` attrs are supported
///     #[config(rename = "str", alias = "string")]
///     renamed: String,
///     /// Nested sub-config. E.g., the tag will be read from path `nested.version`.
///     #[config(nest)]
///     nested: NestedConfig,
///     /// Flattened sub-config. E.g., `array` param will be read from `array`, not `flat.array`.
///     #[config(flatten)]
///     flat: FlattenedConfig,
/// }
///
/// #[derive(DescribeConfig, DeserializeConfig)]
/// #[config(tag = "version", rename_all = "snake_case", derive(Default))]
/// enum NestedConfig {
///     #[config(default)]
///     V0,
///     #[config(alias = "latest")]
///     V1 {
///         /// Param with a custom deserializer. In this case, it will deserialize
///         /// a duration from a number with milliseconds unit of measurement.
///         #[config(default_t = Duration::from_millis(50), with = TimeUnit::Millis)]
///         latency_ms: Duration,
///         /// `Vec`s, sets and other containers are supported out of the box.
///         set: HashSet<NonZeroUsize>,
///     },
/// }
///
/// #[derive(DescribeConfig, DeserializeConfig)]
/// struct FlattenedConfig {
///     #[config(default = FlattenedConfig::default_array)]
///     array: [f32; 2],
/// }
///
/// impl FlattenedConfig {
///     const fn default_array() -> [f32; 2] { [1.0, 2.0] }
/// }
/// ```
pub use smart_config_derive::DescribeConfig;
/// Derives the [`DeserializeConfig`](trait@DeserializeConfig) trait for a type.
///
/// This macro is intended to be used together with [`DescribeConfig`](macro@DescribeConfig). It reuses
/// the same attributes, so see `DescribeConfig` docs for details and examples of usage.
pub use smart_config_derive::DeserializeConfig;
/// Derives the [`ExampleConfig`](trait@ExampleConfig) trait for a type.
///
/// This macro is intended to be used together with [`DescribeConfig`](macro@DescribeConfig); it reuses
/// the same attributes. Specifically, for each config field, the default value is assigned from the following sources
/// in the decreasing priority order:
///
/// 1. `example`
/// 2. `default` / `default_t`, including implied ones for `Option`al fields
/// 3. From [`ExampleConfig`](trait@ExampleConfig) implementation (only for nested / flattened configs)
///
/// # Examples
///
/// ```
/// # use std::collections::HashSet;
/// # use smart_config::{DescribeConfig, ExampleConfig, SerializerOptions};
/// #[derive(DescribeConfig, ExampleConfig)]
/// struct TestConfig {
///     /// Required param that still has an example value.
///     #[config(example = 42)]
///     required: u32,
///     optional: Option<String>,
///     #[config(default_t = true)]
///     with_default: bool,
///     #[config(default, example = vec![5, 8])]
///     values: Vec<u32>,
///     #[config(nest)]
///     nested: NestedConfig,
/// }
///
/// #[derive(DescribeConfig, ExampleConfig)]
/// struct NestedConfig {
///     #[config(default, example = ["eth_call".into()].into())]
///     methods: HashSet<String>,
/// }
///
/// let example: TestConfig = TestConfig::example_config();
/// let json = SerializerOptions::default().serialize(&example);
/// assert_eq!(
///     serde_json::Value::from(json),
///     serde_json::json!({
///         "required": 42,
///         "optional": null,
///         "with_default": true,
///         "values": [5, 8],
///         "nested": {
///             "methods": ["eth_call"],
///         },
///     })
/// );
/// ```
pub use smart_config_derive::ExampleConfig;

pub use self::{
    de::DeserializeConfig,
    error::{DeserializeConfigError, ErrorWithOrigin, ParseError, ParseErrorCategory, ParseErrors},
    schema::{ConfigMut, ConfigRef, ConfigSchema},
    source::{
        ConfigParser, ConfigRepository, ConfigSource, ConfigSourceKind, ConfigSources, Environment,
        Flat, Hierarchical, Json, Prefixed, SerializerOptions, SourceInfo, Yaml,
    },
    types::ByteSize,
};
use self::{metadata::ConfigMetadata, visit::VisitConfig};

pub mod de;
mod error;
pub mod fallback;
pub mod metadata;
mod schema;
mod source;
pub mod testing;
#[cfg(test)]
mod testonly;
mod types;
mod utils;
pub mod validation;
pub mod value;
pub mod visit;

/// Describes a configuration (i.e., a group of related parameters).
pub trait DescribeConfig: 'static + VisitConfig {
    /// Provides the config description.
    const DESCRIPTION: ConfigMetadata;
}

/// Provides an example for this configuration. The produced config can be used in tests etc.
///
/// For struct configs, this can be derived via [the corresponding proc macro](macro@ExampleConfig).
pub trait ExampleConfig {
    /// Constructs an example configuration.
    fn example_config() -> Self;
}

impl<T: ExampleConfig> ExampleConfig for Option<T> {
    fn example_config() -> Self {
        Some(T::example_config())
    }
}

#[cfg(doctest)]
doc_comment::doctest!("../README.md");