Skip to main content

aam_core/
from_aam.rs

1use crate::aam::AAM;
2use crate::aaml::parsing;
3use crate::error::{AamlError, ErrorDiagnostics};
4use std::collections::HashMap;
5
6/// Trait for deserializing Rust types from AAM string values.
7///
8/// `from_aam_str` uses the **same AAM pipeline engine** as `AAM::parse`.
9/// Two formats are supported:
10///
11/// 1. **Inline object** — `{ field = value, ... }`
12/// 2. **Full AAM document** — `key = value\n...`
13///
14/// # Provided implementations
15///
16/// Built-in: `i8`, `i16`, `i32`, `i64`, `isize`, `u8`, `u16`, `u32`, `u64`,
17/// `usize`, `f32`, `f64`, `bool`, `String`.
18///
19/// Generic: `Option<T>`, `Vec<T>`, `HashMap<String, T>`.
20///
21/// # Manual implementation
22///
23/// ```
24/// use aam_core::from_aam::FromAam;
25/// use aam_core::from_aam::parse_fields;
26/// use aam_core::error::AamlError;
27///
28/// struct Point { x: f64, y: f64 }
29///
30/// impl FromAam for Point {
31///     fn from_aam_str(value: &str) -> Result<Self, AamlError> {
32///         let fields = parse_fields(value)?;
33///         Ok(Point {
34///             x: f64::from_aam_str(fields.get("x").ok_or_else(|| {
35///                 AamlError::InvalidValue { details: "missing x".into(), expected: "number".into(), diagnostics: None }
36///             })?)?,
37///             y: f64::from_aam_str(fields.get("y").ok_or_else(|| {
38///                 AamlError::InvalidValue { details: "missing y".into(), expected: "number".into(), diagnostics: None }
39///             })?)?,
40///         })
41///     }
42/// }
43///
44/// let p = Point::from_aam_str("x = 1.5\ny = 2.5\n").unwrap();
45/// assert_eq!(p.x, 1.5);
46/// ```
47///
48/// # Derive macro (requires `aam-derive` / `aam-rs`)
49///
50/// In `Cargo.toml`: `aam-rs = "2"`\
51/// In code: `use aam_rs::FromAam; use aam_rs::from_aam::FromAam as _;`
52///
53/// ```ignore
54/// use aam_rs::FromAam;
55/// use aam_rs::from_aam::FromAam as _;
56///
57/// #[derive(Debug, Clone, FromAam, PartialEq)]
58/// struct Server { host: String, port: i32, debug: Option<bool>, tags: Vec<String> }
59///
60/// let s = Server::from_aam_str("host = localhost\nport = 8080\ntags = [api, worker]\n").unwrap();
61///
62/// #[derive(Debug, Clone, FromAam, PartialEq)]
63/// struct Address { host: String, port: i32 }
64///
65/// #[derive(Debug, Clone, FromAam, PartialEq)]
66/// struct App { name: String, address: Address }
67///
68/// let app = App::from_aam_str("name = proxy\naddress = { host = 10.0.0.1, port = 3128 }\n").unwrap();
69/// ```
70pub trait FromAam: Sized {
71    /// Deserializes a value from an AAM string.
72    ///
73    /// # Errors
74    ///
75    /// Returns `AamlError` if the string cannot be parsed into `Self`.
76    fn from_aam_str(value: &str) -> Result<Self, AamlError>;
77}
78
79impl FromAam for String {
80    fn from_aam_str(value: &str) -> Result<Self, AamlError> {
81        Ok(value.to_string())
82    }
83}
84
85macro_rules! impl_from_aam_parse {
86    ($($t:ty),* $(,)?) => {
87        $(
88            impl FromAam for $t {
89                fn from_aam_str(value: &str) -> Result<Self, AamlError> {
90                    value.trim().parse().map_err(|e| AamlError::InvalidValue {
91                        details: format!("cannot parse '{}' as {}: {}", value, stringify!($t), e),
92                        expected: format!("valid {}", stringify!($t)),
93                        diagnostics: None,
94                    })
95                }
96            }
97        )*
98    };
99}
100
101impl_from_aam_parse!(
102    i8, i16, i32, i64, isize, u8, u16, u32, u64, usize, f32, f64, bool
103);
104
105impl<T: FromAam> FromAam for Option<T> {
106    fn from_aam_str(value: &str) -> Result<Self, AamlError> {
107        let v = value.trim();
108        if v.is_empty() {
109            return Ok(None);
110        }
111        T::from_aam_str(value).map(Some)
112    }
113}
114
115impl<T: FromAam> FromAam for Vec<T> {
116    fn from_aam_str(value: &str) -> Result<Self, AamlError> {
117        let v = value.trim();
118        if v.starts_with('[') && v.ends_with(']') {
119            let items = parse_list_items(v)?;
120            return items.iter().map(|s| T::from_aam_str(s)).collect();
121        }
122        T::from_aam_str(v).map(|item| vec![item])
123    }
124}
125
126impl<T: FromAam, S: ::std::hash::BuildHasher + Default> FromAam for HashMap<String, T, S> {
127    fn from_aam_str(value: &str) -> Result<Self, AamlError> {
128        let fields = parse_object_fields(value)?;
129        fields
130            .into_iter()
131            .map(|(k, v)| T::from_aam_str(&v).map(|tv| (k, tv)))
132            .collect()
133    }
134}
135
136/// Parses the fields of an inline AAM object `{ k = v, ... }` into a map.
137///
138/// # Errors
139///
140/// Returns an error if the value is not a valid inline object.
141pub fn parse_object_fields(value: &str) -> Result<HashMap<String, String>, AamlError> {
142    Ok(parsing::parse_inline_object(value)?.into_iter().collect())
143}
144
145/// Parses key-value pairs using the **AAM pipeline** (`AAM::parse`).
146///
147/// - Inline objects `{ k = v, ... }` are wrapped as `_ = { ... }` before
148///   entering the pipeline, then unwrapped.
149/// - Full documents `k = v\n...` go through the pipeline directly.
150///
151/// # Errors
152///
153/// Returns an error if the value cannot be parsed into key-value pairs.
154pub fn parse_fields(value: &str) -> Result<HashMap<String, String>, AamlError> {
155    let v = value.trim();
156
157    // Inline objects are parsed directly so that whitespace inside values is
158    // preserved. Rounding them through the AAM pipeline strips syntactic
159    // spaces and can collapse values like `export CC=gcc` into `exportCC=gcc`.
160    if v.starts_with('{') && v.ends_with('}') {
161        return parse_object_fields(v);
162    }
163
164    let aam = AAM::parse(v).map_err(|mut errs| errs.remove(0))?;
165    let map: HashMap<String, String> = aam
166        .iter()
167        .map(|(k, val)| (k.to_string(), val.to_string()))
168        .collect();
169    Ok(map)
170}
171
172/// Parses a list literal `[a, b, c]` through the **AAM pipeline**.
173///
174/// # Errors
175///
176/// Returns an error if the value is not a valid list literal.
177pub fn parse_list_items(value: &str) -> Result<Vec<String>, AamlError> {
178    let v = value.trim();
179    let input = format!("_ = {v}");
180    let aam = AAM::parse(&input).map_err(|mut errs| errs.remove(0))?;
181    let raw = aam.get("_").unwrap_or("");
182    crate::types_aam::list::ListType::parse_items(raw).ok_or_else(|| AamlError::InvalidValue {
183        details: format!("'{v}' is not a valid list literal"),
184        expected: "[item, item, ...] format".to_string(),
185        diagnostics: None,
186    })
187}
188
189/// Retrieves and deserializes a value from an [`AAM`] document by key.
190///
191/// # Errors
192///
193/// Returns an error if the key is not found or the value cannot be parsed into `T`.
194pub fn get_aam<T: FromAam>(aam: &AAM, key: &str) -> Result<T, AamlError> {
195    let value = aam.get(key).ok_or_else(|| AamlError::NotFound {
196        key: key.to_string(),
197        context: "AAM map".to_string(),
198        diagnostics: Some(Box::new(ErrorDiagnostics::new(
199            "Key not found",
200            format!("Key '{key}' not found in AAM document"),
201            "Check that the key exists and the document was parsed correctly",
202        ))),
203    })?;
204    T::from_aam_str(value)
205}
206
207/// Retrieves and deserializes an optional value from an [`AAM`] document by key.
208///
209/// # Errors
210///
211/// Returns an error if the key is found but the value cannot be parsed into `T`.
212pub fn get_opt_aam<T: FromAam>(aam: &AAM, key: &str) -> Result<Option<T>, AamlError> {
213    aam.get(key)
214        .map_or_else(|| Ok(None), |value| T::from_aam_str(value).map(Some))
215}
216
217#[cfg(test)]
218#[allow(clippy::float_cmp)]
219mod tests {
220    use super::*;
221
222    #[test]
223    fn test_primitives() {
224        assert_eq!(i32::from_aam_str("42").unwrap(), 42);
225        assert!((f64::from_aam_str("3.14").unwrap() - std::f64::consts::PI).abs() < 0.01);
226        assert!(bool::from_aam_str("true").unwrap());
227        assert_eq!(String::from_aam_str("hello").unwrap(), "hello");
228    }
229
230    #[test]
231    fn test_option() {
232        assert_eq!(Option::<i32>::from_aam_str("42").unwrap(), Some(42));
233        assert_eq!(Option::<i32>::from_aam_str("").unwrap(), None);
234    }
235
236    #[test]
237    fn test_vec() {
238        assert_eq!(
239            Vec::<i32>::from_aam_str("[1, 2, 3]").unwrap(),
240            vec![1, 2, 3]
241        );
242        assert_eq!(
243            Vec::<String>::from_aam_str("[a, b, c]").unwrap(),
244            vec!["a", "b", "c"]
245        );
246        assert_eq!(Vec::<i32>::from_aam_str("42").unwrap(), vec![42]);
247    }
248
249    #[test]
250    fn test_parse_object_fields() {
251        let fields = parse_object_fields("{ x = 1.0, y = 2.0, name = hello }").unwrap();
252        assert_eq!(fields.get("x").unwrap(), "1.0");
253        assert_eq!(fields.get("y").unwrap(), "2.0");
254        assert_eq!(fields.get("name").unwrap(), "hello");
255    }
256
257    #[test]
258    fn test_parse_list_items() {
259        let items = parse_list_items("[rust, aam, config]").unwrap();
260        assert_eq!(items, vec!["rust", "aam", "config"]);
261    }
262
263    #[test]
264    fn test_parse_fields_inline() {
265        let fields = parse_fields("{ x = 1.0, y = 2.0 }").unwrap();
266        assert_eq!(fields.get("x").unwrap(), "1.0");
267        assert_eq!(fields.get("y").unwrap(), "2.0");
268    }
269
270    #[test]
271    fn test_parse_fields_full_aam() {
272        let fields = parse_fields("x = 1.0\ny = 2.0\n").unwrap();
273        assert_eq!(fields.get("x").unwrap(), "1.0");
274        assert_eq!(fields.get("y").unwrap(), "2.0");
275    }
276
277    #[test]
278    fn test_parse_fields_skips_directives_and_comments() {
279        let fields = parse_fields(
280            "@schema Point { x: f64, y: f64 }\n\n# this is a comment\nx = 1.0\n\ny = 2.0\n",
281        )
282        .unwrap();
283        assert_eq!(fields.get("x").unwrap(), "1.0");
284        assert_eq!(fields.get("y").unwrap(), "2.0");
285    }
286
287    #[test]
288    fn test_get_aam() {
289        let aam = AAM::parse("x = 1.5\ny = 2.5\n").unwrap();
290        let x: f64 = get_aam(&aam, "x").unwrap();
291        assert_eq!(x, 1.5);
292    }
293
294    #[test]
295    fn test_get_opt_aam() {
296        let aam = AAM::parse("name = hello\n").unwrap();
297        let name: Option<String> = get_opt_aam(&aam, "name").unwrap();
298        assert_eq!(name, Some("hello".to_string()));
299        let missing: Option<String> = get_opt_aam(&aam, "missing").unwrap();
300        assert_eq!(missing, None);
301    }
302
303    #[test]
304    fn test_manual_full_aam_format() {
305        struct Point {
306            x: f64,
307            y: f64,
308        }
309
310        impl FromAam for Point {
311            fn from_aam_str(value: &str) -> Result<Self, AamlError> {
312                let fields = parse_fields(value)?;
313                Ok(Self {
314                    x: fields
315                        .get("x")
316                        .map(|s| f64::from_aam_str(s))
317                        .ok_or_else(|| AamlError::NotFound {
318                            key: "x".to_string(),
319                            context: "Point".to_string(),
320                            diagnostics: None,
321                        })??,
322                    y: fields
323                        .get("y")
324                        .map(|s| f64::from_aam_str(s))
325                        .ok_or_else(|| AamlError::NotFound {
326                            key: "y".to_string(),
327                            context: "Point".to_string(),
328                            diagnostics: None,
329                        })??,
330                })
331            }
332        }
333
334        let p = Point::from_aam_str("x = 1.5\ny = 2.5\n").unwrap();
335        assert_eq!(p.x, 1.5);
336        assert_eq!(p.y, 2.5);
337
338        let p2 = Point::from_aam_str("{ x = 3.0, y = 4.0 }").unwrap();
339        assert_eq!(p2.x, 3.0);
340        assert_eq!(p2.y, 4.0);
341    }
342}