Module derive_examples

Source
Expand description

§Derive macro examples

Various examples how to use DescribeConfig and other derive macros from the library.

§Basic usage

Shows how to use the macros with nested and flattened sub-configs, enum configs etc.

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] }
}

§Deriving ExampleConfig

#[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"],
        },
    })
);

§Advanced features

Demonstrates some advanced library features:

use std::{
    collections::{HashMap, HashSet},
    num::NonZeroU32,
    path::PathBuf,
    time::Duration,
};

use smart_config::{
    ByteSize, DescribeConfig, DeserializeConfig, EtherAmount, ExampleConfig, de, fallback,
    metadata::{SizeUnit, TimeUnit},
    pat::{LazyRegex, lazy_regex},
    value::SecretString,
};

static APP_NAME_REGEX: LazyRegex = lazy_regex!(r"^[a-z][-a-z0-9]*$");

/// Configuration with type params of several types.
#[derive(Debug, DescribeConfig, DeserializeConfig, ExampleConfig)]
pub(crate) struct TestConfig {
    /// Application name.
    // We validate that the name conforms to the regex defined above.
    #[config(default_t = "app".into(), validate(APP_NAME_REGEX))]
    pub app_name: String,
    /// Port to bind to.
    // `deprecated` allows to specify an alias that will be highlighted as deprecated
    // in docs (e.g., `smart-config-commands` output).
    #[config(example = 8080, deprecated = "bind_to")]
    pub port: u16,
    /// Should be greater than 0.
    // When a range is used as a validation, it's checked whether tha value
    // is in the range.
    #[config(default, validate(0.0..=10.0), example = Some(0.5))]
    pub scaling_factor: Option<f32>,
    /// Directory for temporary stuff.
    // `fallback` allows to read the value from another source that does not fit
    // into the hierarchical config schema.
    #[config(default_t = "/tmp".into(), fallback = &fallback::Env("TMPDIR"))]
    pub temp_dir: PathBuf,
    /// Paths to key directories.
    // `Delimited` deserializer allows to read paths both from an array
    // and from a ':'-delimited string. This is useful if the config
    // is parsed from env vars.
    #[config(default, alias = "dirs", with = de::Delimited::new(":"))]
    #[config(example = HashSet::from_iter(["./local".into()]))]
    pub dir_paths: HashSet<PathBuf>,
    /// In-memory cache size.
    // The default deserializer for `ByteSize`s, `Duration`s and `EtherUnit`s
    // accepts a numeric value together with a unit, e.g. '16 MB'. In case of durations
    // and `EtherUnit`, the value may be a decimal, like '0.5 days' or '0.02 ether'.
    //
    // Note that `deprecated` in this case is a relative path, not just a name.
    #[config(default_t = 16 * SizeUnit::MiB, deprecated = ".experimental.cache_size")]
    pub cache_size: ByteSize,
    /// Timeout for some operation.
    // Using a specific unit as a deserializer will accept numeric values
    // measured in this unit. This is not recommended to use; the default deserializer
    // for united values automatically handles suffixed values.
    #[config(default_t = 1 * TimeUnit::Minutes, with = TimeUnit::Seconds)]
    pub timeout_sec: Duration,
    // Nested configuration.
    #[config(nest)]
    pub experimental: ExperimentalConfig,
}

#[derive(Debug, DescribeConfig, DeserializeConfig, ExampleConfig)]
#[config(derive(Default))]
pub(crate) struct ExperimentalConfig {
    /// Secret string value.
    #[config(example = Some("correct horse battery staple".into()))]
    pub api_key: Option<SecretString>,
    /// Can be deserialized either from a map or an array of tuples.
    #[config(default, with = de::Entries::WELL_KNOWN.named("method", "rps"))]
    #[config(example = HashMap::from_iter([
        ("eth_call".into(), NonZeroU32::new(100).unwrap()),
        ("eth_blockNumber".into(), NonZeroU32::new(1).unwrap()),
    ]))]
    pub method_limits: HashMap<String, NonZeroU32>,
    /// Can be deserialized from a string.
    #[config(
        default,
        with = de::Entries::WELL_KNOWN.delimited(
            lazy_regex!(ref r"\s*[,\n]\s*"),
            lazy_regex!(ref r"\s*=\s*"),
        )
    )]
    pub balances: HashMap<u64, EtherAmount>,
}

§Matching YAML configuration

app_name: stage
# Use the deprecated alias for the `port` param
bind_to: 8080
scaling_factor: 3.0
temp_dir: /var/app-stage
dir_paths:
  - /usr/bin
  - /usr/local/bin
  - /bin
timeout_sec: 3
experimental:
  # Use the deprecated path alias for `cache_size`
  cache_size: 32 MB
  api_key: SUPER_SECRET_DONUT_STEEL
  # Use the array format that would be translated to a map
  method_limits:
    - method: eth_call
      rps: 200
    - method: eth_blockNumber
      rps: 1000
  balances:
    1234: 0.01 ether
    4321: 600000 gwei

§Matching env variables

Assumes the APP_ prefix for env vars.

APP_APP_NAME=stage
APP_PORT=8080
APP_SCALING_FACTOR=3.0
APP_TEMP_DIR=/var/app-stage
# Note the use of ':' delimiter
APP_DIR_PATHS="/usr/bin:/usr/local/bin:/bin"
APP_TIMEOUT_SEC=2

# Unit deserializer is smart enough to parse unit suffixes
# ('_MB' in this case). Note that we still use a deprecated path alias
# for the param.
APP_EXPERIMENTAL_CACHE_SIZE_MB=128
APP_EXPERIMENTAL_API_KEY=SUPER_SECRET_DONUT_STEEL
# Here, we use JSON coercion because the deserializer doesn't support any plain values
APP_EXPERIMENTAL_METHOD_LIMITS__JSON='{ "eth_call": 200, "eth_blockNumber": 1000 }'
# Note the natural spacing around ',' and '=' enabled by the use of regexes
APP_EXPERIMENTAL_BALANCES="1234 = 0.01 ether, 4321 = 600000 gwei"

§See also