use raves_metadata_types::xmp::{XmpPrimitive, XmpValue, parse_types::XmpPrimitiveKind as Prim};
use crate::xmp::error::{XmpParsingError, XmpValueResult};
pub fn parse_primitive(text: String, prim: &Prim) -> XmpValueResult {
Ok(match prim {
Prim::Boolean => XmpValue::Simple(XmpPrimitive::Boolean(match &*text {
"True" => true,
"False" => false,
other => {
log::warn!("Encountered unknown boolean value: `{other}`.");
return Err(XmpParsingError::PrimitiveUnknownBool(text));
}
})),
Prim::Date => XmpValue::Simple(XmpPrimitive::Date(text)),
Prim::Integer => {
let num = text.parse::<i64>()
.map(XmpPrimitive::Integer)
.or_else(|e | {
if [core::num::IntErrorKind::NegOverflow, core::num::IntErrorKind::PosOverflow].contains(e.kind()) {
log::warn!("Given number too large for `i64`. Will be exposed as a `Prim::Text`. value: `{text}`");
Ok(XmpPrimitive::Text(text.clone()))
} else { Err(e) }
})
.inspect_err(|e| {
log::error!(
"Unable to convert integer value `{text}` into integer. \
err: {e}"
)
})
.map_err(|e| XmpParsingError::PrimitiveIntegerParseFail(text, e))
.map(XmpValue::Simple);
num?
}
Prim::Real => XmpValue::Simple(XmpPrimitive::Real(
text.parse()
.inspect_err(|e| {
log::error!(
"Unable to convert integer value `{text}` into float. \
err: {e}"
)
})
.map_err(|e| XmpParsingError::PrimitiveRealParseFail(text, e))?,
)),
Prim::Text => XmpValue::Simple(XmpPrimitive::Text(text)),
})
}
#[cfg(test)]
mod tests {
use raves_metadata_types::xmp::{
XmpPrimitive, XmpValue, parse_types::XmpPrimitiveKind as Prim,
};
use crate::xmp::{error::XmpParsingError, value::prims::parse_primitive};
#[test]
fn only_standard_compliant_booleans_should_parse() {
_ = env_logger::builder()
.filter_level(log::LevelFilter::max())
.format_file(true)
.format_line_number(true)
.try_init();
assert_eq!(
parse_primitive("True".into(), &Prim::Boolean).expect("should parse"),
XmpValue::Simple(XmpPrimitive::Boolean(true)),
"`True` str is represented as `true` (`bool`) in Rust"
);
assert_eq!(
parse_primitive("False".into(), &Prim::Boolean).expect("should parse"),
XmpValue::Simple(XmpPrimitive::Boolean(false)),
"`False` str is represented as `false` (`bool`) in Rust"
);
assert!(parse_primitive("true".into(), &Prim::Boolean).is_err());
assert!(parse_primitive("false".into(), &Prim::Boolean).is_err());
assert!(parse_primitive("1".into(), &Prim::Boolean).is_err());
assert!(parse_primitive("0".into(), &Prim::Boolean).is_err());
assert!(parse_primitive("random text".into(), &Prim::Boolean).is_err());
}
#[test]
fn normal_numbers_parse() {
_ = env_logger::builder()
.filter_level(log::LevelFilter::max())
.format_file(true)
.format_line_number(true)
.try_init();
assert_eq!(
parse_primitive("2025".into(), &Prim::Integer).expect("small values should parse"),
XmpValue::Simple(XmpPrimitive::Integer(2025_i64)),
);
assert_eq!(
parse_primitive("-2147483648".into(), &Prim::Integer).expect("`i32::MIN` should parse"),
XmpValue::Simple(XmpPrimitive::Integer(i32::MIN.into())),
);
assert_eq!(
parse_primitive("0".into(), &Prim::Integer).expect("positive zero should parse"),
XmpValue::Simple(XmpPrimitive::Integer(0_i64)),
);
assert_eq!(
parse_primitive("-0".into(), &Prim::Integer).expect("negative zero should parse"),
XmpValue::Simple(XmpPrimitive::Integer(0_i64)),
);
assert_eq!(
parse_primitive("-9223372036854775808".into(), &Prim::Integer)
.expect("`i64::MIN` should parse"),
XmpValue::Simple(XmpPrimitive::Integer(i64::MIN)),
);
assert_eq!(
parse_primitive("9223372036854775807".into(), &Prim::Integer)
.expect("`i64::MAX` should parse"),
XmpValue::Simple(XmpPrimitive::Integer(i64::MAX)),
);
assert_eq!(
parse_primitive("+1".into(), &Prim::Integer)
.expect("plus sign is permitted before an int"),
XmpValue::Simple(XmpPrimitive::Integer(1_i64)),
);
assert_eq!(
parse_primitive("-1".into(), &Prim::Integer)
.expect("minus sign is permitted before an int"),
XmpValue::Simple(XmpPrimitive::Integer(-1_i64)),
);
}
#[test]
fn huge_numbers_parse_as_prim_text() {
_ = env_logger::builder()
.filter_level(log::LevelFilter::max())
.format_file(true)
.format_line_number(true)
.try_init();
const GIANT_NUMBER_STR: &str = "9857203947509273094750927345702738904578927308945789023475";
assert_eq!(
parse_primitive(GIANT_NUMBER_STR.into(), &Prim::Integer).expect("should parse as str"),
XmpValue::Simple(XmpPrimitive::Text(GIANT_NUMBER_STR.into())),
);
let failing_number_string: String = "1.2.3".into();
let parse_prim_res = parse_primitive(failing_number_string.clone(), &Prim::Integer);
let Err(XmpParsingError::PrimitiveIntegerParseFail(f, _)) = parse_prim_res else {
panic!("legitimately malformed ints should error like normal");
};
assert_eq!(f, failing_number_string);
}
#[test]
fn dates_are_just_text() {
_ = env_logger::builder()
.filter_level(log::LevelFilter::max())
.format_file(true)
.format_line_number(true)
.try_init();
const CORRECTLY_FORMATTED_DATE: &str = "2025-06-23T14:33:00-06:00";
assert_eq!(
parse_primitive(CORRECTLY_FORMATTED_DATE.into(), &Prim::Date)
.expect("a correct date should parse just fine..."),
XmpValue::Simple(XmpPrimitive::Date(CORRECTLY_FORMATTED_DATE.into())),
);
const OBVIOUSLY_NOT_A_DATE_STR: &str = "not a date lol";
assert_eq!(
parse_primitive(OBVIOUSLY_NOT_A_DATE_STR.into(), &Prim::Date)
.expect("random txt should parse"),
XmpValue::Simple(XmpPrimitive::Date(OBVIOUSLY_NOT_A_DATE_STR.into())),
);
}
#[test]
fn reals_should_parse() {
_ = env_logger::builder()
.filter_level(log::LevelFilter::max())
.format_file(true)
.format_line_number(true)
.try_init();
assert_eq!(
parse_primitive("1".into(), &Prim::Real).expect("ints should parse as a real"),
XmpValue::Simple(XmpPrimitive::Real(1_f64)),
);
assert_eq!(
parse_primitive("0.".into(), &Prim::Real).expect("decimal places should be optional"),
XmpValue::Simple(XmpPrimitive::Real(0_f64)),
);
assert_eq!(
parse_primitive(
"100000000000000000.000000000000000000001".into(),
&Prim::Real
)
.expect("decimal places should be optional"),
XmpValue::Simple(XmpPrimitive::Real(100000000000000000_f64)),
);
assert_eq!(
parse_primitive("+1.0".into(), &Prim::Real)
.expect("plus sign is permitted before a real"),
XmpValue::Simple(XmpPrimitive::Real(1.0_f64)),
);
assert_eq!(
parse_primitive("-1.0".into(), &Prim::Real)
.expect("minus sign is permitted before a real"),
XmpValue::Simple(XmpPrimitive::Real(-1.0_f64)),
);
assert_eq!(
parse_primitive("1.0".into(), &Prim::Real).expect("signs are not required"),
XmpValue::Simple(XmpPrimitive::Real(1.0_f64)),
);
}
#[test]
fn text_should_parse() {
_ = env_logger::builder()
.filter_level(log::LevelFilter::max())
.format_file(true)
.format_line_number(true)
.try_init();
const SOME_TEXT: &str = "see you later, alligator! بعد فترة، تمساح";
assert_eq!(
parse_primitive(SOME_TEXT.into(), &Prim::Text).expect("random txt should parse"),
XmpValue::Simple(XmpPrimitive::Text(SOME_TEXT.into())),
);
}
}