1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
// SPDX-License-Identifier: MIT OR Apache-2.0
// Copyright (c) 2026 Noyalib. All rights reserved.
//! Distinct-typed key-collision parity on the STREAMING path (v0.0.15).
//!
//! v0.0.14 gave `from_str::<Value>` (the AST fast path) the KeyCollision
//! guard, but the streaming path — used for typed/struct/map targets and the
//! borrowing API — still stringified keys and silently collapsed a distinct-
//! typed collision (`1` vs `"1"`). This suite pins the fix: the streaming
//! path now detects collisions in parity with the loader.
#![allow(clippy::unwrap_used)]
use std::collections::BTreeMap;
use noyalib::{Error, Value, from_str, from_str_borrowing};
fn assert_collision<T: std::fmt::Debug>(r: Result<T, Error>, ctx: &str) {
assert!(
matches!(r, Err(Error::KeyCollision(_))),
"{ctx}: expected KeyCollision, got {r:?}"
);
}
#[test]
fn typed_map_target_detects_distinct_typed_collision() {
// The typed/map target routes through streaming; `1` (int) and `"1"`
// (string) both stringify to "1" and used to silently collapse.
assert_collision(
from_str::<BTreeMap<String, String>>("1: a\n\"1\": b\n"),
"BTreeMap 1 vs \"1\"",
);
assert_collision(
from_str::<BTreeMap<String, String>>("true: a\n\"true\": b\n"),
"BTreeMap true vs \"true\"",
);
assert_collision(
from_str::<BTreeMap<String, String>>("~: a\n\"null\": b\n"),
"BTreeMap ~ vs \"null\"",
);
}
#[test]
fn borrowing_value_target_detects_distinct_typed_collision() {
// BUG004: the zero-copy entry point built a StreamingDeserializer
// unconditionally, so `Value` collisions collapsed silently there.
assert_collision(
from_str_borrowing::<Value>("1: a\n\"1\": b\n"),
"from_str_borrowing::<Value>",
);
}
#[test]
fn streaming_collision_does_not_over_fire() {
// Distinct keys and genuine same-typed duplicates are NOT collisions.
let ok: BTreeMap<String, i64> = from_str("a: 1\nb: 2\n").unwrap();
assert_eq!(ok.len(), 2);
let ints: BTreeMap<String, i64> = from_str("1: 10\n2: 20\n").unwrap();
assert_eq!(ints.len(), 2);
// Same-typed duplicate key resolves under DuplicateKeyPolicy (last wins),
// not a collision.
let dup: BTreeMap<String, i64> = from_str("a: 1\na: 2\n").unwrap();
assert_eq!(dup.get("a"), Some(&2));
}