use crate::aam::AAM;
use crate::aaml::parsing;
use crate::error::{AamlError, ErrorDiagnostics};
use std::collections::HashMap;
pub trait FromAam: Sized {
fn from_aam_str(value: &str) -> Result<Self, AamlError>;
}
impl FromAam for String {
fn from_aam_str(value: &str) -> Result<Self, AamlError> {
Ok(value.to_string())
}
}
macro_rules! impl_from_aam_parse {
($($t:ty),* $(,)?) => {
$(
impl FromAam for $t {
fn from_aam_str(value: &str) -> Result<Self, AamlError> {
value.trim().parse().map_err(|e| AamlError::InvalidValue {
details: format!("cannot parse '{}' as {}: {}", value, stringify!($t), e),
expected: format!("valid {}", stringify!($t)),
diagnostics: None,
})
}
}
)*
};
}
impl_from_aam_parse!(
i8, i16, i32, i64, isize, u8, u16, u32, u64, usize, f32, f64, bool
);
impl<T: FromAam> FromAam for Option<T> {
fn from_aam_str(value: &str) -> Result<Self, AamlError> {
let v = value.trim();
if v.is_empty() {
return Ok(None);
}
T::from_aam_str(value).map(Some)
}
}
impl<T: FromAam> FromAam for Vec<T> {
fn from_aam_str(value: &str) -> Result<Self, AamlError> {
let v = value.trim();
if v.starts_with('[') && v.ends_with(']') {
let items = parse_list_items(v)?;
return items.iter().map(|s| T::from_aam_str(s)).collect();
}
T::from_aam_str(v).map(|item| vec![item])
}
}
impl<T: FromAam, S: ::std::hash::BuildHasher + Default> FromAam for HashMap<String, T, S> {
fn from_aam_str(value: &str) -> Result<Self, AamlError> {
let fields = parse_object_fields(value)?;
fields
.into_iter()
.map(|(k, v)| T::from_aam_str(&v).map(|tv| (k, tv)))
.collect()
}
}
pub fn parse_object_fields(value: &str) -> Result<HashMap<String, String>, AamlError> {
Ok(parsing::parse_inline_object(value)?.into_iter().collect())
}
pub fn parse_fields(value: &str) -> Result<HashMap<String, String>, AamlError> {
let v = value.trim();
if v.starts_with('{') && v.ends_with('}') {
return parse_object_fields(v);
}
let aam = AAM::parse(v).map_err(|mut errs| errs.remove(0))?;
let map: HashMap<String, String> = aam
.iter()
.map(|(k, val)| (k.to_string(), val.to_string()))
.collect();
Ok(map)
}
pub fn parse_list_items(value: &str) -> Result<Vec<String>, AamlError> {
let v = value.trim();
let input = format!("_ = {v}");
let aam = AAM::parse(&input).map_err(|mut errs| errs.remove(0))?;
let raw = aam.get("_").unwrap_or("");
crate::types_aam::list::ListType::parse_items(raw).ok_or_else(|| AamlError::InvalidValue {
details: format!("'{v}' is not a valid list literal"),
expected: "[item, item, ...] format".to_string(),
diagnostics: None,
})
}
pub fn get_aam<T: FromAam>(aam: &AAM, key: &str) -> Result<T, AamlError> {
let value = aam.get(key).ok_or_else(|| AamlError::NotFound {
key: key.to_string(),
context: "AAM map".to_string(),
diagnostics: Some(Box::new(ErrorDiagnostics::new(
"Key not found",
format!("Key '{key}' not found in AAM document"),
"Check that the key exists and the document was parsed correctly",
))),
})?;
T::from_aam_str(value)
}
pub fn get_opt_aam<T: FromAam>(aam: &AAM, key: &str) -> Result<Option<T>, AamlError> {
aam.get(key)
.map_or_else(|| Ok(None), |value| T::from_aam_str(value).map(Some))
}
#[cfg(test)]
#[allow(clippy::float_cmp)]
mod tests {
use super::*;
#[test]
fn test_primitives() {
assert_eq!(i32::from_aam_str("42").unwrap(), 42);
assert!((f64::from_aam_str("3.14").unwrap() - std::f64::consts::PI).abs() < 0.01);
assert!(bool::from_aam_str("true").unwrap());
assert_eq!(String::from_aam_str("hello").unwrap(), "hello");
}
#[test]
fn test_option() {
assert_eq!(Option::<i32>::from_aam_str("42").unwrap(), Some(42));
assert_eq!(Option::<i32>::from_aam_str("").unwrap(), None);
}
#[test]
fn test_vec() {
assert_eq!(
Vec::<i32>::from_aam_str("[1, 2, 3]").unwrap(),
vec![1, 2, 3]
);
assert_eq!(
Vec::<String>::from_aam_str("[a, b, c]").unwrap(),
vec!["a", "b", "c"]
);
assert_eq!(Vec::<i32>::from_aam_str("42").unwrap(), vec![42]);
}
#[test]
fn test_parse_object_fields() {
let fields = parse_object_fields("{ x = 1.0, y = 2.0, name = hello }").unwrap();
assert_eq!(fields.get("x").unwrap(), "1.0");
assert_eq!(fields.get("y").unwrap(), "2.0");
assert_eq!(fields.get("name").unwrap(), "hello");
}
#[test]
fn test_parse_list_items() {
let items = parse_list_items("[rust, aam, config]").unwrap();
assert_eq!(items, vec!["rust", "aam", "config"]);
}
#[test]
fn test_parse_fields_inline() {
let fields = parse_fields("{ x = 1.0, y = 2.0 }").unwrap();
assert_eq!(fields.get("x").unwrap(), "1.0");
assert_eq!(fields.get("y").unwrap(), "2.0");
}
#[test]
fn test_parse_fields_full_aam() {
let fields = parse_fields("x = 1.0\ny = 2.0\n").unwrap();
assert_eq!(fields.get("x").unwrap(), "1.0");
assert_eq!(fields.get("y").unwrap(), "2.0");
}
#[test]
fn test_parse_fields_skips_directives_and_comments() {
let fields = parse_fields(
"@schema Point { x: f64, y: f64 }\n\n# this is a comment\nx = 1.0\n\ny = 2.0\n",
)
.unwrap();
assert_eq!(fields.get("x").unwrap(), "1.0");
assert_eq!(fields.get("y").unwrap(), "2.0");
}
#[test]
fn test_get_aam() {
let aam = AAM::parse("x = 1.5\ny = 2.5\n").unwrap();
let x: f64 = get_aam(&aam, "x").unwrap();
assert_eq!(x, 1.5);
}
#[test]
fn test_get_opt_aam() {
let aam = AAM::parse("name = hello\n").unwrap();
let name: Option<String> = get_opt_aam(&aam, "name").unwrap();
assert_eq!(name, Some("hello".to_string()));
let missing: Option<String> = get_opt_aam(&aam, "missing").unwrap();
assert_eq!(missing, None);
}
#[test]
fn test_manual_full_aam_format() {
struct Point {
x: f64,
y: f64,
}
impl FromAam for Point {
fn from_aam_str(value: &str) -> Result<Self, AamlError> {
let fields = parse_fields(value)?;
Ok(Self {
x: fields
.get("x")
.map(|s| f64::from_aam_str(s))
.ok_or_else(|| AamlError::NotFound {
key: "x".to_string(),
context: "Point".to_string(),
diagnostics: None,
})??,
y: fields
.get("y")
.map(|s| f64::from_aam_str(s))
.ok_or_else(|| AamlError::NotFound {
key: "y".to_string(),
context: "Point".to_string(),
diagnostics: None,
})??,
})
}
}
let p = Point::from_aam_str("x = 1.5\ny = 2.5\n").unwrap();
assert_eq!(p.x, 1.5);
assert_eq!(p.y, 2.5);
let p2 = Point::from_aam_str("{ x = 3.0, y = 4.0 }").unwrap();
assert_eq!(p2.x, 3.0);
assert_eq!(p2.y, 4.0);
}
}