Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions devel/213_25.md
Original file line number Diff line number Diff line change
@@ -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.
162 changes: 162 additions & 0 deletions moebius/moebius/data/json_serde.cpp
Original file line number Diff line number Diff line change
@@ -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 <http://www.gnu.org/licenses/gpl-3.0.html>.
******************************************************************************/

#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
78 changes: 78 additions & 0 deletions moebius/moebius/data/json_serde.hpp
Original file line number Diff line number Diff line change
@@ -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 <http://www.gnu.org/licenses/gpl-3.0.html>.
******************************************************************************/
#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
115 changes: 115 additions & 0 deletions moebius/tests/moebius/data/json_serde_test.cpp
Original file line number Diff line number Diff line change
@@ -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] == '}');
}