1use crate::{PersistentStr, PropertyValue, ast::Origin};
2
3#[derive(thiserror::Error, Debug)]
7#[error("Conversion to {type_name} failed for value {value} ({origin})")]
8pub struct ConversionFailed {
9 pub type_name: &'static str,
10 pub value: PersistentStr,
11 pub origin: Origin,
12}
13
14pub type ConversionResult<T> = Result<T, ConversionFailed>;
15
16pub trait TypedProperty {
38 fn from_value(value: &PropertyValue) -> ConversionResult<Self>
39 where
40 Self: Sized;
41}
42
43pub trait ToType {
47 fn to_type<T: TypedProperty>(&self) -> ConversionResult<T>
48 where
49 Self: Sized;
50}
51impl ToType for PropertyValue {
52 fn to_type<T: TypedProperty>(&self) -> ConversionResult<T> {
53 T::from_value(self)
54 }
55}
56
57impl TypedProperty for bool {
58 fn from_value(prop: &PropertyValue) -> ConversionResult<Self> {
59 prop.value.parse::<bool>().or_conversion_failed(prop)
60 }
61}
62
63impl TypedProperty for String {
64 fn from_value(prop: &PropertyValue) -> ConversionResult<Self> {
65 Ok(prop.value.to_string())
66 }
67}
68
69impl TypedProperty for std::path::PathBuf {
70 fn from_value(prop: &PropertyValue) -> ConversionResult<Self> {
71 Ok(std::path::PathBuf::from(prop.value.as_ref()))
72 }
73}
74
75#[derive(Debug)]
79pub struct CommaSeparatedList(pub Vec<String>);
80impl TypedProperty for CommaSeparatedList {
81 fn from_value(prop: &PropertyValue) -> ConversionResult<Self> {
82 Ok(CommaSeparatedList(
83 prop.value
84 .as_ref()
85 .split(",")
86 .map(|s| s.trim().to_string())
87 .collect(),
88 ))
89 }
90}
91
92macro_rules! num_type {
93 ($num:ty) => {
94 impl TypedProperty for $num {
95 fn from_value(prop: &PropertyValue) -> ConversionResult<Self> {
96 prop.value.parse::<$num>().or_conversion_failed(prop)
97 }
98 }
99 };
100}
101
102num_type!(i8);
103num_type!(u8);
104
105num_type!(i16);
106num_type!(u16);
107
108num_type!(i32);
109num_type!(u32);
110num_type!(f32);
111
112num_type!(i64);
113num_type!(u64);
114num_type!(f64);
115
116#[cfg(feature = "extra_conversions")]
117pub mod extras {
118 use super::*;
119
120 impl TypedProperty for std::time::Duration {
122 fn from_value(prop: &PropertyValue) -> ConversionResult<Self> {
123 humantime::parse_duration(prop.value.as_ref()).or_conversion_failed(prop)
124 }
125 }
126
127 impl TypedProperty for std::time::SystemTime {
131 fn from_value(prop: &PropertyValue) -> ConversionResult<Self> {
132 Ok(chrono::DateTime::parse_from_rfc3339(prop.value.as_ref())
133 .or_conversion_failed(prop)?
134 .into())
135 }
136 }
137
138 impl TypedProperty for chrono::DateTime<chrono::FixedOffset> {
139 fn from_value(prop: &PropertyValue) -> ConversionResult<Self> {
140 let date_time = chrono::DateTime::parse_from_rfc3339(prop.value.as_ref())
141 .or_conversion_failed(prop)?;
142 let offset = date_time.offset();
143 Ok(chrono::DateTime::from_naive_utc_and_offset(
144 date_time.naive_utc(),
145 *offset,
146 ))
147 }
148 }
149}
150
151pub trait OrConversionFailed<T> {
153 fn or_conversion_failed(self, original: &PropertyValue) -> ConversionResult<T>;
154}
155impl<T, E> OrConversionFailed<T> for std::result::Result<T, E> {
156 fn or_conversion_failed(self, original: &PropertyValue) -> ConversionResult<T> {
157 match self {
158 Ok(value) => Ok(value),
159 Err(_) => Err(ConversionFailed {
160 type_name: simple_type_name::<T>().unwrap_or("UNKNOWN"),
161 value: original.value.clone(),
162 origin: original.origin.clone(),
163 }),
164 }
165 }
166}
167
168fn simple_type_name<T>() -> Option<&'static str> {
169 std::any::type_name::<T>().split("::").last()
170}
171
172#[cfg(test)]
173mod tests {
174 use super::*;
175 use crate::{CcsResult, Context};
176 use assert_approx_eq::assert_approx_eq;
177 use std::time::{Duration, SystemTime};
178
179 #[test]
180 fn get_duration() -> CcsResult<()> {
181 let contents = r#"
182 duration1 = '5ms'
183 duration2 = '3h'
184 constrained { duration2 = '1d5h' }
185 "#;
186 let context = Context::from_str_without_tracing(contents)?;
187
188 assert_eq!(
189 context.get("duration1")?.to_type::<Duration>()?,
190 Duration::from_millis(5)
191 );
192 assert_eq!(
193 context.get("duration2")?.to_type::<Duration>()?,
194 Duration::from_hours(3)
195 );
196
197 let context = context.constrain("constrained");
198 assert_eq!(
199 context.get("duration2")?.to_type::<Duration>()?,
200 Duration::from_hours(29)
201 );
202
203 Ok(())
204 }
205
206 #[test]
207 fn get_system_time() -> CcsResult<()> {
208 let contents = r#"
209 time1 = '2025-01-01T12:34:56Z'
210 time2 = "1999-12-31T12:23:01-04:00"
211 "#;
212 let context = Context::from_str_without_tracing(contents)?;
213
214 assert_eq!(
215 context.get("time1")?.to_type::<SystemTime>()?,
216 chrono::DateTime::parse_from_rfc3339("2025-01-01 12:34:56Z")
217 .unwrap()
218 .into()
219 );
220 assert_eq!(
221 context.get("time2")?.to_type::<SystemTime>()?,
222 chrono::DateTime::parse_from_rfc3339("1999-12-31T12:23:01-04:00")
223 .unwrap()
224 .into()
225 );
226
227 Ok(())
228 }
229
230 #[test]
231 fn get_date_time() -> CcsResult<()> {
232 let contents = r#"
233 time = "1999-12-31T12:23:01-04:00"
234 "#;
235 let context = Context::from_str_without_tracing(contents)?;
236
237 assert_eq!(
238 context
239 .get("time")?
240 .to_type::<chrono::DateTime<chrono::FixedOffset>>()?,
241 chrono::DateTime::parse_from_rfc3339("1999-12-31T12:23:01-04:00").unwrap()
242 );
243
244 Ok(())
245 }
246
247 #[test]
248 fn get_comma_separated_list() -> CcsResult<()> {
249 let contents = r#"
250 oneLineList = "first, second ,third"
251 multiLineList = "
252 this,
253 that,
254 the other,
255 " // Extra comma will make an empty element
256 "#;
257 let context = Context::from_str_without_tracing(contents)?;
258
259 assert_eq!(
260 context
261 .get("oneLineList")?
262 .to_type::<CommaSeparatedList>()?
263 .0,
264 ["first", "second", "third"]
265 );
266
267 assert_eq!(
268 context
269 .get("multiLineList")?
270 .to_type::<CommaSeparatedList>()?
271 .0,
272 ["this", "that", "the other", ""]
273 );
274
275 Ok(())
276 }
277
278 #[test]
279 fn get_arithmetic_types() -> CcsResult<()> {
280 let contents = r#"
281 boolVal = false
282 intVal = 123
283 floatVal = 123.4
284 "#;
285 let context = Context::from_str_without_tracing(contents)?;
286
287 let bool_val = context.get("boolVal")?;
288 let int_val = context.get("intVal")?;
289 let float_val = context.get("floatVal")?;
290
291 assert!(!(bool_val.to_type::<bool>()?));
292
293 assert_eq!(int_val.to_type::<u32>()?, 123u32);
294 assert_eq!(int_val.to_type::<i64>()?, 123i64);
295
296 assert_approx_eq!(float_val.to_type::<f32>()?, 123.4f32);
297 assert_approx_eq!(float_val.to_type::<f64>()?, 123.4f64);
298
299 Ok(())
300 }
301
302 #[test]
303 fn realistic_constraints() -> CcsResult<()> {
304 let contents = r#"
305 env.prod module.logger {
306 level = INFO
307 }
308 env.dev module.logger {
309 level = DEBUG
310 }
311 env.dev module.debug {
312 format = "example format"
313 }
314 "#;
315 let context = Context::from_str_without_tracing(contents)?;
316
317 let prod_logger = context
318 .constrain(("env", "prod"))
319 .constrain(("module", "logger"));
320 let dev_logger = context
321 .constrain(("env", "dev"))
322 .constrain(("module", "logger"));
323
324 assert_eq!(&*prod_logger.get_value("level")?, "INFO");
325 assert_eq!(&*dev_logger.get_value("level")?, "DEBUG");
326
327 let dev_debug_module = context
328 .constrain(("env", "dev"))
329 .constrain(("module", "debug"));
330
331 assert_eq!(&*dev_debug_module.get_value("format")?, "example format");
332 assert!(dev_debug_module.get_value("level").is_err());
333
334 Ok(())
335 }
336}