1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4use core::{cmp::Ordering, fmt};
5
6#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
8pub enum ConfigSourceKind {
9 Default,
11 File,
13 Environment,
15 Runtime,
17 Override,
19 SecretReference,
21 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#[derive(Clone, Debug, Eq, PartialEq, Hash)]
41pub struct ConfigSource {
42 pub kind: ConfigSourceKind,
44 pub name: Option<String>,
46 pub priority: i32,
48}
49
50impl ConfigSource {
51 #[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 #[must_use]
63 pub fn unnamed(kind: ConfigSourceKind, priority: i32) -> Self {
64 Self::new(kind, None, priority)
65 }
66
67 #[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 #[must_use]
75 pub const fn kind(&self) -> &ConfigSourceKind {
76 &self.kind
77 }
78
79 #[must_use]
81 pub fn name(&self) -> Option<&str> {
82 self.name.as_deref()
83 }
84
85 #[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}