1use crate::aam::AAM;
2use crate::aaml::parsing;
3use crate::error::{AamlError, ErrorDiagnostics};
4use std::collections::HashMap;
5
6pub trait FromAam: Sized {
71 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
136pub fn parse_object_fields(value: &str) -> Result<HashMap<String, String>, AamlError> {
142 Ok(parsing::parse_inline_object(value)?.into_iter().collect())
143}
144
145pub fn parse_fields(value: &str) -> Result<HashMap<String, String>, AamlError> {
155 let v = value.trim();
156
157 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
172pub 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
189pub 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
207pub 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}