rfham_config/paths.rs
1//! Dot-separated configuration key paths for `rfham-config`.
2//!
3//! [`ConfigPath`] is a validated sequence of [`Name`](rfham_core::id::Name) segments
4//! joined by `.`. It parses from strings like `"station.callsign"` and displays back
5//! in the same form.
6//!
7//! # Examples
8//!
9//! ```rust
10//! use rfham_config::paths::ConfigPath;
11//!
12//! let path: ConfigPath = "station.callsign".parse().unwrap();
13//! assert_eq!("station.callsign", path.to_string());
14//! assert!("".parse::<ConfigPath>().is_err());
15//! ```
16
17use core::{fmt::Display, str::FromStr};
18use rfham_core::{Name, error::CoreError};
19
20// ------------------------------------------------------------------------------------------------
21// Public Macros
22// ------------------------------------------------------------------------------------------------
23
24// ------------------------------------------------------------------------------------------------
25// Public Types
26// ------------------------------------------------------------------------------------------------
27
28#[derive(Clone, Debug, PartialEq, Eq)]
29pub struct ConfigPath(Vec<Name>);
30
31// ------------------------------------------------------------------------------------------------
32// Public Functions
33// ------------------------------------------------------------------------------------------------
34
35// ------------------------------------------------------------------------------------------------
36// Private Macros
37// ------------------------------------------------------------------------------------------------
38
39// ------------------------------------------------------------------------------------------------
40// Private Types
41// ------------------------------------------------------------------------------------------------
42
43// ------------------------------------------------------------------------------------------------
44// Implementations
45// ------------------------------------------------------------------------------------------------
46
47impl Display for ConfigPath {
48 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
49 write!(
50 f,
51 "{}",
52 self.0
53 .iter()
54 .map(Name::to_string)
55 .collect::<Vec<_>>()
56 .join(".")
57 )
58 }
59}
60
61impl FromStr for ConfigPath {
62 type Err = CoreError;
63
64 fn from_str(s: &str) -> Result<Self, Self::Err> {
65 let results: Result<Vec<Name>, CoreError> = s.split('.').map(Name::from_str).collect();
66 let values = results?;
67 if !values.is_empty() {
68 Ok(Self(values))
69 } else {
70 Err(CoreError::InvalidValueFromStr(s.to_string(), "ConfigPath"))
71 }
72 }
73}
74
75impl From<Name> for ConfigPath {
76 fn from(value: Name) -> Self {
77 Self::from(vec![value])
78 }
79}
80
81impl From<Vec<Name>> for ConfigPath {
82 fn from(values: Vec<Name>) -> Self {
83 assert!(
84 !values.is_empty(),
85 "ConfigPath must have at least one component"
86 );
87 Self(values)
88 }
89}
90
91// ------------------------------------------------------------------------------------------------
92// Private Functions
93// ------------------------------------------------------------------------------------------------
94
95// ------------------------------------------------------------------------------------------------
96// Sub-Modules
97// ------------------------------------------------------------------------------------------------
98
99// ------------------------------------------------------------------------------------------------
100// Unit Tests
101// ------------------------------------------------------------------------------------------------
102
103#[cfg(test)]
104mod tests {
105 use super::ConfigPath;
106 use pretty_assertions::assert_eq;
107
108 #[test]
109 fn test_single_component() {
110 let path: ConfigPath = "station".parse().unwrap();
111 assert_eq!("station", path.to_string());
112 }
113
114 #[test]
115 fn test_multi_component() {
116 let path: ConfigPath = "station.callsign".parse().unwrap();
117 assert_eq!("station.callsign", path.to_string());
118 }
119
120 #[test]
121 fn test_empty_string_is_error() {
122 assert!("".parse::<ConfigPath>().is_err());
123 }
124
125 #[test]
126 fn test_invalid_component_is_error() {
127 // Names cannot contain spaces
128 assert!("station.has space".parse::<ConfigPath>().is_err());
129 }
130}