Skip to main content

rune_cfg/config/
conversion.rs

1// Author: Dustin Pilgrim
2// License: MIT
3
4use std::collections::HashMap;
5
6use crate::{Value, RuneError};
7use crate::ast::ObjectItem;
8
9impl TryFrom<Value> for String {
10    type Error = RuneError;
11
12    fn try_from(value: Value) -> Result<Self, Self::Error> {
13        match value {
14            Value::String(s) => Ok(s),
15            _ => Err(RuneError::TypeError {
16                message: format!("Expected string, got {:?}", value),
17                line: 0,
18                column: 0,
19                hint: Some("Use a string value in your config".into()),
20                code: Some(401),
21            }),
22        }
23    }
24}
25
26impl TryFrom<Value> for f64 {
27    type Error = RuneError;
28
29    fn try_from(value: Value) -> Result<Self, Self::Error> {
30        match value {
31            Value::Number(n) => Ok(n),
32            _ => Err(RuneError::TypeError {
33                message: format!("Expected number, got {:?}", value),
34                line: 0,
35                column: 0,
36                hint: Some("Use a number value in your config".into()),
37                code: Some(402),
38            }),
39        }
40    }
41}
42
43impl TryFrom<Value> for f32 {
44    type Error = RuneError;
45
46    fn try_from(value: Value) -> Result<Self, Self::Error> {
47        match value {
48            Value::Number(n) => Ok(n as f32),
49            _ => Err(RuneError::TypeError {
50                message: format!("Expected number, got {:?}", value),
51                line: 0,
52                column: 0,
53                hint: Some("Use a number value in your config".into()),
54                code: Some(402),
55            }),
56        }
57    }
58}
59
60impl TryFrom<Value> for i32 {
61    type Error = RuneError;
62
63    fn try_from(value: Value) -> Result<Self, Self::Error> {
64        match value {
65            Value::Number(n) => Ok(n as i32),
66            _ => Err(RuneError::TypeError {
67                message: format!("Expected number, got {:?}", value),
68                line: 0,
69                column: 0,
70                hint: Some("Use a number value in your config".into()),
71                code: Some(402),
72            }),
73        }
74    }
75}
76
77impl TryFrom<Value> for i64 {
78    type Error = RuneError;
79
80    fn try_from(value: Value) -> Result<Self, Self::Error> {
81        match value {
82            Value::Number(n) => Ok(n as i64),
83            _ => Err(RuneError::TypeError {
84                message: format!("Expected number, got {:?}", value),
85                line: 0,
86                column: 0,
87                hint: Some("Use a number value in your config".into()),
88                code: Some(402),
89            }),
90        }
91    }
92}
93
94impl TryFrom<Value> for u8 {
95    type Error = RuneError;
96
97    fn try_from(value: Value) -> Result<Self, Self::Error> {
98        match value {
99            Value::Number(n) => {
100                if n >= 0.0 && n <= u8::MAX as f64 {
101                    Ok(n as u8)
102                } else {
103                    Err(RuneError::TypeError {
104                        message: format!("Number {} out of range for u8", n),
105                        line: 0,
106                        column: 0,
107                        hint: Some("Use a number between 0 and 255".into()),
108                        code: Some(407),
109                    })
110                }
111            }
112            _ => Err(RuneError::TypeError {
113                message: format!("Expected number, got {:?}", value),
114                line: 0,
115                column: 0,
116                hint: Some("Use a number value in your config".into()),
117                code: Some(402),
118            }),
119        }
120    }
121}
122
123impl TryFrom<Value> for u16 {
124    type Error = RuneError;
125
126    fn try_from(value: Value) -> Result<Self, Self::Error> {
127        match value {
128            Value::Number(n) => {
129                if n >= 0.0 && n <= u16::MAX as f64 {
130                    Ok(n as u16)
131                } else {
132                    Err(RuneError::TypeError {
133                        message: format!("Number {} out of range for u16", n),
134                        line: 0,
135                        column: 0,
136                        hint: Some("Use a number between 0 and 65535".into()),
137                        code: Some(403),
138                    })
139                }
140            }
141            _ => Err(RuneError::TypeError {
142                message: format!("Expected number, got {:?}", value),
143                line: 0,
144                column: 0,
145                hint: Some("Use a number value in your config".into()),
146                code: Some(402),
147            }),
148        }
149    }
150}
151
152impl TryFrom<Value> for u32 {
153    type Error = RuneError;
154
155    fn try_from(value: Value) -> Result<Self, Self::Error> {
156        match value {
157            Value::Number(n) => {
158                if n >= 0.0 && n <= u32::MAX as f64 {
159                    Ok(n as u32)
160                } else {
161                    Err(RuneError::TypeError {
162                        message: format!("Number {} out of range for u32", n),
163                        line: 0,
164                        column: 0,
165                        hint: Some("Use a number between 0 and 4294967295".into()),
166                        code: Some(408),
167                    })
168                }
169            }
170            _ => Err(RuneError::TypeError {
171                message: format!("Expected number, got {:?}", value),
172                line: 0,
173                column: 0,
174                hint: Some("Use a number value in your config".into()),
175                code: Some(402),
176            }),
177        }
178    }
179}
180
181impl TryFrom<Value> for u64 {
182    type Error = RuneError;
183
184    fn try_from(value: Value) -> Result<Self, Self::Error> {
185        match value {
186            Value::Number(n) => {
187                if n >= 0.0 && n <= u64::MAX as f64 {
188                    Ok(n as u64)
189                } else {
190                    Err(RuneError::TypeError {
191                        message: format!("Number {} out of range for u64", n),
192                        line: 0,
193                        column: 0,
194                        hint: Some("Use a positive number within u64 range".into()),
195                        code: Some(406),
196                    })
197                }
198            }
199            _ => Err(RuneError::TypeError {
200                message: format!("Expected number, got {:?}", value),
201                line: 0,
202                column: 0,
203                hint: Some("Use a number value in your config".into()),
204                code: Some(402),
205            }),
206        }
207    }
208}
209
210impl TryFrom<Value> for usize {
211    type Error = RuneError;
212
213    fn try_from(value: Value) -> Result<Self, Self::Error> {
214        match value {
215            Value::Number(n) => {
216                if n >= 0.0 && n.is_finite() {
217                    Ok(n as usize)
218                } else {
219                    Err(RuneError::TypeError {
220                        message: format!("Number {} out of range for usize", n),
221                        line: 0,
222                        column: 0,
223                        hint: Some("Use a positive integer".into()),
224                        code: Some(409),
225                    })
226                }
227            }
228            _ => Err(RuneError::TypeError {
229                message: format!("Expected number, got {:?}", value),
230                line: 0,
231                column: 0,
232                hint: Some("Use a number value in your config".into()),
233                code: Some(402),
234            }),
235        }
236    }
237}
238
239impl TryFrom<Value> for bool {
240    type Error = RuneError;
241
242    fn try_from(value: Value) -> Result<Self, Self::Error> {
243        match value {
244            Value::Bool(b) => Ok(b),
245            Value::Reference(ref path) if path.len() == 1 => {
246                let ref_name = &path[0];
247                if ref_name.to_lowercase().starts_with("tru") || ref_name.to_lowercase().starts_with("fal") {
248                    Err(RuneError::TypeError {
249                        message: format!("Invalid boolean value '{}'. Did you mean 'true' or 'false'?", ref_name),
250                        line: 0,
251                        column: 0,
252                        hint: None,
253                        code: Some(404),
254                    })
255                } else {
256                    Err(RuneError::TypeError {
257                        message: format!("Expected boolean (true/false), got reference to '{}'", ref_name),
258                        line: 0,
259                        column: 0,
260                        hint: None,
261                        code: Some(404),
262                    })
263                }
264            }
265            _ => Err(RuneError::TypeError {
266                message: format!("Expected boolean, got {:?}", value),
267                line: 0,
268                column: 0,
269                hint: None,
270                code: Some(404),
271            }),
272        }
273    }
274}
275
276impl<T> TryFrom<Value> for Vec<T>
277where
278    T: TryFrom<Value, Error = RuneError>,
279{
280    type Error = RuneError;
281
282    fn try_from(value: Value) -> Result<Self, Self::Error> {
283        match value {
284            Value::Array(arr) => {
285                let mut result = Vec::new();
286                for item in arr {
287                    result.push(T::try_from(item)?);
288                }
289                Ok(result)
290            }
291            _ => Err(RuneError::TypeError {
292                message: format!("Expected array, got {:?}", value),
293                line: 0,
294                column: 0,
295                hint: Some("Use an array [...] in your config".into()),
296                code: Some(405),
297            }),
298        }
299    }
300}
301
302impl<T> TryFrom<Value> for Option<T>
303where
304    T: TryFrom<Value, Error = RuneError>,
305{
306    type Error = RuneError;
307
308    fn try_from(value: Value) -> Result<Self, Self::Error> {
309        match value {
310            Value::Null => Ok(None),
311            v => Ok(Some(T::try_from(v)?)),
312        }
313    }
314}
315
316/// Convert an object `Value` to a map, **expecting the object to be fully-resolved**.
317///
318/// With block `if ... endif` support, objects can contain conditional items. By the time you
319/// convert to a `HashMap`, those conditionals should have been evaluated and flattened into
320/// plain assignments.
321///
322/// If this conversion sees an `IfBlock`, it returns a type error with a helpful hint.
323fn object_items_to_map(items: Vec<ObjectItem>) -> Result<HashMap<String, Value>, RuneError> {
324    let mut map = HashMap::new();
325
326    for item in items {
327        match item {
328            ObjectItem::Assign(key, val) => {
329                map.insert(key, val);
330            }
331            ObjectItem::IfBlock(_) => {
332                return Err(RuneError::TypeError {
333                    message: "Expected object with only key/value pairs, but found an if-block".into(),
334                    line: 0,
335                    column: 0,
336                    hint: Some("This usually means if-blocks were not resolved. Ensure you resolve/evaluate the config before converting it to a HashMap.".into()),
337                    code: Some(410),
338                });
339            }
340        }
341    }
342
343    Ok(map)
344}
345
346impl TryFrom<Value> for HashMap<String, Value> {
347    type Error = RuneError;
348
349    fn try_from(value: Value) -> Result<Self, Self::Error> {
350        match value {
351            Value::Object(items) => object_items_to_map(items),
352            _ => Err(RuneError::TypeError {
353                message: format!("Expected object, got {:?}", value),
354                line: 0,
355                column: 0,
356                hint: Some("Use an object block in your config".into()),
357                code: Some(410),
358            }),
359        }
360    }
361}
362
363impl TryFrom<Value> for HashMap<String, String> {
364    type Error = RuneError;
365
366    fn try_from(value: Value) -> Result<Self, Self::Error> {
367        match value {
368            Value::Object(items) => {
369                let base = object_items_to_map(items)?;
370                let mut map = HashMap::new();
371                for (key, val) in base {
372                    let string_val = String::try_from(val)?;
373                    map.insert(key, string_val);
374                }
375                Ok(map)
376            }
377            _ => Err(RuneError::TypeError {
378                message: format!("Expected object, got {:?}", value),
379                line: 0,
380                column: 0,
381                hint: Some("Use an object block with string values".into()),
382                code: Some(410),
383            }),
384        }
385    }
386}
387
388impl TryFrom<Value> for (String, String) {
389    type Error = RuneError;
390
391    fn try_from(value: Value) -> Result<Self, Self::Error> {
392        match value {
393            Value::Array(arr) if arr.len() == 2 => {
394                let first = String::try_from(arr[0].clone())?;
395                let second = String::try_from(arr[1].clone())?;
396                Ok((first, second))
397            }
398            _ => Err(RuneError::TypeError {
399                message: "Expected array with exactly 2 string elements".into(),
400                line: 0,
401                column: 0,
402                hint: Some("Use [\"key\", \"value\"] format".into()),
403                code: Some(411),
404            }),
405        }
406    }
407}
408
409impl TryFrom<Value> for (String, Value) {
410    type Error = RuneError;
411
412    fn try_from(value: Value) -> Result<Self, Self::Error> {
413        match value {
414            Value::Array(arr) if arr.len() == 2 => {
415                let key = String::try_from(arr[0].clone())?;
416                let val = arr[1].clone();
417                Ok((key, val))
418            }
419            _ => Err(RuneError::TypeError {
420                message: "Expected array with exactly 2 elements (key and value)".into(),
421                line: 0,
422                column: 0,
423                hint: Some("Use [\"key\", value] format".into()),
424                code: Some(411),
425            }),
426        }
427    }
428}
429
430impl RuneError {
431    /// Helper for file-related errors when loading/parsing configs.
432    ///
433    /// Keeps a consistent error code and a friendly default hint.
434    pub fn file_error(message: String, path: String) -> Self {
435        RuneError::FileError {
436            message,
437            path,
438            hint: Some("Check file path and permissions".into()),
439            code: Some(300),
440        }
441    }
442}