Skip to main content

config/
path.rs

1use std::path::Path;
2
3/// Gets the key delimiter used in configuration paths.
4pub const fn delimiter() -> char {
5    ':'
6}
7
8/// Combines the specified segments into one path.
9///
10/// # Arguments
11///
12/// * `segments` - The segments to combine into a path
13#[inline]
14pub fn combine(segments: &[&str]) -> String {
15    segments.join(":")
16}
17
18/// Determines if the given text starts with the provided prefix text.
19///
20/// # Arguments
21///
22/// * `text` - The text to test
23/// * `other` - The prefix to check for
24///
25/// # Remarks
26///
27/// The comparison is case-insensitive.
28pub fn starts_with(text: impl AsRef<str>, other: impl AsRef<str>) -> bool {
29    let a = text.as_ref();
30    let b = other.as_ref();
31
32    a.len() >= b.len() && a.chars().zip(b.chars()).all(|(l, r)| l.eq_ignore_ascii_case(&r))
33}
34
35/// Extracts the last path segment from the path.
36///
37/// # Arguments
38///
39/// * `path` - The path to extract the key from
40pub fn last(path: &str) -> &str {
41    if let Some(index) = path.rfind(delimiter()) {
42        &path[(index + 1)..]
43    } else {
44        path
45    }
46}
47
48/// Extracts the path corresponding to the parent node for a given path.
49///
50/// # Arguments
51///
52/// * `path` - The path to extract the parent path from
53pub fn parent(path: &str) -> &str {
54    if let Some(index) = path.rfind(delimiter()) {
55        &path[..index]
56    } else {
57        ""
58    }
59}
60
61/// Gets the name of a provider from a file path.
62///
63/// # Arguments
64///
65/// * `path` - The path to extract the provider name from
66/// * `name` - The default name to use if the path doesn't have a file name
67pub fn provider<'a>(path: &'a Path, name: &'a str) -> &'a str {
68    path.file_name().map(|n| n.to_str().unwrap_or(name)).unwrap_or(name)
69}
70
71/// Extracts the next path segment with a given base.
72///
73/// # Arguments
74///
75/// * `path` - The path to extract the key from
76/// * `base` - The optional base path to match against
77pub fn next<'a>(path: &'a str, base: Option<&str>) -> Option<&'a str> {
78    if path.is_empty() {
79        None
80    } else if let Some(base) = base {
81        let len = base.len();
82
83        if path.len() > len && path[..len].eq_ignore_ascii_case(base) && path.chars().nth(len) == Some(delimiter()) {
84            if let Some(index) = path[(len + 1)..].find(delimiter()) {
85                Some(&path[..(len + 1 + index)])
86            } else {
87                Some(path)
88            }
89        } else {
90            None
91        }
92    } else if let Some(index) = path.find(delimiter()) {
93        Some(&path[..index])
94    } else {
95        Some(path)
96    }
97}
98
99#[cfg(test)]
100mod tests {
101    use super::*;
102    use test_case::test_case;
103
104    #[test_case(&["parent", ""], "parent:" ; "with 1 segment")]
105    #[test_case(&["parent", "", ""], "parent::" ; "with 2 segments")]
106    #[test_case(&["parent", "", "", "key"], "parent:::key" ; "with segments in between")]
107    fn combine_with_empty_segment_leaves_delimiter(segments: &[&str], expected: &str) {
108        // arrange
109
110        // act
111        let path = combine(segments);
112
113        // assert
114        assert_eq!(&path, expected);
115    }
116
117    #[test_case("", "" ; "when empty")]
118    #[test_case(":::", "" ; "when only delimiters")]
119    #[test_case("a::b:::c", "c" ; "with empty segments in the middle")]
120    #[test_case("a:::b:", "" ; "when last segment is empty")]
121    #[test_case("key", "key" ; "with no parent")]
122    #[test_case(":key", "key" ; "with 1 empty parent")]
123    #[test_case("::key", "key" ; "with 2 empty parents")]
124    #[test_case("parent:key", "key" ; "with parent")]
125    fn last_should_return_expected_segment(path: &str, expected: &str) {
126        // arrange
127
128        // act
129        let key = last(path);
130
131        // assert
132        assert_eq!(key, expected);
133    }
134
135    #[test_case("", "" ; "when empty")]
136    #[test_case(":::", "::" ; "when only delimiters")]
137    #[test_case("a::b:::c", "a::b::" ; "with empty segments in the middle")]
138    #[test_case("a:::b:", "a:::b" ; "when last segment is empty")]
139    #[test_case("key", "" ; "with no parent")]
140    #[test_case(":key", "" ; "with 1 empty parent")]
141    #[test_case("::key", ":" ; "with 2 empty parents")]
142    #[test_case("parent:key", "parent" ; "with parent")]
143    fn parent_should_return_expected_segment(path: &str, expected: &str) {
144        // arrange
145
146        // act
147        let key = parent(path);
148
149        // assert
150        assert_eq!(key, expected);
151    }
152
153    #[test_case("a", Some("") ; "when empty")]
154    #[test_case("a", Some("a:b") ; "when path is too short")]
155    #[test_case("a:b", Some("a:b") ; "when path and base are equal")]
156    fn next_should_return_none(path: &str, base: Option<&str>) {
157        // arrange
158
159        // act
160        let key = next(path, base);
161
162        // assert
163        assert_eq!(key, None);
164    }
165
166    #[test_case("a:b", Some("a"), Some("a:b") ; "when base has 1 segment")]
167    #[test_case("a:b:c", Some("a:b"), Some("a:b:c") ; "when base has 2 segments")]
168    #[test_case("a:b:c", Some("a"), Some("a:b") ; "when path has 3 segments")]
169    fn next_should_return_some(path: &str, base: Option<&str>, expected: Option<&str>) {
170        // arrange
171
172        // act
173        let key = next(path, base);
174
175        // assert
176        assert_eq!(key, expected);
177    }
178}