440 lines
13 KiB
Rust
440 lines
13 KiB
Rust
|
|
//! JSON CRDT public interface: core traits, re-exports, and integration tests.
|
||
|
|
// TODO: serde's json object serialization and deserialization (correctly) do not define anything
|
||
|
|
// object field order in JSON objects. However, the hash check impl in bft-json-bft-crdt does take order
|
||
|
|
// into account. This is going to cause problems later for non-Rust implementations, BFT hash checking
|
||
|
|
// currently depends on JSON serialization/deserialization object order. This shouldn't be the case
|
||
|
|
// but I've hacked in an IndexMap for the moment to get the PoC working. To see the problem, replace this with
|
||
|
|
// a std HashMap, everything will screw up (annoyingly, only *most* of the time).
|
||
|
|
|
||
|
|
use crate::debug::debug_op_on_primitive;
|
||
|
|
use crate::keypair::AuthorId;
|
||
|
|
use crate::op::{Hashable, Op, PathSegment};
|
||
|
|
|
||
|
|
pub use bft_crdt_derive::*;
|
||
|
|
|
||
|
|
mod base;
|
||
|
|
mod signed_op;
|
||
|
|
mod value;
|
||
|
|
|
||
|
|
pub use base::BaseCrdt;
|
||
|
|
pub use signed_op::{OpState, SignedOp, CAUSAL_QUEUE_MAX};
|
||
|
|
pub use value::JsonValue;
|
||
|
|
|
||
|
|
/// Anything that can be nested in a JSON CRDT
|
||
|
|
pub trait CrdtNode: CrdtNodeFromValue + Hashable + Clone {
|
||
|
|
/// Create a new CRDT of this type
|
||
|
|
fn new(id: AuthorId, path: Vec<PathSegment>) -> Self;
|
||
|
|
/// Apply an operation to this CRDT, forwarding if necessary
|
||
|
|
fn apply(&mut self, op: Op<JsonValue>) -> OpState;
|
||
|
|
/// Get a JSON representation of the value in this node
|
||
|
|
fn view(&self) -> JsonValue;
|
||
|
|
}
|
||
|
|
|
||
|
|
/// The following types can be used as a 'terminal' type in CRDTs
|
||
|
|
pub trait MarkPrimitive: Into<JsonValue> + Default {}
|
||
|
|
impl MarkPrimitive for bool {}
|
||
|
|
impl MarkPrimitive for i32 {}
|
||
|
|
impl MarkPrimitive for i64 {}
|
||
|
|
impl MarkPrimitive for f64 {}
|
||
|
|
impl MarkPrimitive for char {}
|
||
|
|
impl MarkPrimitive for String {}
|
||
|
|
impl MarkPrimitive for JsonValue {}
|
||
|
|
|
||
|
|
/// Implement CrdtNode for non-CRDTs
|
||
|
|
/// This is a stub implementation so most functions don't do anything/log an error
|
||
|
|
impl<T> CrdtNode for T
|
||
|
|
where
|
||
|
|
T: CrdtNodeFromValue + MarkPrimitive + Hashable + Clone,
|
||
|
|
{
|
||
|
|
fn apply(&mut self, _op: Op<JsonValue>) -> OpState {
|
||
|
|
OpState::ErrApplyOnPrimitive
|
||
|
|
}
|
||
|
|
|
||
|
|
fn view(&self) -> JsonValue {
|
||
|
|
self.to_owned().into()
|
||
|
|
}
|
||
|
|
|
||
|
|
fn new(_id: AuthorId, _path: Vec<PathSegment>) -> Self {
|
||
|
|
debug_op_on_primitive(_path);
|
||
|
|
Default::default()
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Fallibly create a CRDT Node from a JSON Value
|
||
|
|
pub trait CrdtNodeFromValue: Sized {
|
||
|
|
fn node_from(value: JsonValue, id: AuthorId, path: Vec<PathSegment>) -> Result<Self, String>;
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Fallibly cast a JSON Value into a CRDT Node
|
||
|
|
pub trait IntoCrdtNode<T>: Sized {
|
||
|
|
fn into_node(self, id: AuthorId, path: Vec<PathSegment>) -> Result<T, String>;
|
||
|
|
}
|
||
|
|
|
||
|
|
/// [`CrdtNodeFromValue`] implies [`IntoCrdtNode<T>`]
|
||
|
|
impl<T> IntoCrdtNode<T> for JsonValue
|
||
|
|
where
|
||
|
|
T: CrdtNodeFromValue,
|
||
|
|
{
|
||
|
|
fn into_node(self, id: AuthorId, path: Vec<PathSegment>) -> Result<T, String> {
|
||
|
|
T::node_from(self, id, path)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Trivial conversion from [`JsonValue`] to [`JsonValue`] as [`CrdtNodeFromValue`]
|
||
|
|
impl CrdtNodeFromValue for JsonValue {
|
||
|
|
fn node_from(value: JsonValue, _id: AuthorId, _path: Vec<PathSegment>) -> Result<Self, String> {
|
||
|
|
Ok(value)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
#[cfg(test)]
|
||
|
|
mod test {
|
||
|
|
use serde_json::json;
|
||
|
|
|
||
|
|
use crate::{
|
||
|
|
json_crdt::{add_crdt_fields, BaseCrdt, CrdtNode, IntoCrdtNode, JsonValue, OpState},
|
||
|
|
keypair::make_keypair,
|
||
|
|
list_crdt::ListCrdt,
|
||
|
|
lww_crdt::LwwRegisterCrdt,
|
||
|
|
op::{print_path, ROOT_ID},
|
||
|
|
};
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn test_derive_basic() {
|
||
|
|
#[add_crdt_fields]
|
||
|
|
#[derive(Clone, CrdtNode, Debug)]
|
||
|
|
struct Player {
|
||
|
|
x: LwwRegisterCrdt<f64>,
|
||
|
|
y: LwwRegisterCrdt<f64>,
|
||
|
|
}
|
||
|
|
|
||
|
|
let keypair = make_keypair();
|
||
|
|
let crdt = BaseCrdt::<Player>::new(&keypair);
|
||
|
|
assert_eq!(print_path(crdt.doc.x.path), "x");
|
||
|
|
assert_eq!(print_path(crdt.doc.y.path), "y");
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn test_derive_nested() {
|
||
|
|
#[add_crdt_fields]
|
||
|
|
#[derive(Clone, CrdtNode, Debug)]
|
||
|
|
struct Position {
|
||
|
|
x: LwwRegisterCrdt<f64>,
|
||
|
|
y: LwwRegisterCrdt<f64>,
|
||
|
|
}
|
||
|
|
|
||
|
|
#[add_crdt_fields]
|
||
|
|
#[derive(Clone, CrdtNode, Debug)]
|
||
|
|
struct Player {
|
||
|
|
pos: Position,
|
||
|
|
balance: LwwRegisterCrdt<f64>,
|
||
|
|
messages: ListCrdt<String>,
|
||
|
|
}
|
||
|
|
|
||
|
|
let keypair = make_keypair();
|
||
|
|
let crdt = BaseCrdt::<Player>::new(&keypair);
|
||
|
|
assert_eq!(print_path(crdt.doc.pos.x.path), "pos.x");
|
||
|
|
assert_eq!(print_path(crdt.doc.pos.y.path), "pos.y");
|
||
|
|
assert_eq!(print_path(crdt.doc.balance.path), "balance");
|
||
|
|
assert_eq!(print_path(crdt.doc.messages.path), "messages");
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn test_lww_ops() {
|
||
|
|
#[add_crdt_fields]
|
||
|
|
#[derive(Clone, CrdtNode, Debug)]
|
||
|
|
struct Test {
|
||
|
|
a: LwwRegisterCrdt<f64>,
|
||
|
|
b: LwwRegisterCrdt<bool>,
|
||
|
|
c: LwwRegisterCrdt<String>,
|
||
|
|
}
|
||
|
|
|
||
|
|
let kp1 = make_keypair();
|
||
|
|
let kp2 = make_keypair();
|
||
|
|
let mut base1 = BaseCrdt::<Test>::new(&kp1);
|
||
|
|
let mut base2 = BaseCrdt::<Test>::new(&kp2);
|
||
|
|
|
||
|
|
let _1_a_1 = base1.doc.a.set(3.0).sign(&kp1);
|
||
|
|
let _1_b_1 = base1.doc.b.set(true).sign(&kp1);
|
||
|
|
let _2_a_1 = base2.doc.a.set(1.5).sign(&kp2);
|
||
|
|
let _2_a_2 = base2.doc.a.set(2.13).sign(&kp2);
|
||
|
|
let _2_c_1 = base2.doc.c.set("abc".to_string()).sign(&kp2);
|
||
|
|
|
||
|
|
assert_eq!(base1.doc.a.view(), json!(3.0).into());
|
||
|
|
assert_eq!(base2.doc.a.view(), json!(2.13).into());
|
||
|
|
assert_eq!(base1.doc.b.view(), json!(true).into());
|
||
|
|
assert_eq!(base2.doc.c.view(), json!("abc").into());
|
||
|
|
|
||
|
|
assert_eq!(
|
||
|
|
base1.doc.view().into_json(),
|
||
|
|
json!({
|
||
|
|
"a": 3.0,
|
||
|
|
"b": true,
|
||
|
|
"c": null,
|
||
|
|
})
|
||
|
|
);
|
||
|
|
assert_eq!(
|
||
|
|
base2.doc.view().into_json(),
|
||
|
|
json!({
|
||
|
|
"a": 2.13,
|
||
|
|
"b": null,
|
||
|
|
"c": "abc",
|
||
|
|
})
|
||
|
|
);
|
||
|
|
|
||
|
|
assert_eq!(base2.apply(_1_a_1), OpState::Ok);
|
||
|
|
assert_eq!(base2.apply(_1_b_1), OpState::Ok);
|
||
|
|
assert_eq!(base1.apply(_2_a_1), OpState::Ok);
|
||
|
|
assert_eq!(base1.apply(_2_a_2), OpState::Ok);
|
||
|
|
assert_eq!(base1.apply(_2_c_1), OpState::Ok);
|
||
|
|
|
||
|
|
assert_eq!(base1.doc.view().into_json(), base2.doc.view().into_json());
|
||
|
|
assert_eq!(
|
||
|
|
base1.doc.view().into_json(),
|
||
|
|
json!({
|
||
|
|
"a": 2.13,
|
||
|
|
"b": true,
|
||
|
|
"c": "abc"
|
||
|
|
})
|
||
|
|
)
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn test_vec_and_map_ops() {
|
||
|
|
#[add_crdt_fields]
|
||
|
|
#[derive(Clone, CrdtNode, Debug)]
|
||
|
|
struct Test {
|
||
|
|
a: ListCrdt<String>,
|
||
|
|
}
|
||
|
|
|
||
|
|
let kp1 = make_keypair();
|
||
|
|
let kp2 = make_keypair();
|
||
|
|
let mut base1 = BaseCrdt::<Test>::new(&kp1);
|
||
|
|
let mut base2 = BaseCrdt::<Test>::new(&kp2);
|
||
|
|
|
||
|
|
let _1a = base1.doc.a.insert(ROOT_ID, "a".to_string()).sign(&kp1);
|
||
|
|
let _1b = base1.doc.a.insert(_1a.id(), "b".to_string()).sign(&kp1);
|
||
|
|
let _2c = base2.doc.a.insert(ROOT_ID, "c".to_string()).sign(&kp2);
|
||
|
|
let _2d = base2.doc.a.insert(_1b.id(), "d".to_string()).sign(&kp2);
|
||
|
|
|
||
|
|
assert_eq!(
|
||
|
|
base1.doc.view().into_json(),
|
||
|
|
json!({
|
||
|
|
"a": ["a", "b"],
|
||
|
|
})
|
||
|
|
);
|
||
|
|
|
||
|
|
// as _1b hasn't been delivered to base2 yet
|
||
|
|
assert_eq!(
|
||
|
|
base2.doc.view().into_json(),
|
||
|
|
json!({
|
||
|
|
"a": ["c"],
|
||
|
|
})
|
||
|
|
);
|
||
|
|
|
||
|
|
assert_eq!(base2.apply(_1b), OpState::MissingCausalDependencies);
|
||
|
|
assert_eq!(base2.apply(_1a), OpState::Ok);
|
||
|
|
assert_eq!(base1.apply(_2d), OpState::Ok);
|
||
|
|
assert_eq!(base1.apply(_2c), OpState::Ok);
|
||
|
|
assert_eq!(base1.doc.view().into_json(), base2.doc.view().into_json());
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn test_causal_field_dependency() {
|
||
|
|
#[add_crdt_fields]
|
||
|
|
#[derive(Clone, CrdtNode, Debug)]
|
||
|
|
struct Item {
|
||
|
|
name: LwwRegisterCrdt<String>,
|
||
|
|
soulbound: LwwRegisterCrdt<bool>,
|
||
|
|
}
|
||
|
|
|
||
|
|
#[add_crdt_fields]
|
||
|
|
#[derive(Clone, CrdtNode, Debug)]
|
||
|
|
struct Player {
|
||
|
|
inventory: ListCrdt<Item>,
|
||
|
|
balance: LwwRegisterCrdt<f64>,
|
||
|
|
}
|
||
|
|
|
||
|
|
let kp1 = make_keypair();
|
||
|
|
let kp2 = make_keypair();
|
||
|
|
let mut base1 = BaseCrdt::<Player>::new(&kp1);
|
||
|
|
let mut base2 = BaseCrdt::<Player>::new(&kp2);
|
||
|
|
|
||
|
|
// require balance update to happen before inventory update
|
||
|
|
let _add_money = base1.doc.balance.set(5000.0).sign(&kp1);
|
||
|
|
let _spend_money = base1
|
||
|
|
.doc
|
||
|
|
.balance
|
||
|
|
.set(3000.0)
|
||
|
|
.sign_with_dependencies(&kp1, vec![&_add_money]);
|
||
|
|
|
||
|
|
let sword: JsonValue = json!({
|
||
|
|
"name": "Sword",
|
||
|
|
"soulbound": true,
|
||
|
|
})
|
||
|
|
.into();
|
||
|
|
let _new_inventory_item = base1
|
||
|
|
.doc
|
||
|
|
.inventory
|
||
|
|
.insert_idx(0, sword)
|
||
|
|
.sign_with_dependencies(&kp1, vec![&_spend_money]);
|
||
|
|
|
||
|
|
assert_eq!(
|
||
|
|
base1.doc.view().into_json(),
|
||
|
|
json!({
|
||
|
|
"balance": 3000.0,
|
||
|
|
"inventory": [
|
||
|
|
{
|
||
|
|
"name": "Sword",
|
||
|
|
"soulbound": true
|
||
|
|
}
|
||
|
|
]
|
||
|
|
})
|
||
|
|
);
|
||
|
|
|
||
|
|
// do it completely out of order
|
||
|
|
assert_eq!(
|
||
|
|
base2.apply(_new_inventory_item),
|
||
|
|
OpState::MissingCausalDependencies
|
||
|
|
);
|
||
|
|
assert_eq!(
|
||
|
|
base2.apply(_spend_money),
|
||
|
|
OpState::MissingCausalDependencies
|
||
|
|
);
|
||
|
|
assert_eq!(base2.apply(_add_money), OpState::Ok);
|
||
|
|
assert_eq!(base1.doc.view().into_json(), base2.doc.view().into_json());
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn test_2d_grid() {
|
||
|
|
#[add_crdt_fields]
|
||
|
|
#[derive(Clone, CrdtNode, Debug)]
|
||
|
|
struct Game {
|
||
|
|
grid: ListCrdt<ListCrdt<LwwRegisterCrdt<bool>>>,
|
||
|
|
}
|
||
|
|
|
||
|
|
let kp1 = make_keypair();
|
||
|
|
let kp2 = make_keypair();
|
||
|
|
let mut base1 = BaseCrdt::<Game>::new(&kp1);
|
||
|
|
let mut base2 = BaseCrdt::<Game>::new(&kp2);
|
||
|
|
|
||
|
|
// init a 2d grid
|
||
|
|
let row0: JsonValue = json!([true, false]).into();
|
||
|
|
let row1: JsonValue = json!([false, true]).into();
|
||
|
|
let construct1 = base1.doc.grid.insert_idx(0, row0).sign(&kp1);
|
||
|
|
let construct2 = base1.doc.grid.insert_idx(1, row1).sign(&kp1);
|
||
|
|
|
||
|
|
assert_eq!(base2.apply(construct1), OpState::Ok);
|
||
|
|
assert_eq!(base2.apply(construct2.clone()), OpState::Ok);
|
||
|
|
|
||
|
|
assert_eq!(base1.doc.view().into_json(), base2.doc.view().into_json());
|
||
|
|
assert_eq!(
|
||
|
|
base1.doc.view().into_json(),
|
||
|
|
json!({
|
||
|
|
"grid": [[true, false], [false, true]]
|
||
|
|
})
|
||
|
|
);
|
||
|
|
|
||
|
|
let set1 = base1.doc.grid[0][0].set(false).sign(&kp1);
|
||
|
|
let set2 = base2.doc.grid[1][1].set(false).sign(&kp2);
|
||
|
|
assert_eq!(base1.apply(set2), OpState::Ok);
|
||
|
|
assert_eq!(base2.apply(set1), OpState::Ok);
|
||
|
|
|
||
|
|
assert_eq!(base1.doc.view().into_json(), base2.doc.view().into_json());
|
||
|
|
assert_eq!(
|
||
|
|
base1.doc.view().into_json(),
|
||
|
|
json!({
|
||
|
|
"grid": [[false, false], [false, false]]
|
||
|
|
})
|
||
|
|
);
|
||
|
|
|
||
|
|
let topright = base1.doc.grid[0].id_at(1).unwrap();
|
||
|
|
base1.doc.grid[0].delete(topright);
|
||
|
|
assert_eq!(
|
||
|
|
base1.doc.view().into_json(),
|
||
|
|
json!({
|
||
|
|
"grid": [[false], [false, false]]
|
||
|
|
})
|
||
|
|
);
|
||
|
|
|
||
|
|
base1.doc.grid.delete(construct2.id());
|
||
|
|
assert_eq!(
|
||
|
|
base1.doc.view().into_json(),
|
||
|
|
json!({
|
||
|
|
"grid": [[false]]
|
||
|
|
})
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn test_arb_json() {
|
||
|
|
#[add_crdt_fields]
|
||
|
|
#[derive(Clone, CrdtNode, Debug)]
|
||
|
|
struct Test {
|
||
|
|
reg: LwwRegisterCrdt<JsonValue>,
|
||
|
|
}
|
||
|
|
|
||
|
|
let kp1 = make_keypair();
|
||
|
|
let mut base1 = BaseCrdt::<Test>::new(&kp1);
|
||
|
|
|
||
|
|
let base_val: JsonValue = json!({
|
||
|
|
"a": true,
|
||
|
|
"b": "asdf",
|
||
|
|
"c": {
|
||
|
|
"d": [],
|
||
|
|
"e": [ false ]
|
||
|
|
}
|
||
|
|
})
|
||
|
|
.into();
|
||
|
|
base1.doc.reg.set(base_val).sign(&kp1);
|
||
|
|
assert_eq!(
|
||
|
|
base1.doc.view().into_json(),
|
||
|
|
json!({
|
||
|
|
"reg": {
|
||
|
|
"a": true,
|
||
|
|
"b": "asdf",
|
||
|
|
"c": {
|
||
|
|
"d": [],
|
||
|
|
"e": [ false ]
|
||
|
|
}
|
||
|
|
}
|
||
|
|
})
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn test_wrong_json_types() {
|
||
|
|
#[add_crdt_fields]
|
||
|
|
#[derive(Clone, CrdtNode, Debug)]
|
||
|
|
struct Nested {
|
||
|
|
list: ListCrdt<f64>,
|
||
|
|
}
|
||
|
|
|
||
|
|
#[add_crdt_fields]
|
||
|
|
#[derive(Clone, CrdtNode, Debug)]
|
||
|
|
struct Test {
|
||
|
|
reg: LwwRegisterCrdt<bool>,
|
||
|
|
strct: ListCrdt<Nested>,
|
||
|
|
}
|
||
|
|
|
||
|
|
let key = make_keypair();
|
||
|
|
let mut crdt = BaseCrdt::<Test>::new(&key);
|
||
|
|
|
||
|
|
// wrong type should not go through
|
||
|
|
crdt.doc.reg.set(32);
|
||
|
|
assert_eq!(crdt.doc.reg.view(), json!(null).into());
|
||
|
|
crdt.doc.reg.set(true);
|
||
|
|
assert_eq!(crdt.doc.reg.view(), json!(true).into());
|
||
|
|
|
||
|
|
// set nested
|
||
|
|
let mut list_view: JsonValue = crdt.doc.strct.view().into();
|
||
|
|
assert_eq!(list_view, json!([]).into());
|
||
|
|
|
||
|
|
// only keeps actual numbers
|
||
|
|
let list: JsonValue = json!({"list": [0, 123, -0.45, "char", []]}).into();
|
||
|
|
crdt.doc.strct.insert_idx(0, list);
|
||
|
|
list_view = crdt.doc.strct.view().into();
|
||
|
|
assert_eq!(list_view, json!([{ "list": [0, 123, -0.45]}]).into());
|
||
|
|
}
|
||
|
|
}
|