Typed structs for Loro CRDTs
rust crdt
at main 517 lines 18 kB view raw
1//! Proc-macro implementation for the `loroscope` crate. 2//! 3//! Depend on [`loroscope`](https://docs.rs/loroscope) rather than using this 4//! crate directly. 5 6#![deny(missing_docs)] 7#![deny(clippy::unwrap_used)] 8 9use proc_macro::TokenStream; 10use quote::quote; 11use syn::punctuated::Punctuated; 12use syn::{GenericArgument, Ident, ItemStruct, Meta, PathArguments, Type, parse_macro_input}; 13 14/// Defines a typed struct over a Loro CRDT document. 15/// 16/// # Example 17/// 18/// ```ignore 19/// use loroscope::loroscope; 20/// 21/// #[loroscope] 22/// struct Player { 23/// name: Text, 24/// score: i64, 25/// } 26/// 27/// let player = Player::new(); 28/// player.set_score(100); 29/// assert_eq!(player.score(), 100); 30/// ``` 31/// 32/// # Field types 33/// 34/// | Type | Accessor | 35/// |---|---| 36/// | `f64`, `i64`, `bool`, `String` | Getter + `set_` setter | 37/// | [`Text`], [`Counter`] | Getter returning the container | 38/// | [`List<T>`], [`Map<V>`], [`MovableList<T>`], [`Tree<T>`] | Getter returning a typed collection | 39/// | `LoroList`, `LoroMap`, `LoroText`, … | Getter returning the raw Loro container | 40/// | Any `#[loroscope]` struct | Getter returning a nested typed view | 41/// 42/// # Generated methods 43/// 44/// - `new()` — creates a new [`LoroDoc`](loro::LoroDoc) and returns 45/// a typed view into it. 46/// - `doc()` — returns the [`LoroDoc`](loro::LoroDoc) this struct belongs to. 47/// - `raw()` — returns a reference to the underlying [`LoroMap`](loro::LoroMap). 48/// 49/// # Derives 50/// 51/// Derive macros on the input struct are forwarded to the generated struct. 52/// `Debug` is handled specially: it prints the struct using its field 53/// accessors rather than exposing internals. 54/// 55/// [`Text`]: https://docs.rs/loroscope/latest/loroscope/type.Text.html 56/// [`Counter`]: https://docs.rs/loroscope/latest/loroscope/type.Counter.html 57/// [`Tree`]: https://docs.rs/loroscope/latest/loroscope/type.Tree.html 58/// [`List<T>`]: https://docs.rs/loroscope/latest/loroscope/struct.List.html 59/// [`Map<V>`]: https://docs.rs/loroscope/latest/loroscope/struct.Map.html 60/// [`MovableList<T>`]: https://docs.rs/loroscope/latest/loroscope/struct.MovableList.html 61#[proc_macro_attribute] 62pub fn loroscope(attr: TokenStream, item: TokenStream) -> TokenStream { 63 if !attr.is_empty() { 64 let ident = parse_macro_input!(attr as Ident); 65 return syn::Error::new(ident.span(), "unexpected attribute argument") 66 .to_compile_error() 67 .into(); 68 } 69 70 let input = parse_macro_input!(item as ItemStruct); 71 let struct_name = &input.ident; 72 let struct_name_str = struct_name.to_string(); 73 let vis = &input.vis; 74 let map_key = struct_name.to_string(); 75 76 // Separate derive macros from other attributes, pulling out Debug specially. 77 let mut has_debug = false; 78 let mut other_derives: Vec<syn::Path> = Vec::new(); 79 let mut other_attrs: Vec<&syn::Attribute> = Vec::new(); 80 81 for attr in &input.attrs { 82 if attr.path().is_ident("derive") { 83 if let Meta::List(meta_list) = &attr.meta 84 && let Ok(paths) = meta_list 85 .parse_args_with(Punctuated::<syn::Path, syn::token::Comma>::parse_terminated) 86 { 87 for path in paths { 88 if path.is_ident("Debug") { 89 has_debug = true; 90 } else { 91 other_derives.push(path); 92 } 93 } 94 } 95 } else { 96 other_attrs.push(attr); 97 } 98 } 99 100 let derive_attr = if other_derives.is_empty() { 101 quote!() 102 } else { 103 quote!(#[derive(#(#other_derives),*)]) 104 }; 105 106 let fields = match &input.fields { 107 syn::Fields::Named(f) => &f.named, 108 _ => { 109 return syn::Error::new_spanned(&input, "expected named fields") 110 .to_compile_error() 111 .into(); 112 } 113 }; 114 115 let mut accessors = Vec::new(); 116 117 for field in fields { 118 let field_name = field.ident.as_ref().expect("named fields"); 119 let field_name_str = field_name.to_string(); 120 let field_ty = &field.ty; 121 122 match categorize_type(field_ty) { 123 TypeCategory::Primitive(prim) => { 124 accessors.push(gen_primitive_getter(field_name, &field_name_str, &prim)); 125 accessors.push(gen_primitive_setter(field_name, &field_name_str, &prim)); 126 } 127 TypeCategory::NonGenericContainer(container) => { 128 accessors.push(gen_non_generic_container_getter( 129 field_name, 130 &field_name_str, 131 &container, 132 )); 133 } 134 TypeCategory::GenericContainer(container, type_args) => { 135 accessors.push(gen_generic_container_getter( 136 field_name, 137 &field_name_str, 138 &container, 139 &type_args, 140 )); 141 } 142 TypeCategory::NestedLoroscope => { 143 accessors.push(gen_nested_getter(field_name, &field_name_str, field_ty)); 144 } 145 } 146 } 147 148 // Generate a custom Debug impl that prints as if the original fields exist. 149 let debug_impl = if has_debug { 150 let debug_fields: Vec<_> = fields 151 .iter() 152 .map(|field| { 153 let name = field.ident.as_ref().expect("named fields"); 154 let name_str = name.to_string(); 155 quote! { .field(#name_str, &self.#name()) } 156 }) 157 .collect(); 158 159 quote! { 160 impl ::core::fmt::Debug for #struct_name { 161 fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result { 162 f.debug_struct(#struct_name_str) 163 #(#debug_fields)* 164 .finish() 165 } 166 } 167 } 168 } else { 169 quote!() 170 }; 171 172 let output = quote! { 173 #(#other_attrs)* 174 #derive_attr 175 #vis struct #struct_name { 176 _map: ::loroscope::__private::LoroMap, 177 } 178 179 impl ::core::convert::From<::loroscope::__private::LoroMap> for #struct_name { 180 fn from(map: ::loroscope::__private::LoroMap) -> Self { 181 Self { _map: map } 182 } 183 } 184 185 impl ::loroscope::FromLoroMap for #struct_name { 186 fn from_loro_map(map: ::loroscope::__private::LoroMap) -> Self { 187 Self { _map: map } 188 } 189 } 190 191 #debug_impl 192 193 impl #struct_name { 194 /// Creates a new [`LoroDoc`](loro::LoroDoc) and returns a typed view into it. 195 pub fn new() -> Self { 196 let doc = ::loroscope::__private::LoroDoc::new(); 197 let map = doc.get_map(#map_key); 198 Self { _map: map } 199 } 200 201 /// Returns the [`LoroDoc`](loro::LoroDoc) this struct belongs to. 202 pub fn doc(&self) -> ::loroscope::__private::LoroDoc { 203 use ::loroscope::__private::ContainerTrait; 204 self._map.doc().expect("loroscope container is not attached to a document") 205 } 206 207 /// Returns a reference to the underlying [`LoroMap`](loro::LoroMap). 208 pub fn raw(&self) -> &::loroscope::__private::LoroMap { 209 &self._map 210 } 211 212 #(#accessors)* 213 } 214 }; 215 216 output.into() 217} 218 219// --------------------------------------------------------------------------- 220// Type categorization 221// --------------------------------------------------------------------------- 222 223enum TypeCategory { 224 Primitive(PrimitiveKind), 225 NonGenericContainer(NonGenericContainerKind), 226 GenericContainer(GenericContainerKind, Vec<Type>), 227 NestedLoroscope, 228} 229 230enum PrimitiveKind { 231 F64, 232 I64, 233 Bool, 234 String, 235} 236 237enum NonGenericContainerKind { 238 Text, 239 Counter, 240 LoroTree, 241 LoroList, 242 LoroMap, 243 LoroMovableList, 244} 245 246enum GenericContainerKind { 247 List, 248 Map, 249 MovableList, 250 Tree, 251} 252 253/// Known crate-level prefixes. If a multi-segment path has one of these as 254/// the segment immediately before the type name, we treat the final segment 255/// the same as a bare import. For any other prefix (e.g. `foo::Text`) we 256/// conservatively fall back to [`TypeCategory::NestedLoroscope`]. 257const KNOWN_PREFIXES: &[&str] = &["loroscope", "loro"]; 258 259fn categorize_type(ty: &Type) -> TypeCategory { 260 if let Type::Path(type_path) = ty { 261 let segments = &type_path.path.segments; 262 let last_segment = match segments.last() { 263 Some(s) => s, 264 None => return TypeCategory::NestedLoroscope, 265 }; 266 267 // For multi-segment paths (e.g. `loroscope::Text`, `loro::LoroMap`), 268 // only proceed if the parent segment is a known crate prefix. 269 // Proc macros cannot do real name resolution, so this is a heuristic. 270 if segments.len() > 1 { 271 let parent = segments[segments.len() - 2].ident.to_string(); 272 if !KNOWN_PREFIXES.contains(&parent.as_str()) { 273 return TypeCategory::NestedLoroscope; 274 } 275 } 276 277 let ident = last_segment.ident.to_string(); 278 279 match ident.as_str() { 280 "f64" => TypeCategory::Primitive(PrimitiveKind::F64), 281 "i64" => TypeCategory::Primitive(PrimitiveKind::I64), 282 "bool" => TypeCategory::Primitive(PrimitiveKind::Bool), 283 "String" => TypeCategory::Primitive(PrimitiveKind::String), 284 "Text" | "LoroText" => TypeCategory::NonGenericContainer(NonGenericContainerKind::Text), 285 "Counter" | "LoroCounter" => { 286 TypeCategory::NonGenericContainer(NonGenericContainerKind::Counter) 287 } 288 "LoroTree" => TypeCategory::NonGenericContainer(NonGenericContainerKind::LoroTree), 289 "LoroList" => TypeCategory::NonGenericContainer(NonGenericContainerKind::LoroList), 290 "LoroMap" => TypeCategory::NonGenericContainer(NonGenericContainerKind::LoroMap), 291 "LoroMovableList" => { 292 TypeCategory::NonGenericContainer(NonGenericContainerKind::LoroMovableList) 293 } 294 "List" | "Map" | "MovableList" | "Tree" => { 295 let kind = match ident.as_str() { 296 "List" => GenericContainerKind::List, 297 "Map" => GenericContainerKind::Map, 298 "MovableList" => GenericContainerKind::MovableList, 299 "Tree" => GenericContainerKind::Tree, 300 _ => unreachable!(), 301 }; 302 let type_args = extract_type_args(&last_segment.arguments); 303 TypeCategory::GenericContainer(kind, type_args) 304 } 305 _ => TypeCategory::NestedLoroscope, 306 } 307 } else { 308 TypeCategory::NestedLoroscope 309 } 310} 311 312fn extract_type_args(args: &PathArguments) -> Vec<Type> { 313 match args { 314 PathArguments::AngleBracketed(ab) => ab 315 .args 316 .iter() 317 .filter_map(|a| { 318 if let GenericArgument::Type(t) = a { 319 Some(t.clone()) 320 } else { 321 None 322 } 323 }) 324 .collect(), 325 _ => Vec::new(), 326 } 327} 328 329// --------------------------------------------------------------------------- 330// Code generation — primitives 331// --------------------------------------------------------------------------- 332 333fn gen_primitive_getter( 334 field_name: &Ident, 335 field_name_str: &str, 336 prim: &PrimitiveKind, 337) -> proc_macro2::TokenStream { 338 match prim { 339 PrimitiveKind::F64 => quote! { 340 pub fn #field_name(&self) -> f64 { 341 self._map.get(#field_name_str) 342 .and_then(|v| v.into_value().ok()) 343 .and_then(|v| match v { 344 ::loroscope::__private::LoroValue::Double(d) => Some(d), 345 _ => None, 346 }) 347 .unwrap_or_default() 348 } 349 }, 350 PrimitiveKind::I64 => quote! { 351 pub fn #field_name(&self) -> i64 { 352 self._map.get(#field_name_str) 353 .and_then(|v| v.into_value().ok()) 354 .and_then(|v| match v { 355 ::loroscope::__private::LoroValue::I64(i) => Some(i), 356 _ => None, 357 }) 358 .unwrap_or_default() 359 } 360 }, 361 PrimitiveKind::Bool => quote! { 362 pub fn #field_name(&self) -> bool { 363 self._map.get(#field_name_str) 364 .and_then(|v| v.into_value().ok()) 365 .and_then(|v| match v { 366 ::loroscope::__private::LoroValue::Bool(b) => Some(b), 367 _ => None, 368 }) 369 .unwrap_or_default() 370 } 371 }, 372 PrimitiveKind::String => quote! { 373 pub fn #field_name(&self) -> String { 374 self._map.get(#field_name_str) 375 .and_then(|v| v.into_value().ok()) 376 .and_then(|v| match v { 377 ::loroscope::__private::LoroValue::String(s) => Some(s.to_string()), 378 _ => None, 379 }) 380 .unwrap_or_default() 381 } 382 }, 383 } 384} 385 386fn gen_primitive_setter( 387 field_name: &Ident, 388 field_name_str: &str, 389 prim: &PrimitiveKind, 390) -> proc_macro2::TokenStream { 391 let setter_name = Ident::new(&format!("set_{}", field_name), field_name.span()); 392 393 match prim { 394 PrimitiveKind::String => quote! { 395 pub fn #setter_name(&self, val: &str) { 396 self._map.insert(#field_name_str, val).expect("insert into attached map"); 397 } 398 }, 399 PrimitiveKind::F64 => quote! { 400 pub fn #setter_name(&self, val: f64) { 401 self._map.insert(#field_name_str, val).expect("insert into attached map"); 402 } 403 }, 404 PrimitiveKind::I64 => quote! { 405 pub fn #setter_name(&self, val: i64) { 406 self._map.insert(#field_name_str, val).expect("insert into attached map"); 407 } 408 }, 409 PrimitiveKind::Bool => quote! { 410 pub fn #setter_name(&self, val: bool) { 411 self._map.insert(#field_name_str, val).expect("insert into attached map"); 412 } 413 }, 414 } 415} 416 417// --------------------------------------------------------------------------- 418// Code generation — non-generic containers 419// --------------------------------------------------------------------------- 420 421fn gen_non_generic_container_getter( 422 field_name: &Ident, 423 field_name_str: &str, 424 container: &NonGenericContainerKind, 425) -> proc_macro2::TokenStream { 426 let (loro_type, loroscope_type) = match container { 427 NonGenericContainerKind::Text => ( 428 quote!(::loroscope::__private::LoroText), 429 quote!(::loroscope::Text), 430 ), 431 NonGenericContainerKind::Counter => ( 432 quote!(::loroscope::__private::LoroCounter), 433 quote!(::loroscope::Counter), 434 ), 435 NonGenericContainerKind::LoroTree => ( 436 quote!(::loroscope::__private::LoroTree), 437 quote!(::loroscope::__private::LoroTree), 438 ), 439 NonGenericContainerKind::LoroList => ( 440 quote!(::loroscope::__private::LoroList), 441 quote!(::loroscope::__private::LoroList), 442 ), 443 NonGenericContainerKind::LoroMap => ( 444 quote!(::loroscope::__private::LoroMap), 445 quote!(::loroscope::__private::LoroMap), 446 ), 447 NonGenericContainerKind::LoroMovableList => ( 448 quote!(::loroscope::__private::LoroMovableList), 449 quote!(::loroscope::__private::LoroMovableList), 450 ), 451 }; 452 453 quote! { 454 pub fn #field_name(&self) -> #loroscope_type { 455 self._map.get_or_create_container(#field_name_str, #loro_type::new()).expect("create container on attached map") 456 } 457 } 458} 459 460// --------------------------------------------------------------------------- 461// Code generation — generic containers 462// --------------------------------------------------------------------------- 463 464fn gen_generic_container_getter( 465 field_name: &Ident, 466 field_name_str: &str, 467 container: &GenericContainerKind, 468 type_args: &[Type], 469) -> proc_macro2::TokenStream { 470 let (loro_container, wrapper_path) = match container { 471 GenericContainerKind::List => ( 472 quote!(::loroscope::__private::LoroList), 473 quote!(::loroscope::List), 474 ), 475 GenericContainerKind::Map => ( 476 quote!(::loroscope::__private::LoroMap), 477 quote!(::loroscope::Map), 478 ), 479 GenericContainerKind::MovableList => ( 480 quote!(::loroscope::__private::LoroMovableList), 481 quote!(::loroscope::MovableList), 482 ), 483 GenericContainerKind::Tree => ( 484 quote!(::loroscope::__private::LoroTree), 485 quote!(::loroscope::Tree), 486 ), 487 }; 488 489 quote! { 490 pub fn #field_name(&self) -> #wrapper_path<#(#type_args),*> { 491 #wrapper_path::new( 492 self._map.get_or_create_container(#field_name_str, #loro_container::new()).expect("create container on attached map") 493 ) 494 } 495 } 496} 497 498// --------------------------------------------------------------------------- 499// Code generation — nested loroscope structs 500// --------------------------------------------------------------------------- 501 502fn gen_nested_getter( 503 field_name: &Ident, 504 field_name_str: &str, 505 field_ty: &Type, 506) -> proc_macro2::TokenStream { 507 quote! { 508 pub fn #field_name(&self) -> #field_ty { 509 ::loroscope::FromLoroMap::from_loro_map( 510 self._map.get_or_create_container( 511 #field_name_str, 512 ::loroscope::__private::LoroMap::new(), 513 ).expect("create container on attached map") 514 ) 515 } 516 } 517}