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
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
//! Insert a value at a dotted path inside an object, creating intermediate
//! objects as needed. Each segment is validated as the path is descended,
//! so callers should not pre-validate.
use indexmap::map::Entry;
use crate::error::{ConflictKind, Error, ErrorKind, Span};
use crate::value::{ObjectMap, Value};
use super::validate::is_valid_key;
pub(super) fn insert_value(
table: &mut ObjectMap,
path: &str,
value: Value,
line_num: usize,
span: Span,
) -> Result<(), Error> {
// Fast path: non-dotted key — the vast majority of inserts. A single
// `entry()` call collapses the old `contains_key` + `insert` into one
// hash lookup.
if !path.as_bytes().contains(&b'.') {
// § 4: trim the single key segment of leading/trailing whitespace
// before validation. Empty-after-trim → EmptyKey.
let trimmed_key = path.trim();
if trimmed_key.is_empty() {
return Err(Error::Structured(ErrorKind::EmptyKey {
line: line_num as u32,
span,
}));
}
if !is_valid_key(trimmed_key) {
return Err(Error::Structured(ErrorKind::InvalidKey {
line: line_num as u32,
key: path.to_string(),
span,
}));
}
return match table.entry(trimmed_key.into()) {
Entry::Occupied(e) => {
let existing = e.get();
if matches!(existing, Value::Object(_)) && !matches!(value, Value::Object(_))
|| !matches!(existing, Value::Object(_)) && matches!(value, Value::Object(_))
{
Err(Error::Structured(ErrorKind::KeyPathConflict {
line: line_num as u32,
path: path.to_string(),
kind: ConflictKind::Overwrite {
existing: kind_label(existing),
new_kind: kind_label(&value),
},
span,
}))
} else {
Err(Error::Structured(ErrorKind::DuplicateKey {
line: line_num as u32,
key: path.to_string(),
span,
}))
}
}
Entry::Vacant(v) => {
v.insert(value);
Ok(())
}
};
}
insert_dotted(table, path, value, line_num, span)
}
fn insert_dotted(
mut table: &mut ObjectMap,
full_path: &str,
value: Value,
line_num: usize,
span: Span,
) -> Result<(), Error> {
let mut rest = full_path;
loop {
if let Some((part, tail)) = rest.split_once('.') {
// § 4: trim each segment of leading/trailing whitespace.
let trimmed_part = part.trim();
if trimmed_part.is_empty() {
return Err(Error::Structured(ErrorKind::EmptyKey {
line: line_num as u32,
span,
}));
}
if !is_valid_key(trimmed_part) {
return Err(Error::Structured(ErrorKind::InvalidKey {
line: line_num as u32,
key: full_path.to_string(),
span,
}));
}
// Single lookup via `entry()`: if Vacant, create an empty
// Object; if Occupied, it must already be an Object to
// continue descending.
let entry = table
.entry(trimmed_part.into())
.or_insert_with(|| Value::Object(ObjectMap::default()));
table = match entry {
Value::Object(sub) => sub,
_ => {
return Err(Error::Structured(ErrorKind::KeyPathConflict {
line: line_num as u32,
path: full_path.to_string(),
kind: ConflictKind::BlockedByValue,
span,
}));
}
};
rest = tail;
} else {
// Leaf insert — trim the final segment.
let trimmed_rest = rest.trim();
if trimmed_rest.is_empty() {
return Err(Error::Structured(ErrorKind::EmptyKey {
line: line_num as u32,
span,
}));
}
if !is_valid_key(trimmed_rest) {
return Err(Error::Structured(ErrorKind::InvalidKey {
line: line_num as u32,
key: full_path.to_string(),
span,
}));
}
return match table.entry(trimmed_rest.into()) {
Entry::Occupied(_) => Err(Error::Structured(ErrorKind::DuplicateKey {
line: line_num as u32,
key: full_path.to_string(),
span,
})),
Entry::Vacant(v) => {
v.insert(value);
Ok(())
}
};
}
}
}
fn kind_label(v: &Value) -> &'static str {
match v {
Value::Null => "null",
Value::Bool(_) => "bool",
Value::Integer(_) => "integer",
Value::Float(_) => "float",
Value::String(_) => "string",
Value::Array(_) => "array",
Value::Object(_) => "object",
}
}