//! 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) -> Self; /// Apply an operation to this CRDT, forwarding if necessary fn apply(&mut self, op: Op) -> 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 + 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 CrdtNode for T where T: CrdtNodeFromValue + MarkPrimitive + Hashable + Clone, { fn apply(&mut self, _op: Op) -> OpState { OpState::ErrApplyOnPrimitive } fn view(&self) -> JsonValue { self.to_owned().into() } fn new(_id: AuthorId, _path: Vec) -> 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) -> Result; } /// Fallibly cast a JSON Value into a CRDT Node pub trait IntoCrdtNode: Sized { fn into_node(self, id: AuthorId, path: Vec) -> Result; } /// [`CrdtNodeFromValue`] implies [`IntoCrdtNode`] impl IntoCrdtNode for JsonValue where T: CrdtNodeFromValue, { fn into_node(self, id: AuthorId, path: Vec) -> Result { 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) -> Result { 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, y: LwwRegisterCrdt, } let keypair = make_keypair(); let crdt = BaseCrdt::::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, y: LwwRegisterCrdt, } #[add_crdt_fields] #[derive(Clone, CrdtNode, Debug)] struct Player { pos: Position, balance: LwwRegisterCrdt, messages: ListCrdt, } let keypair = make_keypair(); let crdt = BaseCrdt::::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, b: LwwRegisterCrdt, c: LwwRegisterCrdt, } let kp1 = make_keypair(); let kp2 = make_keypair(); let mut base1 = BaseCrdt::::new(&kp1); let mut base2 = BaseCrdt::::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, } let kp1 = make_keypair(); let kp2 = make_keypair(); let mut base1 = BaseCrdt::::new(&kp1); let mut base2 = BaseCrdt::::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, soulbound: LwwRegisterCrdt, } #[add_crdt_fields] #[derive(Clone, CrdtNode, Debug)] struct Player { inventory: ListCrdt, balance: LwwRegisterCrdt, } let kp1 = make_keypair(); let kp2 = make_keypair(); let mut base1 = BaseCrdt::::new(&kp1); let mut base2 = BaseCrdt::::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>>, } let kp1 = make_keypair(); let kp2 = make_keypair(); let mut base1 = BaseCrdt::::new(&kp1); let mut base2 = BaseCrdt::::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, } let kp1 = make_keypair(); let mut base1 = BaseCrdt::::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, } #[add_crdt_fields] #[derive(Clone, CrdtNode, Debug)] struct Test { reg: LwwRegisterCrdt, strct: ListCrdt, } let key = make_keypair(); let mut crdt = BaseCrdt::::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()); } }