1#![forbid(unsafe_code)]
4
5use std::collections::BTreeMap;
6
7use proc_macro::TokenStream;
8use proc_macro_crate::{FoundCrate, crate_name};
9use proc_macro2::{Span, TokenStream as TokenStream2};
10use quote::{format_ident, quote};
11use syn::{
12 Data, DeriveInput, Fields, GenericArgument, Ident, LitInt, LitStr, PathArguments, Type,
13 parse_macro_input, spanned::Spanned as _,
14};
15
16#[proc_macro_derive(IrohRecord, attributes(iroh_db))]
18pub fn derive_iroh_record(input: TokenStream) -> TokenStream {
19 let input = parse_macro_input!(input as DeriveInput);
20 expand(input)
21 .unwrap_or_else(syn::Error::into_compile_error)
22 .into()
23}
24
25#[derive(Default)]
26struct RecordAttrs {
27 collection: Option<String>,
28 version: Option<u32>,
29}
30
31#[derive(Default)]
32struct FieldAttrs {
33 id: bool,
34 indexed: bool,
35 field_id: Option<u32>,
36}
37
38struct FieldInfo {
39 ident: Ident,
40 ty: Type,
41 field_id: u32,
42 is_id: bool,
43 indexed: bool,
44 value_ty: Type,
45 kind: Ident,
46}
47
48#[allow(clippy::too_many_lines)]
50fn expand(input: DeriveInput) -> syn::Result<TokenStream2> {
51 if !input.generics.params.is_empty() {
52 return Err(syn::Error::new_spanned(
53 input.generics,
54 "IrohRecord does not support generic records; use a concrete durable schema type",
55 ));
56 }
57
58 let attrs = parse_record_attrs(&input)?;
59 let fields = match &input.data {
60 Data::Struct(data) => match &data.fields {
61 Fields::Named(fields) => &fields.named,
62 Fields::Unnamed(_) | Fields::Unit => {
63 return Err(syn::Error::new_spanned(
64 &input.ident,
65 "IrohRecord requires a struct with named fields",
66 ));
67 }
68 },
69 Data::Enum(_) | Data::Union(_) => {
70 return Err(syn::Error::new_spanned(
71 &input.ident,
72 "IrohRecord can only be derived for structs",
73 ));
74 }
75 };
76
77 let mut infos: Vec<FieldInfo> = Vec::with_capacity(fields.len());
78 let mut numeric_ids = BTreeMap::<u32, Span>::new();
79 let mut record_id_index = None;
80 for field in fields {
81 let ident = field
82 .ident
83 .clone()
84 .expect("named fields always carry identifiers");
85 let field_attrs = parse_field_attrs(field)?;
86 let field_id = field_attrs
87 .field_id
88 .unwrap_or_else(|| stable_field_id(&ident.to_string()));
89 if field_id == 0 {
90 return Err(syn::Error::new(
91 ident.span(),
92 "field_id 0 is reserved for record-level operations",
93 ));
94 }
95 if let Some(previous) = numeric_ids.insert(field_id, ident.span()) {
96 let mut error = syn::Error::new(
97 ident.span(),
98 format!("duplicate iroh-db field_id {field_id}"),
99 );
100 error.combine(syn::Error::new(previous, "first used by this field"));
101 return Err(error);
102 }
103
104 let (kind, value_ty) = if field_attrs.id {
105 if let Some(previous) = record_id_index.replace(infos.len()) {
106 let mut error = syn::Error::new(ident.span(), "multiple #[iroh_db(id)] fields");
107 error.combine(syn::Error::new(
108 infos[previous].ident.span(),
109 "first record ID is here",
110 ));
111 return Err(error);
112 }
113 (format_ident!("Immutable"), field.ty.clone())
114 } else {
115 replicated_type(&field.ty)?
116 };
117
118 infos.push(FieldInfo {
119 ident,
120 ty: field.ty.clone(),
121 field_id,
122 is_id: field_attrs.id,
123 indexed: field_attrs.indexed,
124 value_ty,
125 kind,
126 });
127 }
128
129 let id_index = record_id_index.ok_or_else(|| {
130 syn::Error::new_spanned(
131 &input.ident,
132 "IrohRecord requires exactly one #[iroh_db(id)] field",
133 )
134 })?;
135 if infos[id_index].indexed {
136 return Err(syn::Error::new(
137 infos[id_index].ident.span(),
138 "the record ID is always indexed; remove #[iroh_db(index)]",
139 ));
140 }
141
142 let core = core_path()?;
143 let name = &input.ident;
144 let collection = attrs.collection.unwrap_or_else(|| name.to_string());
145 let collection = LitStr::new(&collection, name.span());
146 let version = attrs.version.unwrap_or(1);
147 let id_ident = &infos[id_index].ident;
148 let id_ty = &infos[id_index].ty;
149
150 let descriptors = infos.iter().map(|field| {
151 let field_id = field.field_id;
152 let field_name = LitStr::new(&field.ident.to_string(), field.ident.span());
153 let kind = &field.kind;
154 let value_ty = &field.value_ty;
155 let indexed = field.indexed;
156 let record_id = field.is_id;
157 quote! {
158 {
159 let descriptor = #core::FieldDescriptor::new(
160 #field_id,
161 #field_name,
162 #core::CrdtKind::#kind,
163 stringify!(#value_ty),
164 );
165 let descriptor = if #indexed { descriptor.indexed() } else { descriptor };
166 if #record_id { descriptor.record_id() } else { descriptor }
167 }
168 }
169 });
170
171 let encoders = infos.iter().map(|field| {
172 let ident = &field.ident;
173 let ty = &field.ty;
174 let field_id = field.field_id;
175 if field.is_id {
176 quote! { (#field_id, <#ty as #core::RecordKey>::encode_key(&self.#ident)?) }
177 } else {
178 quote! { (#field_id, <#ty as #core::FieldState>::encode_state(&self.#ident)?) }
179 }
180 });
181
182 let decoders = infos.iter().map(|field| {
183 let ident = &field.ident;
184 let ty = &field.ty;
185 let field_id = field.field_id;
186 if field.is_id {
187 quote! { #ident: <#ty as #core::RecordKey>::decode_key(fields.required(#field_id)?)? }
188 } else {
189 quote! { #ident: <#ty as #core::FieldState>::decode_state(fields.required(#field_id)?)? }
190 }
191 });
192
193 let constants = infos.iter().map(|field| {
194 let ident = &field.ident;
195 let field_id = field.field_id;
196 let field_name = LitStr::new(&ident.to_string(), ident.span());
197 let value_ty = &field.value_ty;
198 let indexed = field.indexed || field.is_id;
199 quote! {
200 #[allow(non_upper_case_globals)]
201 pub const #ident: #core::TypedField<Self, #value_ty> =
202 #core::TypedField::new(#field_id, #field_name, #indexed);
203 }
204 });
205
206 let value_extractors = infos.iter().map(|field| {
207 let ident = &field.ident;
208 let ty = &field.ty;
209 let field_id = field.field_id;
210 if field.is_id {
211 quote! { #field_id => Ok(vec![<#ty as #core::RecordKey>::encode_key(&self.#ident)?]) }
212 } else {
213 quote! { #field_id => <#ty as #core::FieldState>::visible_values(&self.#ident) }
214 }
215 });
216
217 let mergers = infos.iter().filter(|field| !field.is_id).map(|field| {
218 let ident = &field.ident;
219 let ty = &field.ty;
220 quote! {
221 <#ty as #core::FieldState>::merge_state(&mut self.#ident, &other.#ident)?;
222 }
223 });
224
225 let causal_normalizers = infos.iter().filter(|field| !field.is_id).map(|field| {
226 let ident = &field.ident;
227 let ty = &field.ty;
228 quote! {
229 self.#ident = <#ty as #core::FieldState>::canonicalize_for_commit(
230 &self.#ident,
231 author,
232 sequence,
233 context,
234 next_operation,
235 )?;
236 }
237 });
238
239 Ok(quote! {
240 impl #core::IrohRecord for #name {
241 type Id = #id_ty;
242
243 fn schema() -> ::core::result::Result<#core::SchemaDescriptor, #core::RecordError> {
244 Ok(#core::SchemaDescriptor::new(
245 #collection,
246 #version,
247 vec![#(#descriptors),*],
248 )?)
249 }
250
251 fn id(&self) -> &Self::Id {
252 &self.#id_ident
253 }
254
255 fn encode_record(&self) -> ::core::result::Result<Vec<u8>, #core::RecordError> {
256 #core::encode_record_fields([#(#encoders),*])
257 }
258
259 fn decode_record(bytes: &[u8]) -> ::core::result::Result<Self, #core::RecordError> {
260 let fields = #core::decode_record_fields(bytes)?;
261 Ok(Self { #(#decoders),* })
262 }
263
264 fn field_values(
265 &self,
266 field_id: u32,
267 ) -> ::core::result::Result<Vec<Vec<u8>>, #core::RecordError> {
268 match field_id {
269 #(#value_extractors),*,
270 _ => Ok(Vec::new()),
271 }
272 }
273
274 fn merge_record(&mut self, other: &Self) -> ::core::result::Result<(), #core::RecordError> {
275 if self.record_id()? != other.record_id()? {
276 return Err(#core::RecordError::MergeUnavailable);
277 }
278 #(#mergers)*
279 Ok(())
280 }
281
282 fn canonicalize_causality(
283 &mut self,
284 author: #core::AuthorId,
285 sequence: u64,
286 context: &#core::VersionVector,
287 next_operation: &mut u32,
288 ) -> ::core::result::Result<(), #core::RecordError> {
289 #(#causal_normalizers)*
290 Ok(())
291 }
292 }
293
294 impl #name {
295 #(#constants)*
296 }
297 })
298}
299
300fn parse_record_attrs(input: &DeriveInput) -> syn::Result<RecordAttrs> {
301 let mut result = RecordAttrs::default();
302 for attr in &input.attrs {
303 if !attr.path().is_ident("iroh_db") {
304 continue;
305 }
306 attr.parse_nested_meta(|meta| {
307 if meta.path.is_ident("collection") {
308 if result.collection.is_some() {
309 return Err(meta.error("duplicate collection attribute"));
310 }
311 result.collection = Some(meta.value()?.parse::<LitStr>()?.value());
312 Ok(())
313 } else if meta.path.is_ident("version") {
314 if result.version.is_some() {
315 return Err(meta.error("duplicate version attribute"));
316 }
317 result.version = Some(meta.value()?.parse::<LitInt>()?.base10_parse()?);
318 Ok(())
319 } else {
320 Err(meta.error("unsupported iroh_db record attribute"))
321 }
322 })?;
323 }
324 Ok(result)
325}
326
327fn parse_field_attrs(field: &syn::Field) -> syn::Result<FieldAttrs> {
328 let mut result = FieldAttrs::default();
329 for attr in &field.attrs {
330 if !attr.path().is_ident("iroh_db") {
331 continue;
332 }
333 attr.parse_nested_meta(|meta| {
334 if meta.path.is_ident("id") {
335 if result.id {
336 return Err(meta.error("duplicate id attribute"));
337 }
338 result.id = true;
339 Ok(())
340 } else if meta.path.is_ident("index") {
341 if result.indexed {
342 return Err(meta.error("duplicate index attribute"));
343 }
344 result.indexed = true;
345 Ok(())
346 } else if meta.path.is_ident("field_id") {
347 if result.field_id.is_some() {
348 return Err(meta.error("duplicate field_id attribute"));
349 }
350 result.field_id = Some(meta.value()?.parse::<LitInt>()?.base10_parse()?);
351 Ok(())
352 } else {
353 Err(meta.error("unsupported iroh_db field attribute"))
354 }
355 })?;
356 }
357 Ok(result)
358}
359
360fn replicated_type(ty: &Type) -> syn::Result<(Ident, Type)> {
361 let Type::Path(path) = ty else {
362 return Err(syn::Error::new(
363 ty.span(),
364 "replicated fields must use a built-in iroh-db CRDT type",
365 ));
366 };
367 let segment =
368 path.path.segments.last().ok_or_else(|| {
369 syn::Error::new(ty.span(), "replicated field type path cannot be empty")
370 })?;
371 let kind = segment.ident.to_string();
372 if kind == "BlobRef" {
373 if !matches!(segment.arguments, PathArguments::None) {
374 return Err(syn::Error::new_spanned(
375 &segment.arguments,
376 "BlobRef does not accept a value type",
377 ));
378 }
379 return Ok((format_ident!("Immutable"), ty.clone()));
380 }
381 if kind == "PnCounter" {
382 if !matches!(segment.arguments, PathArguments::None) {
383 return Err(syn::Error::new_spanned(
384 &segment.arguments,
385 "PnCounter does not accept a value type",
386 ));
387 }
388 return Ok((segment.ident.clone(), syn::parse_quote!(i128)));
389 }
390 if !matches!(
391 kind.as_str(),
392 "Lww" | "OrSet" | "GrowOnlySet" | "MultiValue" | "Immutable" | "OrderedList"
393 ) {
394 return Err(syn::Error::new(
395 ty.span(),
396 "replicated fields must use Lww, OrSet, GrowOnlySet, PnCounter, MultiValue, Immutable, or OrderedList",
397 ));
398 }
399 let PathArguments::AngleBracketed(arguments) = &segment.arguments else {
400 return Err(syn::Error::new_spanned(
401 &segment.ident,
402 "this CRDT requires exactly one value type",
403 ));
404 };
405 let mut types = arguments.args.iter().filter_map(|argument| match argument {
406 GenericArgument::Type(ty) => Some(ty.clone()),
407 _ => None,
408 });
409 let value_ty = types.next().ok_or_else(|| {
410 syn::Error::new_spanned(arguments, "this CRDT requires exactly one value type")
411 })?;
412 if types.next().is_some() || arguments.args.len() != 1 {
413 return Err(syn::Error::new_spanned(
414 arguments,
415 "this CRDT requires exactly one value type",
416 ));
417 }
418 Ok((segment.ident.clone(), value_ty))
419}
420
421fn stable_field_id(name: &str) -> u32 {
422 let mut hasher = blake3::Hasher::new();
423 hasher.update(b"iroh-db/field/v1");
424 hasher.update(name.as_bytes());
425 let bytes: [u8; 4] = hasher.finalize().as_bytes()[..4]
426 .try_into()
427 .expect("a BLAKE3 digest always has four bytes");
428 u32::from_be_bytes(bytes).max(1)
429}
430
431fn core_path() -> syn::Result<TokenStream2> {
432 if let Ok(found) = crate_name("iroh-db") {
433 return Ok(match found {
434 FoundCrate::Itself => quote!(::iroh_db::__private::core),
435 FoundCrate::Name(name) => {
436 let name = format_ident!("{}", name.replace('-', "_"));
437 quote!(::#name::__private::core)
438 }
439 });
440 }
441 match crate_name("iroh-db-core") {
442 Ok(FoundCrate::Itself) => Ok(quote!(crate)),
443 Ok(FoundCrate::Name(name)) => {
444 let name = format_ident!("{}", name.replace('-', "_"));
445 Ok(quote!(::#name))
446 }
447 Err(error) => Err(syn::Error::new(
448 Span::call_site(),
449 format!("IrohRecord needs the iroh-db or iroh-db-core crate: {error}"),
450 )),
451 }
452}