use std::borrow::Cow;
use super::{
KeyLiteral, PropertyPath, PropertyPathError as Error, PropertyPathErrorKind as Kind, Segment,
Subscript,
};
fn parse(path: &str) -> PropertyPath {
PropertyPath::new(path).unwrap_or_else(|e| panic!("{path:?} should parse, got {e}"))
}
fn error(path: &str) -> Error {
PropertyPath::new(path).expect_err(&format!("{path:?} should not parse"))
}
fn field(name: &str) -> Segment<'_> {
Segment {
name,
subscript: None,
}
}
fn index(name: &str, index: u32) -> Segment<'_> {
Segment {
name,
subscript: Some(Subscript::Index(index)),
}
}
fn key<'a>(name: &'a str, key: KeyLiteral<'a>) -> Segment<'a> {
Segment {
name,
subscript: Some(Subscript::Key(key)),
}
}
#[test]
fn parses_fields() {
let path = parse("Position.UIRect.Size");
assert_eq!(
path.segments().collect::<Vec<_>>(),
[field("Position"), field("UIRect"), field("Size")]
);
}
#[test]
fn parses_a_single_field() {
let path = parse("Enabled");
assert_eq!(path.segments().collect::<Vec<_>>(), [field("Enabled")]);
}
#[test]
fn parses_indices() {
let path = parse("Elements[3]");
assert_eq!(path.segments().collect::<Vec<_>>(), [index("Elements", 3)]);
let path = parse("AnimationItems[0x1].SpeedScale");
assert_eq!(
path.segments().collect::<Vec<_>>(),
[index("AnimationItems", 1), field("SpeedScale")]
);
assert_eq!(path.as_str(), "AnimationItems[0x1].SpeedScale");
assert_eq!(
parse("A[0X1f]").segments().collect::<Vec<_>>(),
[index("A", 31)]
);
assert_eq!(
parse("A[017]").segments().collect::<Vec<_>>(),
[index("A", 15)]
);
assert_eq!(
parse("A[0]").segments().collect::<Vec<_>>(),
[index("A", 0)]
);
assert_eq!(
parse("A[4294967295]").segments().collect::<Vec<_>>(),
[index("A", u32::MAX)]
);
}
#[test]
fn parses_keys() {
let path = parse(r#"PerAttachmentMaterial{"weapon"}"#);
assert_eq!(
path.segments().collect::<Vec<_>>(),
[key("PerAttachmentMaterial", KeyLiteral::from("weapon"))]
);
let path = parse("Lookup{ 12 }");
assert_eq!(
path.segments().collect::<Vec<_>>(),
[key("Lookup", KeyLiteral::Number("12"))]
);
assert_eq!(path.as_str(), "Lookup{ 12 }");
assert_eq!(
parse("A{true}.B").segments().collect::<Vec<_>>(),
[key("A", KeyLiteral::Bool(true)), field("B")]
);
assert_eq!(
parse("A{false}").segments().collect::<Vec<_>>(),
[key("A", KeyLiteral::Bool(false))]
);
assert_eq!(
parse("A{-1.5e+3}").segments().collect::<Vec<_>>(),
[key("A", KeyLiteral::Number("-1.5e+3"))]
);
}
#[test]
fn unescapes_string_keys() {
let path = parse(r#"A{"a\"b\\cA\n"}"#);
assert_eq!(
path.segments().collect::<Vec<_>>(),
[key(
"A",
KeyLiteral::String(Cow::Owned("a\"b\\cA\n".into()))
)]
);
let path = parse(r#"A{"😀"}"#);
assert_eq!(
path.segments().collect::<Vec<_>>(),
[key("A", KeyLiteral::String(Cow::Owned("\u{1f600}".into())))]
);
let path = parse(r#"A{"}"}.B"#);
assert_eq!(
path.segments().collect::<Vec<_>>(),
[key("A", KeyLiteral::from("}")), field("B")]
);
}
#[test]
fn hashes_names_case_insensitively() {
let lower = parse("size");
let upper = parse("SIZE");
assert_ne!(lower, upper);
assert_eq!(
lower.segments().next().unwrap().name_hash(),
upper.segments().next().unwrap().name_hash()
);
assert_eq!(
parse("Position").segments().next().unwrap().name_hash(),
ltk_hash::BinHash(0x934f_4e0a)
);
}
#[test]
fn rejects_empty_segments() {
for path in ["", "Position.", ".Position", "A..B"] {
assert_eq!(error(path).kind(), Kind::EmptySegment, "{path:?}");
}
assert_eq!(error("Position.").offset(), 9);
assert_eq!(error(".Position").offset(), 0);
}
#[test]
fn rejects_trailing_characters() {
let e = error("Elements[3]x");
assert_eq!(e.kind(), Kind::UnexpectedCharacter('x'));
assert_eq!(e.offset(), 11);
assert_eq!(error("A]B").kind(), Kind::UnexpectedCharacter(']'));
assert_eq!(error("A}B").kind(), Kind::UnexpectedCharacter('}'));
assert_eq!(error("A(B)").kind(), Kind::UnexpectedCharacter('('));
}
#[test]
fn rejects_a_second_subscript() {
let e = error("A[1][2]");
assert_eq!(e.kind(), Kind::DoubleSubscript);
assert_eq!(e.offset(), 4);
assert_eq!(error("A[1]{2}").kind(), Kind::DoubleSubscript);
}
#[test]
fn rejects_unbalanced_brackets() {
for path in ["A[(1]", "A[1", "A{1", r#"A{"1"#] {
let kind = error(path).kind();
assert!(
matches!(kind, Kind::UnbalancedBracket | Kind::InvalidIndex),
"{path:?} gave {kind}"
);
}
assert_eq!(error("A[1").kind(), Kind::UnbalancedBracket);
assert_eq!(error("A{1").kind(), Kind::UnbalancedBracket);
}
#[test]
fn rejects_bad_indices() {
for path in [
"A[-1]", "A[ 3 ]", "A[3.0]", "A[]", "A[0x]", "A[08]", "A[+1]",
] {
assert_eq!(error(path).kind(), Kind::InvalidIndex, "{path:?}");
}
assert_eq!(error("A[4294967296]").kind(), Kind::InvalidIndex);
}
#[test]
fn rejects_bad_keys() {
for path in [
"A{null}", "A{[1]}", "A{}", "A{tru}", "A{1 2}", "A{01}", "A{-}",
] {
assert_eq!(error(path).kind(), Kind::InvalidKey, "{path:?}");
}
}
#[test]
fn rejects_paths_that_do_not_fit_the_wire_format() {
let long = "A".repeat(PropertyPath::MAX_LEN + 1);
let e = error(&long);
assert_eq!(e.kind(), Kind::TooLong(PropertyPath::MAX_LEN + 1));
assert!(PropertyPath::new("A".repeat(PropertyPath::MAX_LEN)).is_ok());
}
#[test]
fn pushes_pieces() {
let mut path = parse("Position");
path.push_field("Elements").unwrap();
path.push_index(3).unwrap();
path.push_field("Lookup").unwrap();
path.push_key(&KeyLiteral::from("weapon")).unwrap();
assert_eq!(path.as_str(), r#"Position.Elements[3].Lookup{"weapon"}"#);
let before = path.clone();
assert_eq!(
path.push_index(1).unwrap_err().kind(),
Kind::DoubleSubscript
);
assert_eq!(path, before);
assert_eq!(
path.push_field("A.B").unwrap_err().kind(),
Kind::UnexpectedCharacter('.')
);
assert_eq!(path.push_field("").unwrap_err().kind(), Kind::EmptySegment);
assert_eq!(path, before);
}
#[test]
fn pushes_escaped_keys() {
let mut path = parse("A");
path.push_key(&KeyLiteral::String(Cow::Borrowed("a\"b\n")))
.unwrap();
assert_eq!(path.as_str(), r#"A{"a\"b\n"}"#);
assert_eq!(
path.segments().collect::<Vec<_>>(),
[key("A", KeyLiteral::String(Cow::Owned("a\"b\n".into())))]
);
}
#[test]
fn round_trips_through_text() {
for text in [
"Enabled",
"Position.UIRect.Size",
"Elements[3]",
"AnimationItems[0x1].SpeedScale",
r#"PerAttachmentMaterial{"weapon"}"#,
"Lookup{ 12 }",
] {
let path: PropertyPath = text.parse().unwrap();
assert_eq!(path.to_string(), text);
assert_eq!(PropertyPath::try_from(text).unwrap(), path);
let printed = path
.segments()
.map(|segment| segment.to_string())
.collect::<Vec<_>>()
.join(".");
assert_eq!(
printed,
text.replace("[0x1]", "[1]").replace("{ 12 }", "{12}")
);
}
}