Adding a working bft-json-crdt implementation for the PoC
This commit is contained in:
100
crates/bft-json-crdt/bft-crdt-derive/Cargo.lock
generated
Normal file
100
crates/bft-json-crdt/bft-crdt-derive/Cargo.lock
generated
Normal file
@@ -0,0 +1,100 @@
|
||||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 3
|
||||
|
||||
[[package]]
|
||||
name = "bft-crdt-derive"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"proc-macro-crate",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.16.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "86f0b0d4bf799edbc74508c1e8bf170ff5f41238e5f8225603ca7caaae2b7860"
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro-crate"
|
||||
version = "1.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "eda0fc3b0fb7c975631757e14d9049da17374063edb6ebbcbc54d880d4fe94e9"
|
||||
dependencies = [
|
||||
"once_cell",
|
||||
"thiserror",
|
||||
"toml",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.47"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5ea3d908b0e36316caf9e9e2c4625cdde190a7e6f440d794667ed17a1855e725"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bbe448f377a7d6961e30f5955f9b8d106c3f5e449d493ee1b125c1d43c2b5179"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.147"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d193d69bae983fc11a79df82342761dfbf28a99fc8d203dca4c3c1b590948965"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "1.0.103"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a864042229133ada95abf3b54fdc62ef5ccabe9515b64717bcb9a1919e59445d"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "1.0.37"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "10deb33631e3c9018b9baf9dcbbc4f737320d2b576bac10f6aefa048fa407e3e"
|
||||
dependencies = [
|
||||
"thiserror-impl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror-impl"
|
||||
version = "1.0.37"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "982d17546b47146b28f7c22e3d08465f6b8903d0ea13c1660d9d84a6e7adcdbb"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml"
|
||||
version = "0.5.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8d82e1a7758622a465f8cee077614c73484dac5b836c02ff6a40d5d1010324d7"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6ceab39d59e4c9499d4e5a8ee0e2735b891bb7308ac83dfb4e80cad195c9f6f3"
|
||||
14
crates/bft-json-crdt/bft-crdt-derive/Cargo.toml
Normal file
14
crates/bft-json-crdt/bft-crdt-derive/Cargo.toml
Normal file
@@ -0,0 +1,14 @@
|
||||
[package]
|
||||
name = "bft-crdt-derive"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
|
||||
[lib]
|
||||
proc-macro = true
|
||||
|
||||
[dependencies]
|
||||
proc-macro2 = "1.0.47"
|
||||
proc-macro-crate = "1.2.1"
|
||||
quote = "1.0.21"
|
||||
syn = { version = "1.0.103", features = ["full"] }
|
||||
189
crates/bft-json-crdt/bft-crdt-derive/src/lib.rs
Normal file
189
crates/bft-json-crdt/bft-crdt-derive/src/lib.rs
Normal file
@@ -0,0 +1,189 @@
|
||||
use proc_macro::TokenStream as OgTokenStream;
|
||||
use proc_macro2::{Ident, Span, TokenStream};
|
||||
use proc_macro_crate::{crate_name, FoundCrate};
|
||||
use quote::{quote, quote_spanned, ToTokens};
|
||||
use syn::{
|
||||
parse::{self, Parser},
|
||||
parse_macro_input,
|
||||
spanned::Spanned,
|
||||
Data, DeriveInput, Field, Fields, ItemStruct, LitStr, Type
|
||||
};
|
||||
|
||||
/// Helper to get tokenstream representing the parent crate
|
||||
fn get_crate_name() -> TokenStream {
|
||||
let cr8 = crate_name("bft-json-crdt")
|
||||
.unwrap_or(FoundCrate::Itself);
|
||||
match cr8 {
|
||||
FoundCrate::Itself => quote! { ::bft_json_crdt },
|
||||
FoundCrate::Name(name) => {
|
||||
let ident = Ident::new(&name, Span::call_site());
|
||||
quote! { ::#ident }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Proc macro to insert a keypair and path field on a given struct
|
||||
#[proc_macro_attribute]
|
||||
pub fn add_crdt_fields(args: OgTokenStream, input: OgTokenStream) -> OgTokenStream {
|
||||
let mut input = parse_macro_input!(input as ItemStruct);
|
||||
let crate_name = get_crate_name();
|
||||
let _ = parse_macro_input!(args as parse::Nothing);
|
||||
|
||||
if let syn::Fields::Named(ref mut fields) = input.fields {
|
||||
fields.named.push(
|
||||
Field::parse_named
|
||||
.parse2(quote! { path: Vec<#crate_name::op::PathSegment> })
|
||||
.unwrap(),
|
||||
);
|
||||
fields.named.push(
|
||||
Field::parse_named
|
||||
.parse2(quote! { id: #crate_name::keypair::AuthorId })
|
||||
.unwrap(),
|
||||
);
|
||||
}
|
||||
|
||||
return quote! {
|
||||
#input
|
||||
}
|
||||
.into();
|
||||
}
|
||||
|
||||
/// Proc macro to automatically derive the CRDTNode trait
|
||||
#[proc_macro_derive(CrdtNode)]
|
||||
pub fn derive_json_crdt(input: OgTokenStream) -> OgTokenStream {
|
||||
// parse the input tokens into a syntax tree
|
||||
let input = parse_macro_input!(input as DeriveInput);
|
||||
let crate_name = get_crate_name();
|
||||
|
||||
// used in the quasi-quotation below as `#name`
|
||||
let ident = input.ident;
|
||||
let ident_str = LitStr::new(&*ident.to_string(), ident.span());
|
||||
|
||||
let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
|
||||
match input.data {
|
||||
Data::Struct(data) => match &data.fields {
|
||||
Fields::Named(fields) => {
|
||||
let mut field_impls = vec![];
|
||||
let mut ident_literals = vec![];
|
||||
let mut ident_strings = vec![];
|
||||
let mut tys = vec![];
|
||||
// parse all named fields
|
||||
for field in &fields.named {
|
||||
let ident = field.ident.as_ref().expect("Failed to get struct field identifier");
|
||||
if ident != "path" && ident != "id" {
|
||||
let ty = match &field.ty {
|
||||
Type::Path(t) => t.to_token_stream(),
|
||||
_ => return quote_spanned! { field.span() => compile_error!("Field should be a primitive or struct which implements CRDTNode") }.into(),
|
||||
};
|
||||
let str_literal = LitStr::new(&*ident.to_string(), ident.span());
|
||||
ident_strings.push(str_literal.clone());
|
||||
ident_literals.push(ident.clone());
|
||||
tys.push(ty.clone());
|
||||
field_impls.push(quote! {
|
||||
#ident: <#ty as CrdtNode>::new(
|
||||
id,
|
||||
#crate_name::op::join_path(path.clone(), #crate_name::op::PathSegment::Field(#str_literal.to_string()))
|
||||
)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let expanded = quote! {
|
||||
impl #impl_generics #crate_name::json_crdt::CrdtNodeFromValue for #ident #ty_generics #where_clause {
|
||||
fn node_from(value: #crate_name::json_crdt::Value, id: #crate_name::keypair::AuthorId, path: Vec<#crate_name::op::PathSegment>) -> Result<Self, String> {
|
||||
if let #crate_name::json_crdt::Value::Object(mut obj) = value {
|
||||
Ok(#ident {
|
||||
path: path.clone(),
|
||||
id,
|
||||
#(#ident_literals: obj.remove(#ident_strings)
|
||||
.unwrap()
|
||||
.into_node(
|
||||
id,
|
||||
#crate_name::op::join_path(path.clone(), #crate_name::op::PathSegment::Field(#ident_strings.to_string()))
|
||||
)
|
||||
.unwrap()
|
||||
),*
|
||||
})
|
||||
} else {
|
||||
Err(format!("failed to convert {:?} -> {}<T>", value, #ident_str.to_string()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl #impl_generics std::fmt::Debug for #ident #ty_generics #where_clause {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let mut fields = Vec::new();
|
||||
#(fields.push(format!("{}", #ident_strings.to_string()));)*
|
||||
write!(f, "{{ {:?} }}", fields.join(", "))
|
||||
}
|
||||
}
|
||||
|
||||
impl #impl_generics #crate_name::json_crdt::CrdtNode for #ident #ty_generics #where_clause {
|
||||
fn apply(&mut self, op: #crate_name::op::Op<#crate_name::json_crdt::Value>) -> #crate_name::json_crdt::OpState {
|
||||
let path = op.path.clone();
|
||||
let author = op.id.clone();
|
||||
if !#crate_name::op::ensure_subpath(&self.path, &op.path) {
|
||||
#crate_name::debug::debug_path_mismatch(self.path.to_owned(), op.path);
|
||||
return #crate_name::json_crdt::OpState::ErrPathMismatch;
|
||||
}
|
||||
|
||||
if self.path.len() == op.path.len() {
|
||||
return #crate_name::json_crdt::OpState::ErrApplyOnStruct;
|
||||
} else {
|
||||
let idx = self.path.len();
|
||||
if let #crate_name::op::PathSegment::Field(path_seg) = &op.path[idx] {
|
||||
match &path_seg[..] {
|
||||
#(#ident_strings => {
|
||||
return self.#ident_literals.apply(op.into());
|
||||
}),*
|
||||
_ => {},
|
||||
};
|
||||
};
|
||||
return #crate_name::json_crdt::OpState::ErrPathMismatch
|
||||
}
|
||||
}
|
||||
|
||||
fn view(&self) -> #crate_name::json_crdt::Value {
|
||||
let mut view_map = std::collections::HashMap::new();
|
||||
#(view_map.insert(#ident_strings.to_string(), self.#ident_literals.view().into());)*
|
||||
#crate_name::json_crdt::Value::Object(view_map)
|
||||
}
|
||||
|
||||
fn new(id: #crate_name::keypair::AuthorId, path: Vec<#crate_name::op::PathSegment>) -> Self {
|
||||
Self {
|
||||
path: path.clone(),
|
||||
id,
|
||||
#(#field_impls),*
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl #crate_name::debug::DebugView for #ident {
|
||||
#[cfg(feature = "logging-base")]
|
||||
fn debug_view(&self, indent: usize) -> String {
|
||||
let inner_spacing = " ".repeat(indent + 2);
|
||||
let path_str = #crate_name::op::print_path(self.path.clone());
|
||||
let mut inner = vec![];
|
||||
#(inner.push(format!("{}\"{}\": {}", inner_spacing, #ident_strings, self.#ident_literals.debug_view(indent + 4)));)*
|
||||
let inner_str = inner.join("\n");
|
||||
format!("{} @ /{}\n{}", #ident_str, path_str, inner_str)
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "logging-base"))]
|
||||
fn debug_view(&self, _indent: usize) -> String {
|
||||
"".to_string()
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Hand the output tokens back to the compiler
|
||||
expanded.into()
|
||||
}
|
||||
_ => {
|
||||
return quote_spanned! { ident.span() => compile_error!("Cannot derive CRDT on tuple or unit structs"); }
|
||||
.into()
|
||||
}
|
||||
},
|
||||
_ => return quote_spanned! { ident.span() => compile_error!("Cannot derive CRDT on enums or unions"); }.into(),
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user