From 0a17c1f437f729c05185758c820da47d11520adf Mon Sep 17 00:00:00 2001 From: Michal Pokrywka Date: Fri, 29 May 2026 14:56:40 +0200 Subject: [PATCH] BTreeMap deserialization from an JSON object --- .../src/driver_module/js_value/serialize.rs | 46 +++++++++---- .../src/driver_module/js_value/tests.rs | 69 ++++++++++++++++++- docs/CHANGELOG.md | 1 + 3 files changed, 101 insertions(+), 15 deletions(-) diff --git a/crates/vertigo/src/driver_module/js_value/serialize.rs b/crates/vertigo/src/driver_module/js_value/serialize.rs index 5dddd14c..55b7c609 100644 --- a/crates/vertigo/src/driver_module/js_value/serialize.rs +++ b/crates/vertigo/src/driver_module/js_value/serialize.rs @@ -211,21 +211,41 @@ impl { fn from_json(context: JsJsonContext, json: JsJson) -> Result { - let JsJson::List(list) = json else { - let message = ["list expected, received ", json.typename()].concat(); - return Err(context.add(message)); - }; - let mut result = BTreeMap::new(); - for (index, item) in list.into_iter().enumerate() { - let item = from_json::>(item) - .map_err(|err| context.add(format!("index={index} error={err}")))?; - - let exist = result.insert(item.key, item.value); - - if exist.is_some() { - return Err(context.add("Duplicate key")); + match json { + // Native vertigo form: a list of `{k, v}` items. + JsJson::List(list) => { + for (index, item) in list.into_iter().enumerate() { + let item = from_json::>(item) + .map_err(|err| context.add(format!("index={index} error={err}")))?; + + if result.insert(item.key, item.value).is_some() { + return Err(context.add("Duplicate key")); + } + } + } + // Interop form (e.g. produced by serde): a plain object `{"key": value}`. + // Synthesize a `{k, v}` MapItem per entry and delegate. Non-String keys + // surface a graceful error because the key arrives as a JsJson::String. + JsJson::Object(obj) => { + for (key, value) in obj { + let synthetic = JsJson::Object(BTreeMap::from([ + ("k".to_string(), JsJson::String(key.clone())), + ("v".to_string(), value), + ])); + + let item = from_json::>(synthetic) + .map_err(|err| context.add(format!("key='{key}' error={err}")))?; + + if result.insert(item.key, item.value).is_some() { + return Err(context.add("Duplicate key")); + } + } + } + other => { + let message = ["list or object expected, received ", other.typename()].concat(); + return Err(context.add(message)); } } diff --git a/crates/vertigo/src/driver_module/js_value/tests.rs b/crates/vertigo/src/driver_module/js_value/tests.rs index 10d06e27..c94ee754 100644 --- a/crates/vertigo/src/driver_module/js_value/tests.rs +++ b/crates/vertigo/src/driver_module/js_value/tests.rs @@ -1,7 +1,7 @@ -use std::collections::BTreeMap; +use std::{collections::BTreeMap, error::Error}; use super::{ - MemoryBlock, MemoryBlockWrite, + MemoryBlock, MemoryBlockWrite, from_json, js_json_struct::{JsJson, JsJsonNumber}, }; @@ -67,6 +67,71 @@ fn json_json_list() { assert_eq!(data1, data2); } +#[test] +fn btreemap_from_empty_object() -> Result<(), Box> { + // An empty JSON object `{}` is a valid representation of an empty map. + let json = JsJson::Object(BTreeMap::new()); + let result = from_json::>(json)?; + assert!(result.is_empty()); + Ok(()) +} + +#[test] +fn btreemap_from_empty_list() -> Result<(), Box> { + // An empty list `[]` still deserializes to an empty map. + let json = JsJson::List(vec![]); + let result = from_json::>(json)?; + assert!(result.is_empty()); + Ok(()) +} + +#[test] +fn btreemap_from_non_empty_object_string_keys() -> Result<(), Box> { + // serde serializes BTreeMap as a plain object `{"key": value}`, + // so the object form must deserialize into a map for String keys. + let json = JsJson::Object(BTreeMap::from([ + ("foo".to_string(), JsJson::Number(JsJsonNumber(1.0))), + ("bar".to_string(), JsJson::Number(JsJsonNumber(2.0))), + ])); + let result = from_json::>(json)?; + assert_eq!(result.len(), 2); + assert_eq!(result.get("foo"), Some(&1)); + assert_eq!(result.get("bar"), Some(&2)); + Ok(()) +} + +#[test] +fn btreemap_from_object_non_string_keys_errors() { + // Object form only makes sense for String keys; numeric keys arrive as + // strings and must fail gracefully. + let json = JsJson::Object(BTreeMap::from([( + "1".to_string(), + JsJson::Number(JsJsonNumber(10.0)), + )])); + assert!(from_json::>(json).is_err()); +} + +#[test] +fn btreemap_from_list_of_items() -> Result<(), Box> { + // A populated list of `{k, v}` items round-trips into a map. + let json = JsJson::List(vec![ + JsJson::Object(BTreeMap::from([ + ("k".to_string(), JsJson::String("aaa".into())), + ("v".to_string(), JsJson::Number(JsJsonNumber(1.0))), + ])), + JsJson::Object(BTreeMap::from([ + ("k".to_string(), JsJson::String("bbb".into())), + ("v".to_string(), JsJson::Number(JsJsonNumber(2.0))), + ])), + ]); + + let result = from_json::>(json)?; + assert_eq!(result.len(), 2); + assert_eq!(result.get("aaa"), Some(&1)); + assert_eq!(result.get("bbb"), Some(&2)); + Ok(()) +} + #[test] fn json_json_vec() { let data1 = JsJson::Vec(vec![1, 2, 3, 4, 5]); diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 8410f47e..7e45ee34 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -14,6 +14,7 @@ * Partially fixed SVG tags rendering, for full fix trace [#539] * `TwClass` in dynamic attribute (`dyn:tw={my_tw_class}`) * `dom!` now correctly handles block of statements with attribute name inferred from variable name in the last statement +* `BTreeMap` deserialization now accepts objects `{...}` as well. ## 0.11.4 - 2026-04-18