Skip to main content

use_config_source/
lib.rs

1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4use core::{cmp::Ordering, fmt};
5
6/// A primitive kind for describing a configuration source.
7#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
8pub enum ConfigSourceKind {
9    /// Built-in or caller-provided default values.
10    Default,
11    /// Values identified as coming from a file.
12    File,
13    /// Values identified as coming from an environment source.
14    Environment,
15    /// Values identified as coming from runtime input.
16    Runtime,
17    /// Values identified as explicit overrides.
18    Override,
19    /// Values identified as secret references.
20    SecretReference,
21    /// Caller-defined source kind.
22    Custom(String),
23}
24
25impl fmt::Display for ConfigSourceKind {
26    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
27        match self {
28            Self::Default => formatter.write_str("default"),
29            Self::File => formatter.write_str("file"),
30            Self::Environment => formatter.write_str("environment"),
31            Self::Runtime => formatter.write_str("runtime"),
32            Self::Override => formatter.write_str("override"),
33            Self::SecretReference => formatter.write_str("secret-reference"),
34            Self::Custom(value) => formatter.write_str(value),
35        }
36    }
37}
38
39/// Identity and precedence metadata for a configuration source.
40#[derive(Clone, Debug, Eq, PartialEq, Hash)]
41pub struct ConfigSource {
42    /// Source kind.
43    pub kind: ConfigSourceKind,
44    /// Optional source name.
45    pub name: Option<String>,
46    /// Source priority. Higher priority wins in layer merges.
47    pub priority: i32,
48}
49
50impl ConfigSource {
51    /// Creates a source from explicit parts.
52    #[must_use]
53    pub fn new(kind: ConfigSourceKind, name: Option<String>, priority: i32) -> Self {
54        Self {
55            kind,
56            name: normalize_name(name),
57            priority,
58        }
59    }
60
61    /// Creates an unnamed source.
62    #[must_use]
63    pub fn unnamed(kind: ConfigSourceKind, priority: i32) -> Self {
64        Self::new(kind, None, priority)
65    }
66
67    /// Creates a named source.
68    #[must_use]
69    pub fn named(kind: ConfigSourceKind, name: impl Into<String>, priority: i32) -> Self {
70        Self::new(kind, Some(name.into()), priority)
71    }
72
73    /// Returns the source kind.
74    #[must_use]
75    pub const fn kind(&self) -> &ConfigSourceKind {
76        &self.kind
77    }
78
79    /// Returns the optional source name.
80    #[must_use]
81    pub fn name(&self) -> Option<&str> {
82        self.name.as_deref()
83    }
84
85    /// Returns the source priority.
86    #[must_use]
87    pub const fn priority(&self) -> i32 {
88        self.priority
89    }
90}
91
92impl fmt::Display for ConfigSource {
93    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
94        if let Some(name) = &self.name {
95            write!(formatter, "{}:{}@{}", self.kind, name, self.priority)
96        } else {
97            write!(formatter, "{}@{}", self.kind, self.priority)
98        }
99    }
100}
101
102impl Ord for ConfigSource {
103    fn cmp(&self, other: &Self) -> Ordering {
104        self.priority
105            .cmp(&other.priority)
106            .then_with(|| self.kind.cmp(&other.kind))
107            .then_with(|| self.name.cmp(&other.name))
108    }
109}
110
111impl PartialOrd for ConfigSource {
112    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
113        Some(self.cmp(other))
114    }
115}
116
117fn normalize_name(name: Option<String>) -> Option<String> {
118    name.and_then(|value| {
119        let trimmed = value.trim();
120
121        if trimmed.is_empty() {
122            None
123        } else {
124            Some(trimmed.to_owned())
125        }
126    })
127}
128
129#[cfg(test)]
130mod tests {
131    use super::{ConfigSource, ConfigSourceKind};
132
133    #[test]
134    fn source_creation() {
135        let source = ConfigSource::named(ConfigSourceKind::File, " app.toml ", 10);
136
137        assert_eq!(source.kind(), &ConfigSourceKind::File);
138        assert_eq!(source.name(), Some("app.toml"));
139        assert_eq!(source.priority(), 10);
140    }
141
142    #[test]
143    fn source_display() {
144        let named = ConfigSource::named(ConfigSourceKind::Override, "cli", 20);
145        let unnamed = ConfigSource::unnamed(ConfigSourceKind::Default, 0);
146
147        assert_eq!(named.to_string(), "override:cli@20");
148        assert_eq!(unnamed.to_string(), "default@0");
149    }
150
151    #[test]
152    fn priority_ordering() {
153        let low = ConfigSource::unnamed(ConfigSourceKind::Default, 0);
154        let high = ConfigSource::unnamed(ConfigSourceKind::Override, 10);
155        let mut sources = vec![high.clone(), low.clone()];
156
157        sources.sort();
158
159        assert_eq!(sources, vec![low, high]);
160    }
161
162    #[test]
163    fn custom_source_kind() {
164        let source = ConfigSource::named(ConfigSourceKind::Custom("fixture".to_owned()), "base", 5);
165
166        assert_eq!(source.kind().to_string(), "fixture");
167        assert_eq!(source.to_string(), "fixture:base@5");
168    }
169}