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**: JSON array of integers, e.g. `[0,1,3]`. Native JSON type,
no custom parsing needed on the receiver side.
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.
227 changes: 227 additions & 0 deletions moebius/moebius/data/json_serde.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,227 @@
/******************************************************************************
* 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) {
string r;
r << "[";
bool first= true;
path cur = p;
while (!is_nil (cur)) {
if (!first) r << ",";
r << as_string (cur->item);
first= false;
cur = cur->next;
}
r << "]";
return r;
}

path
json_string_to_path (string s) {
int n= N (s);
if (n < 2 || s[0] != '[' || s[n - 1] != ']') return path ();
string inner= s (1, n - 1);
if (N (inner) == 0) return path ();
path head;
path* tail= &head;
int i = 0;
int in = N (inner);
while (i < in) {
// skip whitespace
while (i < in && inner[i] == ' ')
i++;
// parse integer
int start= i;
if (i < in && inner[i] == '-') i++;
while (i < in && inner[i] >= '0' && inner[i] <= '9')
i++;
string num_str= inner (start, i);
int num = as_int (num_str);
*tail = path (num);
tail = &((*tail)->next);
// skip comma
while (i < in && (inner[i] == ',' || inner[i] == ' '))
i++;
}
return head;
}

/******************************************************************************
* 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\":" << 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);
}

// Extract a JSON array value (e.g. [0,1,3]) for a given key
static string
json_extract_array (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 - 1; // point to '['
break;
}
}
if (pos < 0) return "[]";
// Find matching ']'
for (int i= pos + 1; i < n; i++) {
if (json[i] == ']') {
return json (pos, i + 1);
}
}
return "[]";
}

modification
json_to_modification (string s) {
string type_str= json_extract_value (s, "type");
string path_str= json_extract_array (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
86 changes: 86 additions & 0 deletions moebius/moebius/data/json_serde.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
/******************************************************************************
* 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 array string.
*
* Uses JSON array of integers, e.g. "[0,1,3]".
* An empty path is represented as "[]".
*
* @param p The path to serialize.
* @return A JSON array string representation.
*/
string path_to_json_string (path p);

/**
* @brief Deserialize a path from a JSON array string.
*
* @param s A JSON array string, e.g. "[0,1,3]".
* @return The deserialized path.
*/
path json_string_to_path (string s);

/**
* @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
Loading