From de738b27ed660942d970d0736d1f7672f79c9d1f Mon Sep 17 00:00:00 2001 From: Timmy Date: Sat, 11 Apr 2026 00:16:10 +0100 Subject: [PATCH] fix: CrdtNode derive macro defaults missing fields instead of panicking When replaying old CRDT ops that predate new struct fields (e.g. claimed_by, claim_ts added by story 479), node_from would call .unwrap() on None and panic during init. Now defaults to an empty CrdtNode::new() for missing fields, allowing schema evolution without breaking replay of historical ops. Co-Authored-By: Claude Opus 4.6 (1M context) --- crates/bft-json-crdt/bft-crdt-derive/src/lib.rs | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/crates/bft-json-crdt/bft-crdt-derive/src/lib.rs b/crates/bft-json-crdt/bft-crdt-derive/src/lib.rs index 675f1533..3810be5a 100644 --- a/crates/bft-json-crdt/bft-crdt-derive/src/lib.rs +++ b/crates/bft-json-crdt/bft-crdt-derive/src/lib.rs @@ -94,14 +94,18 @@ pub fn derive_json_crdt(input: OgTokenStream) -> OgTokenStream { Ok(#ident { path: path.clone(), id, - #(#ident_literals: obj.remove(#ident_strings) - .unwrap() - .into_node( + #(#ident_literals: if let Some(val) = obj.remove(#ident_strings) { + val.into_node( id, #crate_name::op::join_path(path.clone(), #crate_name::op::PathSegment::Field(#ident_strings.to_string())) ) .unwrap() - ),* + } else { + <#tys as #crate_name::json_crdt::CrdtNode>::new( + id, + #crate_name::op::join_path(path.clone(), #crate_name::op::PathSegment::Field(#ident_strings.to_string())) + ) + }),* }) } else { Err(format!("failed to convert {:?} -> {}", value, #ident_str.to_string()))