1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4use core::{fmt, str::FromStr};
5use std::error::Error;
6
7#[derive(Clone, Copy, Debug, Eq, PartialEq)]
9pub enum ConfigKeyError {
10 Empty,
12 EmptySegment,
14 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#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
34pub struct ConfigKey(String);
35
36impl ConfigKey {
37 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 #[must_use]
48 pub fn as_str(&self) -> &str {
49 &self.0
50 }
51
52 #[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#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
75pub struct ConfigSection(String);
76
77impl ConfigSection {
78 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 #[must_use]
89 pub fn as_str(&self) -> &str {
90 &self.0
91 }
92
93 #[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#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
116pub struct ConfigPath {
117 segments: Vec<String>,
118}
119
120impl ConfigPath {
121 pub fn parse(input: &str) -> Result<Self, ConfigKeyError> {
127 input.parse()
128 }
129
130 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 #[must_use]
157 pub const fn len(&self) -> usize {
158 self.segments.len()
159 }
160
161 #[must_use]
163 pub const fn is_empty(&self) -> bool {
164 self.segments.is_empty()
165 }
166
167 pub fn segments(&self) -> impl Iterator<Item = &str> {
169 self.segments.iter().map(String::as_str)
170 }
171
172 #[must_use]
174 pub fn get(&self, index: usize) -> Option<&str> {
175 self.segments.get(index).map(String::as_str)
176 }
177
178 #[must_use]
180 pub fn first(&self) -> Option<&str> {
181 self.get(0)
182 }
183
184 #[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}