Skip to main content

use_config_key/
lib.rs

1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4use core::{fmt, str::FromStr};
5use std::error::Error;
6
7/// Error returned when a configuration key, section, or path is invalid.
8#[derive(Clone, Copy, Debug, Eq, PartialEq)]
9pub enum ConfigKeyError {
10    /// The provided key or path was empty after trimming whitespace.
11    Empty,
12    /// A dotted path contained an empty segment.
13    EmptySegment,
14    /// A single key or section segment contained a dot.
15    DottedSegment,
16}
17
18impl fmt::Display for ConfigKeyError {
19    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
20        let message = match self {
21            Self::Empty => "configuration key is empty",
22            Self::EmptySegment => "configuration path contains an empty segment",
23            Self::DottedSegment => "configuration segment must not contain dots",
24        };
25
26        formatter.write_str(message)
27    }
28}
29
30impl Error for ConfigKeyError {}
31
32/// A validated single configuration key segment.
33#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
34pub struct ConfigKey(String);
35
36impl ConfigKey {
37    /// Creates a configuration key from one non-empty segment.
38    ///
39    /// # Errors
40    ///
41    /// Returns [`ConfigKeyError`] when the input is empty after trimming or contains a dot.
42    pub fn new(input: impl AsRef<str>) -> Result<Self, ConfigKeyError> {
43        validated_single_segment(input.as_ref()).map(|segment| Self(segment.to_owned()))
44    }
45
46    /// Returns the key as a borrowed string slice.
47    #[must_use]
48    pub fn as_str(&self) -> &str {
49        &self.0
50    }
51
52    /// Returns the owned key string.
53    #[must_use]
54    pub fn into_string(self) -> String {
55        self.0
56    }
57}
58
59impl fmt::Display for ConfigKey {
60    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
61        formatter.write_str(&self.0)
62    }
63}
64
65impl FromStr for ConfigKey {
66    type Err = ConfigKeyError;
67
68    fn from_str(input: &str) -> Result<Self, Self::Err> {
69        Self::new(input)
70    }
71}
72
73/// A validated configuration section name.
74#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
75pub struct ConfigSection(String);
76
77impl ConfigSection {
78    /// Creates a configuration section from one non-empty segment.
79    ///
80    /// # Errors
81    ///
82    /// Returns [`ConfigKeyError`] when the input is empty after trimming or contains a dot.
83    pub fn new(input: impl AsRef<str>) -> Result<Self, ConfigKeyError> {
84        validated_single_segment(input.as_ref()).map(|segment| Self(segment.to_owned()))
85    }
86
87    /// Returns the section as a borrowed string slice.
88    #[must_use]
89    pub fn as_str(&self) -> &str {
90        &self.0
91    }
92
93    /// Returns the owned section string.
94    #[must_use]
95    pub fn into_string(self) -> String {
96        self.0
97    }
98}
99
100impl fmt::Display for ConfigSection {
101    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
102        formatter.write_str(&self.0)
103    }
104}
105
106impl FromStr for ConfigSection {
107    type Err = ConfigKeyError;
108
109    fn from_str(input: &str) -> Result<Self, Self::Err> {
110        Self::new(input)
111    }
112}
113
114/// A validated dotted configuration path.
115#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
116pub struct ConfigPath {
117    segments: Vec<String>,
118}
119
120impl ConfigPath {
121    /// Parses a dotted configuration path.
122    ///
123    /// # Errors
124    ///
125    /// Returns [`ConfigKeyError`] when the path is empty or contains an invalid segment.
126    pub fn parse(input: &str) -> Result<Self, ConfigKeyError> {
127        input.parse()
128    }
129
130    /// Creates a path from already separated segments.
131    ///
132    /// # Errors
133    ///
134    /// Returns [`ConfigKeyError`] when no segments are provided or any segment is invalid.
135    pub fn from_segments<I, S>(segments: I) -> Result<Self, ConfigKeyError>
136    where
137        I: IntoIterator<Item = S>,
138        S: AsRef<str>,
139    {
140        let mut validated = Vec::new();
141
142        for segment in segments {
143            validated.push(validated_path_segment(segment.as_ref())?.to_owned());
144        }
145
146        if validated.is_empty() {
147            return Err(ConfigKeyError::Empty);
148        }
149
150        Ok(Self {
151            segments: validated,
152        })
153    }
154
155    /// Returns the number of path segments.
156    #[must_use]
157    pub const fn len(&self) -> usize {
158        self.segments.len()
159    }
160
161    /// Returns `true` when the path has no segments.
162    #[must_use]
163    pub const fn is_empty(&self) -> bool {
164        self.segments.is_empty()
165    }
166
167    /// Iterates over path segments in deterministic order.
168    pub fn segments(&self) -> impl Iterator<Item = &str> {
169        self.segments.iter().map(String::as_str)
170    }
171
172    /// Returns a path segment by index.
173    #[must_use]
174    pub fn get(&self, index: usize) -> Option<&str> {
175        self.segments.get(index).map(String::as_str)
176    }
177
178    /// Returns the first path segment.
179    #[must_use]
180    pub fn first(&self) -> Option<&str> {
181        self.get(0)
182    }
183
184    /// Returns the final path segment.
185    #[must_use]
186    pub fn last(&self) -> Option<&str> {
187        self.segments.last().map(String::as_str)
188    }
189}
190
191impl fmt::Display for ConfigPath {
192    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
193        for (index, segment) in self.segments.iter().enumerate() {
194            if index > 0 {
195                formatter.write_str(".")?;
196            }
197
198            formatter.write_str(segment)?;
199        }
200
201        Ok(())
202    }
203}
204
205impl From<ConfigKey> for ConfigPath {
206    fn from(key: ConfigKey) -> Self {
207        Self {
208            segments: vec![key.into_string()],
209        }
210    }
211}
212
213impl FromStr for ConfigPath {
214    type Err = ConfigKeyError;
215
216    fn from_str(input: &str) -> Result<Self, Self::Err> {
217        let trimmed = input.trim();
218
219        if trimmed.is_empty() {
220            return Err(ConfigKeyError::Empty);
221        }
222
223        let mut segments = Vec::new();
224
225        for segment in trimmed.split('.') {
226            segments.push(validated_path_segment(segment)?.to_owned());
227        }
228
229        Ok(Self { segments })
230    }
231}
232
233fn validated_single_segment(input: &str) -> Result<&str, ConfigKeyError> {
234    let trimmed = input.trim();
235
236    if trimmed.is_empty() {
237        return Err(ConfigKeyError::Empty);
238    }
239
240    if trimmed.contains('.') {
241        return Err(ConfigKeyError::DottedSegment);
242    }
243
244    Ok(trimmed)
245}
246
247fn validated_path_segment(input: &str) -> Result<&str, ConfigKeyError> {
248    let trimmed = input.trim();
249
250    if trimmed.is_empty() {
251        return Err(ConfigKeyError::EmptySegment);
252    }
253
254    if trimmed.contains('.') {
255        return Err(ConfigKeyError::DottedSegment);
256    }
257
258    Ok(trimmed)
259}
260
261#[cfg(test)]
262mod tests {
263    use super::{ConfigKey, ConfigKeyError, ConfigPath, ConfigSection};
264    use std::str::FromStr;
265
266    #[test]
267    fn valid_single_key() {
268        let key = ConfigKey::from_str(" port ").expect("key should parse");
269
270        assert_eq!(key.as_str(), "port");
271        assert_eq!(key.to_string(), "port");
272    }
273
274    #[test]
275    fn valid_dotted_path() {
276        let path = ConfigPath::parse("server.port").expect("path should parse");
277
278        assert_eq!(path.len(), 2);
279        assert_eq!(path.first(), Some("server"));
280        assert_eq!(path.last(), Some("port"));
281    }
282
283    #[test]
284    fn invalid_empty_key() {
285        assert_eq!(ConfigKey::new(" "), Err(ConfigKeyError::Empty));
286        assert_eq!(ConfigSection::new(""), Err(ConfigKeyError::Empty));
287        assert_eq!(ConfigPath::parse(""), Err(ConfigKeyError::Empty));
288    }
289
290    #[test]
291    fn invalid_empty_segment() {
292        assert_eq!(
293            ConfigPath::parse("server..port"),
294            Err(ConfigKeyError::EmptySegment)
295        );
296    }
297
298    #[test]
299    fn segment_iteration_and_access_preserve_order() {
300        let path = ConfigPath::parse("server.http.port").expect("path should parse");
301        let segments: Vec<_> = path.segments().collect();
302
303        assert_eq!(segments, vec!["server", "http", "port"]);
304        assert_eq!(path.get(1), Some("http"));
305    }
306
307    #[test]
308    fn display_round_trip() {
309        let path = ConfigPath::parse("server.port").expect("path should parse");
310        let round_trip = ConfigPath::parse(&path.to_string()).expect("display should parse");
311
312        assert_eq!(path, round_trip);
313    }
314}