From cd5e38b1f699a912ba1a4ab29cebe01674184c7b Mon Sep 17 00:00:00 2001 From: cc-fuyu Date: Sat, 28 Feb 2026 20:06:01 -0500 Subject: [PATCH] [213_25] Add JSON serialization for modification (collaborative editing transport) Add JSON serde for modification in moebius, enabling OT operation transport over WebSocket for web-based collaborative editing. New files: - moebius/moebius/data/json_serde.hpp (public API) - moebius/moebius/data/json_serde.cpp (implementation) - moebius/tests/moebius/data/json_serde_test.cpp (round-trip tests) Uses existing scheme serialization for tree payloads and dot-separated path format. Includes a minimal JSON builder/parser to keep moebius dependency-light. --- devel/213_25.md | 27 +++ moebius/moebius/data/json_serde.cpp | 162 ++++++++++++++++++ moebius/moebius/data/json_serde.hpp | 78 +++++++++ .../tests/moebius/data/json_serde_test.cpp | 115 +++++++++++++ 4 files changed, 382 insertions(+) create mode 100644 devel/213_25.md create mode 100644 moebius/moebius/data/json_serde.cpp create mode 100644 moebius/moebius/data/json_serde.hpp create mode 100644 moebius/tests/moebius/data/json_serde_test.cpp diff --git a/devel/213_25.md b/devel/213_25.md new file mode 100644 index 0000000000..32f9090181 --- /dev/null +++ b/devel/213_25.md @@ -0,0 +1,27 @@ +# [213_25] Add JSON serialization for modification (collaborative editing transport) + +Web-based collaborative editing requires transmitting OT operations between +clients and server over WebSocket. Currently, modifications can only be +serialized to a debug text format via `operator<<`. This PR adds a proper +JSON serialization/deserialization layer for `modification` in the `moebius` +library. + +## New Files +- `moebius/moebius/data/json_serde.hpp` — public API +- `moebius/moebius/data/json_serde.cpp` — implementation +- `moebius/tests/moebius/data/json_serde_test.cpp` — round-trip tests + +## Design Decisions +1. **JSON format**: `{"type":"assign","path":"0.1","tree":"(document \"a\" \"b\")"}`. + Simple, flat, easy to parse in any language (JS/TS, Python, C++). +2. **Tree transport**: Uses existing scheme serialization (`tree_to_scheme` / + `scheme_to_tree`) as the tree payload format, avoiding reinvention. +3. **Path format**: Dot-separated integers matching the existing `as_string(path)` + convention. +4. **Minimal JSON parser**: A lightweight `json_extract_value` function is used + instead of pulling in a full JSON library, keeping `moebius` dependency-light. + +## Why This Matters +This is a foundational building block for the GSoC 2026 Web-Based Collaborative +Editing Core project. With this module, the WASM-compiled moebius library can +exchange OT operations with a JavaScript frontend via simple JSON messages. diff --git a/moebius/moebius/data/json_serde.cpp b/moebius/moebius/data/json_serde.cpp new file mode 100644 index 0000000000..fb67e65b00 --- /dev/null +++ b/moebius/moebius/data/json_serde.cpp @@ -0,0 +1,162 @@ +/****************************************************************************** + * MODULE : json_serde.cpp + * DESCRIPTION: JSON serialization/deserialization for modification and patch, + * enabling network transport for collaborative editing + * COPYRIGHT : (C) 2026 cc-fuyu + ******************************************************************************* + * This software falls under the GNU general public license version 3 or later. + * It comes WITHOUT ANY WARRANTY WHATSOEVER. For details, see the file LICENSE + * in the root directory or . + ******************************************************************************/ + +#include "moebius/data/json_serde.hpp" +#include "moebius/data/scheme.hpp" +#include "path.hpp" +#include "tree.hpp" + +namespace moebius { +namespace data { + +/****************************************************************************** + * Path serialization + ******************************************************************************/ + +string +path_to_json_string (path p) { + if (is_nil (p)) return ""; + return as_string (p); +} + +path +json_string_to_path (string s) { + if (N (s) == 0) return path (); + return as_path (s); +} + +/****************************************************************************** + * Tree serialization (using scheme format as transport) + ******************************************************************************/ + +string +tree_to_json_string (tree t) { + return tree_to_scheme (t); +} + +tree +json_string_to_tree (string s) { + return scheme_to_tree (s); +} + +/****************************************************************************** + * JSON string escaping helpers + ******************************************************************************/ + +static string +json_escape (string s) { + int i, n= N (s); + string r; + for (i= 0; i < n; i++) { + char c= s[i]; + if (c == '"') r << "\\\""; + else if (c == '\\') r << "\\\\"; + else if (c == '\n') r << "\\n"; + else if (c == '\r') r << "\\r"; + else if (c == '\t') r << "\\t"; + else r << c; + } + return r; +} + +static string +json_unescape (string s) { + int i, n= N (s); + string r; + for (i= 0; i < n; i++) { + if (s[i] == '\\' && i + 1 < n) { + i++; + if (s[i] == '"') r << '"'; + else if (s[i] == '\\') r << '\\'; + else if (s[i] == 'n') r << '\n'; + else if (s[i] == 'r') r << '\r'; + else if (s[i] == 't') r << '\t'; + else { + r << '\\'; + r << s[i]; + } + } + else r << s[i]; + } + return r; +} + +/****************************************************************************** + * Modification to JSON + ******************************************************************************/ + +string +modification_to_json (modification mod) { + string type_str= get_type (mod); + string path_str= path_to_json_string (mod->p); + string tree_str= tree_to_json_string (mod->t); + + string r; + r << "{\"type\":\"" << json_escape (type_str) << "\""; + r << ",\"path\":\"" << json_escape (path_str) << "\""; + r << ",\"tree\":\"" << json_escape (tree_str) << "\""; + r << "}"; + return r; +} + +/****************************************************************************** + * JSON to Modification - minimal parser + ******************************************************************************/ + +// Extract value for a given key from a simple flat JSON object +static string +json_extract_value (string json, string key) { + string search; + search << "\"" << key << "\":\""; + int pos= -1; + int n = N (json); + int sn = N (search); + for (int i= 0; i + sn <= n; i++) { + bool match= true; + for (int j= 0; j < sn; j++) { + if (json[i + j] != search[j]) { + match= false; + break; + } + } + if (match) { + pos= i + sn; + break; + } + } + if (pos < 0) return ""; + + // Find closing quote (handling escapes) + string val; + for (int i= pos; i < n; i++) { + if (json[i] == '\\' && i + 1 < n) { + val << json[i]; + val << json[i + 1]; + i++; + } + else if (json[i] == '"') break; + else val << json[i]; + } + return json_unescape (val); +} + +modification +json_to_modification (string s) { + string type_str= json_extract_value (s, "type"); + string path_str= json_extract_value (s, "path"); + string tree_str= json_extract_value (s, "tree"); + path p = json_string_to_path (path_str); + tree t = json_string_to_tree (tree_str); + return make_modification (type_str, p, t); +} + +} // namespace data +} // namespace moebius diff --git a/moebius/moebius/data/json_serde.hpp b/moebius/moebius/data/json_serde.hpp new file mode 100644 index 0000000000..474cebe6cf --- /dev/null +++ b/moebius/moebius/data/json_serde.hpp @@ -0,0 +1,78 @@ +/****************************************************************************** + * MODULE : json_serde.hpp + * DESCRIPTION: JSON serialization/deserialization for modification and patch, + * enabling network transport for collaborative editing + * COPYRIGHT : (C) 2026 cc-fuyu + ******************************************************************************* + * This software falls under the GNU general public license version 3 or later. + * It comes WITHOUT ANY WARRANTY WHATSOEVER. For details, see the file LICENSE + * in the root directory or . + ******************************************************************************/ +#pragma once +#include "modification.hpp" +#include "patch.hpp" + +namespace moebius { +namespace data { + +/** + * @brief Serialize a modification to a JSON-formatted string. + * + * The output format is: + * {"type":"assign","path":"0.1","tree":"..."} + * + * This is designed for transmitting OT operations over WebSocket + * in a collaborative editing session. + * + * @param mod The modification to serialize. + * @return A JSON string representing the modification. + */ +string modification_to_json (modification mod); + +/** + * @brief Deserialize a modification from a JSON-formatted string. + * + * @param s A JSON string produced by modification_to_json. + * @return The deserialized modification. + */ +modification json_to_modification (string s); + +/** + * @brief Serialize a path to a JSON-compatible string. + * + * Uses dot-separated integers, e.g. "0.1.3". + * An empty path is represented as "". + * + * @param p The path to serialize. + * @return A dot-separated string representation. + */ +string path_to_json_string (path p); + +/** + * @brief Deserialize a path from a dot-separated string. + * + * @param s A dot-separated string, e.g. "0.1.3". + * @return The deserialized path. + */ +path json_string_to_path (string s); + +/** + * @brief Serialize a tree to a JSON-compatible string. + * + * Uses the existing scheme serialization as the transport format. + * + * @param t The tree to serialize. + * @return A scheme-formatted string representation. + */ +string tree_to_json_string (tree t); + +/** + * @brief Deserialize a tree from a scheme-formatted string. + * + * @param s A scheme-formatted string. + * @return The deserialized tree. + */ +tree json_string_to_tree (string s); + +} // namespace data +} // namespace moebius diff --git a/moebius/tests/moebius/data/json_serde_test.cpp b/moebius/tests/moebius/data/json_serde_test.cpp new file mode 100644 index 0000000000..eec2a9d828 --- /dev/null +++ b/moebius/tests/moebius/data/json_serde_test.cpp @@ -0,0 +1,115 @@ +#include "modification.hpp" +#include "moe_doctests.hpp" +#include "moebius/data/json_serde.hpp" +#include "tree.hpp" + +using moebius::data::json_string_to_path; +using moebius::data::json_string_to_tree; +using moebius::data::json_to_modification; +using moebius::data::modification_to_json; +using moebius::data::path_to_json_string; +using moebius::data::tree_to_json_string; + +/****************************************************************************** + * Path round-trip + ******************************************************************************/ + +TEST_CASE ("path_to_json_string empty path") { + path p= path (); + string s= path_to_json_string (p); + CHECK (s == ""); +} + +TEST_CASE ("path round-trip single element") { + path p= path (3); + string s= path_to_json_string (p); + path q= json_string_to_path (s); + CHECK (p == q); +} + +TEST_CASE ("path round-trip multi element") { + path p= path (0, path (1, path (2))); + string s= path_to_json_string (p); + path q= json_string_to_path (s); + CHECK (p == q); +} + +/****************************************************************************** + * Tree round-trip + ******************************************************************************/ + +TEST_CASE ("tree round-trip atomic") { + tree t= tree ("hello world"); + string s= tree_to_json_string (t); + tree u= json_string_to_tree (s); + CHECK (t == u); +} + +TEST_CASE ("tree round-trip compound") { + tree t= tree (DOCUMENT, "line1", "line2"); + string s= tree_to_json_string (t); + tree u= json_string_to_tree (s); + CHECK (t == u); +} + +/****************************************************************************** + * Modification round-trip + ******************************************************************************/ + +TEST_CASE ("modification round-trip assign") { + modification m= mod_assign (path (0), tree ("new content")); + string s= modification_to_json (m); + modification n= json_to_modification (s); + CHECK (m == n); +} + +TEST_CASE ("modification round-trip insert") { + modification m= mod_insert (path (0), 3, tree ("inserted")); + string s= modification_to_json (m); + modification n= json_to_modification (s); + CHECK (m == n); +} + +TEST_CASE ("modification round-trip remove") { + modification m= mod_remove (path (1), 2, 5); + string s= modification_to_json (m); + modification n= json_to_modification (s); + CHECK (m == n); +} + +TEST_CASE ("modification round-trip split") { + modification m= mod_split (path (), 1, 3); + string s= modification_to_json (m); + modification n= json_to_modification (s); + CHECK (m == n); +} + +TEST_CASE ("modification round-trip join") { + modification m= mod_join (path (0), 2); + string s= modification_to_json (m); + modification n= json_to_modification (s); + CHECK (m == n); +} + +TEST_CASE ("modification round-trip assign_node") { + modification m= mod_assign_node (path (0, path (1)), DOCUMENT); + string s= modification_to_json (m); + modification n= json_to_modification (s); + CHECK (m == n); +} + +TEST_CASE ("modification round-trip set_cursor") { + modification m= mod_set_cursor (path (0), 5, tree ("cursor_data")); + string s= modification_to_json (m); + modification n= json_to_modification (s); + CHECK (m == n); +} + +TEST_CASE ("modification json contains expected keys") { + modification m= mod_assign (path (0), tree ("test")); + string s= modification_to_json (m); + // Verify the JSON string contains the expected structure + CHECK (N (s) > 0); + CHECK (s[0] == '{'); + CHECK (s[N (s) - 1] == '}'); +}