tier 0.1.17

Rust configuration library for layered TOML, env, and CLI settings
Documentation
use crate::error::ConfigError;

use super::Layer;
use super::file::FileSource;
use super::overrides::parse_override_value;
use super::path::try_normalize_external_path;

mod claim;
mod layer;

use self::layer::ArgsLayerState;

#[derive(Debug, Clone)]
/// CLI override source definition.
///
/// `ArgsSource` parses the same `--config`, `--profile`, and `--set key=value`
/// flags that `tier` accepts through its reusable `clap` integration.
///
/// # Examples
///
/// ```
/// use serde::{Deserialize, Serialize};
/// use tier::{ArgsSource, ConfigLoader};
///
/// #[derive(Debug, Clone, Serialize, Deserialize)]
/// struct AppConfig {
///     port: u16,
/// }
///
/// impl Default for AppConfig {
///     fn default() -> Self {
///         Self { port: 3000 }
///     }
/// }
///
/// let loaded = ConfigLoader::new(AppConfig::default())
///     .args(ArgsSource::from_args(["app", "--set", "port=7000"]))
///     .load()?;
///
/// assert_eq!(loaded.port, 7000);
/// # Ok::<(), tier::ConfigError>(())
/// ```
pub struct ArgsSource {
    args: Vec<String>,
}

impl ArgsSource {
    /// Captures the current process arguments.
    #[must_use]
    pub fn from_env() -> Self {
        Self::from_args(std::env::args())
    }

    /// Creates an argument source from explicit argv values.
    #[must_use]
    pub fn from_args<I, S>(iter: I) -> Self
    where
        I: IntoIterator<Item = S>,
        S: Into<String>,
    {
        Self {
            args: iter.into_iter().map(Into::into).collect(),
        }
    }
}

#[derive(Debug, Clone)]
pub(super) struct ParsedArgs {
    pub(super) profile: Option<String>,
    pub(super) files: Vec<FileSource>,
    pub(super) layer: Option<Layer>,
}

pub(super) fn parse_args(source: ArgsSource) -> Result<ParsedArgs, ConfigError> {
    let mut args = source.args.into_iter();
    let mut files = Vec::new();
    let mut profile = None;
    let mut layer = ArgsLayerState::new();

    while let Some(arg) = args.next() {
        if let Some(value) = arg.strip_prefix("--config=") {
            files.push(FileSource::new(value));
            continue;
        }

        if arg == "--config" {
            let value = args.next().ok_or_else(|| ConfigError::MissingArgValue {
                flag: "--config".to_owned(),
            })?;
            files.push(FileSource::new(value));
            continue;
        }

        if let Some(value) = arg.strip_prefix("--profile=") {
            profile = Some(value.to_owned());
            continue;
        }

        if arg == "--profile" {
            profile = Some(args.next().ok_or_else(|| ConfigError::MissingArgValue {
                flag: "--profile".to_owned(),
            })?);
            continue;
        }

        let set_value = if let Some(value) = arg.strip_prefix("--set=") {
            Some(value.to_owned())
        } else if arg == "--set" {
            Some(args.next().ok_or_else(|| ConfigError::MissingArgValue {
                flag: "--set".to_owned(),
            })?)
        } else {
            None
        };

        let Some(set_value) = set_value else {
            continue;
        };

        let (raw_path, raw_value) =
            set_value
                .split_once('=')
                .ok_or_else(|| ConfigError::InvalidArg {
                    arg: set_value.clone(),
                    message: "expected key=value".to_owned(),
                })?;
        let path =
            try_normalize_external_path(raw_path).map_err(|message| ConfigError::InvalidArg {
                arg: format!("--set {raw_path}={raw_value}"),
                message,
            })?;
        if path.is_empty() {
            return Err(ConfigError::InvalidArg {
                arg: set_value,
                message: "configuration path cannot be empty".to_owned(),
            });
        }

        let parsed =
            parse_override_value(raw_value).map_err(|message| ConfigError::InvalidArg {
                arg: format!("--set {path}={raw_value}"),
                message,
            })?;
        let arg_trace_name = format!("--set {raw_path}={raw_value}");
        layer.insert_override(
            &arg_trace_name,
            &path,
            parsed,
            format!("--set {path}={raw_value}"),
        )?;
    }

    Ok(ParsedArgs {
        profile,
        files,
        layer: layer.into_layer(),
    })
}