crdt_migrate_macros/
lib.rs1use proc_macro::TokenStream;
12use quote::quote;
13use syn::{parse_macro_input, punctuated::Punctuated, token::Comma, ItemFn, ItemStruct, Meta};
14
15#[proc_macro_attribute]
42pub fn crdt_schema(attr: TokenStream, item: TokenStream) -> TokenStream {
43 let input = parse_macro_input!(item as ItemStruct);
44 let args = parse_macro_input!(attr with Punctuated::<Meta, Comma>::parse_terminated);
45
46 let mut version: Option<u32> = None;
47 let mut table: Option<String> = None;
48 let mut min_version: Option<u32> = None;
49
50 for meta in &args {
51 if let Meta::NameValue(nv) = meta {
52 let key = nv
53 .path
54 .get_ident()
55 .map(|i| i.to_string())
56 .unwrap_or_default();
57 match key.as_str() {
58 "version" => {
59 if let syn::Expr::Lit(syn::ExprLit {
60 lit: syn::Lit::Int(lit),
61 ..
62 }) = &nv.value
63 {
64 version = lit.base10_parse().ok();
65 }
66 }
67 "table" => {
68 if let syn::Expr::Lit(syn::ExprLit {
69 lit: syn::Lit::Str(lit),
70 ..
71 }) = &nv.value
72 {
73 table = Some(lit.value());
74 }
75 }
76 "min_version" => {
77 if let syn::Expr::Lit(syn::ExprLit {
78 lit: syn::Lit::Int(lit),
79 ..
80 }) = &nv.value
81 {
82 min_version = lit.base10_parse().ok();
83 }
84 }
85 _ => {
86 return syn::Error::new_spanned(&nv.path, format!("unknown attribute `{key}`"))
87 .to_compile_error()
88 .into();
89 }
90 }
91 }
92 }
93
94 let version = match version {
95 Some(v) => v,
96 None => {
97 return syn::Error::new(
98 proc_macro2::Span::call_site(),
99 "missing required attribute `version`",
100 )
101 .to_compile_error()
102 .into();
103 }
104 };
105
106 let table = match table {
107 Some(t) => t,
108 None => {
109 return syn::Error::new(
110 proc_macro2::Span::call_site(),
111 "missing required attribute `table`",
112 )
113 .to_compile_error()
114 .into();
115 }
116 };
117
118 let min_ver = min_version.unwrap_or(1);
119 let version_u8 = version as u8;
120 let struct_name = &input.ident;
121
122 let expanded = quote! {
123 #input
124
125 impl crdt_store::CrdtVersioned for #struct_name {
126 const SCHEMA_VERSION: u8 = #version_u8;
127 }
128
129 impl crdt_migrate::Schema for #struct_name {
130 const VERSION: u32 = #version;
131 const MIN_SUPPORTED_VERSION: u32 = #min_ver;
132 const NAMESPACE: &'static str = #table;
133 }
134 };
135
136 expanded.into()
137}
138
139#[proc_macro_attribute]
181pub fn migration(attr: TokenStream, item: TokenStream) -> TokenStream {
182 let input = parse_macro_input!(item as ItemFn);
183 let args = parse_macro_input!(attr with Punctuated::<Meta, Comma>::parse_terminated);
184
185 let mut from_version: Option<u32> = None;
186 let mut to_version: Option<u32> = None;
187
188 for meta in &args {
189 if let Meta::NameValue(nv) = meta {
190 let key = nv
191 .path
192 .get_ident()
193 .map(|i| i.to_string())
194 .unwrap_or_default();
195 match key.as_str() {
196 "from" => {
197 if let syn::Expr::Lit(syn::ExprLit {
198 lit: syn::Lit::Int(lit),
199 ..
200 }) = &nv.value
201 {
202 from_version = lit.base10_parse().ok();
203 }
204 }
205 "to" => {
206 if let syn::Expr::Lit(syn::ExprLit {
207 lit: syn::Lit::Int(lit),
208 ..
209 }) = &nv.value
210 {
211 to_version = lit.base10_parse().ok();
212 }
213 }
214 _ => {
215 return syn::Error::new_spanned(&nv.path, format!("unknown attribute `{key}`"))
216 .to_compile_error()
217 .into();
218 }
219 }
220 }
221 }
222
223 let from_ver = match from_version {
224 Some(v) => v,
225 None => {
226 return syn::Error::new(
227 proc_macro2::Span::call_site(),
228 "missing required attribute `from`",
229 )
230 .to_compile_error()
231 .into();
232 }
233 };
234
235 let to_ver = match to_version {
236 Some(v) => v,
237 None => {
238 return syn::Error::new(
239 proc_macro2::Span::call_site(),
240 "missing required attribute `to`",
241 )
242 .to_compile_error()
243 .into();
244 }
245 };
246
247 let fn_name = &input.sig.ident;
248
249 let input_type = match input.sig.inputs.first() {
251 Some(syn::FnArg::Typed(pat_type)) => &pat_type.ty,
252 _ => {
253 return syn::Error::new_spanned(
254 &input.sig,
255 "migration function must take exactly one argument",
256 )
257 .to_compile_error()
258 .into();
259 }
260 };
261
262 let output_type = match &input.sig.output {
264 syn::ReturnType::Type(_, ty) => ty,
265 syn::ReturnType::Default => {
266 return syn::Error::new_spanned(
267 &input.sig,
268 "migration function must have a return type",
269 )
270 .to_compile_error()
271 .into();
272 }
273 };
274
275 let struct_name = {
277 let name = fn_name.to_string();
278 let pascal: String = name
279 .split('_')
280 .map(|part| {
281 let mut chars = part.chars();
282 match chars.next() {
283 Some(c) => c.to_uppercase().collect::<String>() + chars.as_str(),
284 None => String::new(),
285 }
286 })
287 .collect();
288 syn::Ident::new(&format!("{pascal}Migration"), fn_name.span())
289 };
290
291 let register_fn = syn::Ident::new(&format!("register_{fn_name}"), fn_name.span());
293
294 let expanded = quote! {
295 #input
296
297 pub struct #struct_name;
299
300 impl crdt_migrate::MigrationStep for #struct_name {
301 fn source_version(&self) -> u32 {
302 #from_ver
303 }
304
305 fn target_version(&self) -> u32 {
306 #to_ver
307 }
308
309 fn migrate(&self, data: &[u8]) -> Result<Vec<u8>, crdt_migrate::MigrationError> {
310 let old: #input_type = postcard::from_bytes(data)
311 .map_err(|e| crdt_migrate::MigrationError::Deserialization(
312 e.to_string()
313 ))?;
314 let new: #output_type = #fn_name(old);
315 postcard::to_allocvec(&new)
316 .map_err(|e| crdt_migrate::MigrationError::Serialization(
317 e.to_string()
318 ))
319 }
320 }
321
322 pub fn #register_fn() -> Box<dyn crdt_migrate::MigrationStep> {
324 Box::new(#struct_name)
325 }
326 };
327
328 expanded.into()
329}