nodedb_query/msgpack_scan/
kv_body.rs1use std::collections::HashMap;
13
14use nodedb_types::{MsgpackError, NotScalar, Value, scalar_to_raw_bytes};
15
16use crate::msgpack_scan::{map_header, write_map_header, write_str};
17
18#[derive(Debug, Clone, Copy, PartialEq, Eq)]
20pub enum KvBodyShape {
21 Raw,
23 Map,
25}
26
27#[derive(Debug, thiserror::Error)]
29pub enum KvBodyError {
30 #[error("KV map body: {0}")]
32 Decode(#[from] MsgpackError),
33 #[error("KV row must be an object, got {kind}")]
35 RowNotObject {
36 kind: &'static str,
38 },
39 #[error("KV raw body has no `value` field after merge")]
41 RawMissingValue,
42 #[error("KV row holds a bare `value`, not typed columns; cannot set {keys}")]
44 RawExtraKeys {
45 keys: String,
47 },
48 #[error("KV raw body `value`: {0}")]
50 RawNotScalar(#[from] NotScalar),
51 #[error("KV map body encode: {0}")]
53 Encode(zerompk::Error),
54}
55
56pub fn kv_body_shape(body: &[u8]) -> KvBodyShape {
59 if map_header(body, 0).is_some() {
60 KvBodyShape::Map
61 } else {
62 KvBodyShape::Raw
63 }
64}
65
66pub fn kv_body_to_row(body: &[u8]) -> Result<(Value, KvBodyShape), MsgpackError> {
73 if kv_body_shape(body) == KvBodyShape::Map {
74 return Ok((nodedb_types::value_from_msgpack(body)?, KvBodyShape::Map));
75 }
76 let mut row = std::collections::HashMap::with_capacity(1);
77 row.insert(
78 "value".to_string(),
79 Value::String(String::from_utf8_lossy(body).into_owned()),
80 );
81 Ok((Value::Object(row), KvBodyShape::Raw))
82}
83
84pub fn row_to_kv_body(row: &Value, shape: KvBodyShape) -> Result<Vec<u8>, KvBodyError> {
93 let Value::Object(map) = row else {
94 return Err(KvBodyError::RowNotObject {
95 kind: row.type_name(),
96 });
97 };
98 match shape {
99 KvBodyShape::Map => {
100 let mut fields: Vec<(&String, &Value)> = map.iter().collect();
101 fields.sort_unstable_by(|a, b| a.0.cmp(b.0));
102 let mut buf = Vec::with_capacity(map.len() * 16);
103 write_map_header(&mut buf, fields.len());
104 for (key, value) in fields {
105 write_str(&mut buf, key);
106 let encoded = nodedb_types::value_to_msgpack(value).map_err(KvBodyError::Encode)?;
107 buf.extend_from_slice(&encoded);
108 }
109 Ok(buf)
110 }
111 KvBodyShape::Raw => {
112 let mut extra: Vec<&str> = map
113 .keys()
114 .map(String::as_str)
115 .filter(|k| *k != "value")
116 .collect();
117 if !extra.is_empty() {
118 extra.sort_unstable();
119 return Err(KvBodyError::RawExtraKeys {
120 keys: extra.join(", "),
121 });
122 }
123 let value = map.get("value").ok_or(KvBodyError::RawMissingValue)?;
124 Ok(scalar_to_raw_bytes(value)?)
125 }
126 }
127}
128
129pub fn kv_row_to_body_fields(
141 key: &str,
142 row: &[u8],
143) -> Result<(HashMap<String, Value>, KvBodyShape), KvBodyError> {
144 let row = nodedb_types::value_from_msgpack(row)?;
145 let Value::Object(mut fields) = row else {
146 return Err(KvBodyError::RowNotObject {
147 kind: row.type_name(),
148 });
149 };
150 if matches!(fields.get("key"), Some(Value::String(stored)) if stored == key) {
151 fields.remove("key");
152 }
153 let shape = if fields.len() == 1 && fields.contains_key("value") {
154 KvBodyShape::Raw
155 } else {
156 KvBodyShape::Map
157 };
158 Ok((fields, shape))
159}
160
161#[cfg(test)]
162mod tests {
163 use super::*;
164
165 fn body_from_row(key: &str, row: &[u8]) -> Vec<u8> {
166 let (fields, shape) = kv_row_to_body_fields(key, row).unwrap();
167 row_to_kv_body(&Value::Object(fields), shape).unwrap()
168 }
169
170 #[test]
171 fn a_raw_body_round_trips_through_its_row() {
172 for body in [b"v1".to_vec(), b"1".to_vec(), Vec::new()] {
173 let row = crate::msgpack_scan::kv_row_msgpack("k1", &body);
174 assert_eq!(body_from_row("k1", &row), body);
175 }
176 }
177
178 #[test]
179 fn a_map_body_round_trips_through_its_row() {
180 let mut fields = HashMap::new();
181 fields.insert("n".to_string(), Value::Integer(7));
182 fields.insert("s".to_string(), Value::String("x".into()));
183 let body = row_to_kv_body(&Value::Object(fields), KvBodyShape::Map).unwrap();
184 let row = crate::msgpack_scan::kv_row_msgpack("k2", &body);
185 assert_eq!(body_from_row("k2", &row), body);
186 }
187
188 #[test]
189 fn a_key_column_that_differs_from_the_entry_key_is_kept() {
190 let mut fields = HashMap::new();
191 fields.insert("key".to_string(), Value::String("stored".into()));
192 fields.insert("n".to_string(), Value::Integer(1));
193 let body = row_to_kv_body(&Value::Object(fields), KvBodyShape::Map).unwrap();
194 let row = crate::msgpack_scan::kv_row_msgpack("slot", &body);
195 assert_eq!(body_from_row("slot", &row), body);
196 }
197
198 #[test]
199 fn a_row_that_is_not_a_map_is_an_error() {
200 let not_a_map = nodedb_types::value_to_msgpack(&Value::Integer(118)).unwrap();
201 assert!(matches!(
202 kv_row_to_body_fields("k", ¬_a_map),
203 Err(KvBodyError::RowNotObject { kind: "int" })
204 ));
205 }
206
207 #[test]
208 fn bytes_that_are_not_msgpack_are_a_decode_error() {
209 assert!(matches!(
210 kv_row_to_body_fields("k", &[0x81]),
211 Err(KvBodyError::Decode(_))
212 ));
213 }
214
215 fn value_of(row: &Value) -> &Value {
216 row.get("value").expect("row carries `value`")
217 }
218
219 #[test]
220 fn raw_string_round_trips_in_raw_shape() {
221 let (row, shape) = kv_body_to_row(b"second-longer-value").unwrap();
222 assert_eq!(shape, KvBodyShape::Raw);
223 assert_eq!(value_of(&row), &Value::String("second-longer-value".into()));
224 assert_eq!(
225 row_to_kv_body(&row, shape).unwrap(),
226 b"second-longer-value".to_vec()
227 );
228 }
229
230 #[test]
231 fn raw_single_byte_value_is_a_string_not_a_fixint() {
232 let (row, shape) = kv_body_to_row(b"1").unwrap();
235 assert_eq!(shape, KvBodyShape::Raw);
236 assert_eq!(value_of(&row), &Value::String("1".into()));
237 assert_eq!(row_to_kv_body(&row, shape).unwrap(), b"1".to_vec());
238 }
239
240 #[test]
241 fn raw_integer_value_encodes_as_decimal_text() {
242 let mut row = HashMap::new();
243 row.insert("value".to_string(), Value::Integer(42));
244 assert_eq!(
245 row_to_kv_body(&Value::Object(row), KvBodyShape::Raw).unwrap(),
246 b"42".to_vec()
247 );
248 }
249
250 #[test]
251 fn map_body_round_trips_in_map_shape() {
252 let mut fields = HashMap::new();
253 fields.insert("n".to_string(), Value::Integer(7));
254 let body = nodedb_types::value_to_msgpack(&Value::Object(fields.clone())).unwrap();
255
256 let (row, shape) = kv_body_to_row(&body).unwrap();
257 assert_eq!(shape, KvBodyShape::Map);
258 assert_eq!(row, Value::Object(fields.clone()));
259
260 let out = row_to_kv_body(&row, shape).unwrap();
261 assert_eq!(
262 nodedb_types::value_from_msgpack(&out).unwrap(),
263 Value::Object(fields)
264 );
265 }
266
267 #[test]
268 fn map_body_encodes_fields_in_key_order() {
269 let mut fields = HashMap::new();
270 fields.insert("b".to_string(), Value::Integer(2));
271 fields.insert("a".to_string(), Value::Integer(1));
272 let out = row_to_kv_body(&Value::Object(fields), KvBodyShape::Map).unwrap();
273 assert_eq!(out[0], 0x82, "fixmap of two entries");
274 let key_at = |name: u8| out.windows(2).position(|w| w == [0xa1, name]).unwrap();
275 assert!(
276 key_at(b'a') < key_at(b'b'),
277 "keys must be written in sorted order"
278 );
279 }
280
281 #[test]
282 fn raw_shape_with_extra_keys_is_an_error_naming_them() {
283 let mut row = HashMap::new();
284 row.insert("value".to_string(), Value::String("x".into()));
285 row.insert("n".to_string(), Value::Integer(1));
286 row.insert("a".to_string(), Value::Integer(2));
287 let err = row_to_kv_body(&Value::Object(row), KvBodyShape::Raw).unwrap_err();
288 match err {
289 KvBodyError::RawExtraKeys { keys } => assert_eq!(keys, "a, n"),
290 other => panic!("expected RawExtraKeys, got {other:?}"),
291 }
292 }
293
294 #[test]
295 fn raw_shape_with_non_scalar_value_is_an_error() {
296 let mut row = HashMap::new();
297 row.insert("value".to_string(), Value::Array(vec![Value::Integer(1)]));
298 let err = row_to_kv_body(&Value::Object(row), KvBodyShape::Raw).unwrap_err();
299 assert!(matches!(err, KvBodyError::RawNotScalar(_)), "{err:?}");
300 }
301
302 #[test]
303 fn non_object_row_is_an_error_in_either_shape() {
304 for shape in [KvBodyShape::Raw, KvBodyShape::Map] {
305 let err = row_to_kv_body(&Value::Integer(1), shape).unwrap_err();
306 assert!(matches!(err, KvBodyError::RowNotObject { kind: "int" }));
307 }
308 }
309
310 #[test]
311 fn empty_body_is_a_raw_empty_string() {
312 let (row, shape) = kv_body_to_row(b"").unwrap();
313 assert_eq!(shape, KvBodyShape::Raw);
314 assert_eq!(value_of(&row), &Value::String(String::new()));
315 assert!(row_to_kv_body(&row, shape).unwrap().is_empty());
316 }
317
318 #[test]
319 fn corrupt_map_body_is_a_decode_error() {
320 assert!(kv_body_to_row(&[0x81]).is_err());
322 }
323}