ankurah_derive/lib.rs
1mod model;
2mod property;
3mod selection;
4#[cfg(feature = "wasm")]
5mod tsify;
6#[cfg(feature = "wasm")]
7mod wasm_signal;
8mod wrapper_macros;
9
10use proc_macro::TokenStream;
11use quote::quote;
12use syn::{parse_macro_input, DeriveInput};
13
14#[proc_macro_derive(Model, attributes(active_type, ephemeral, model))]
15pub fn derive_model(input: TokenStream) -> TokenStream {
16 let input = parse_macro_input!(input as DeriveInput);
17
18 // Parse the model description
19 let desc = match model::description::ModelDescription::parse(&input) {
20 Ok(model) => model,
21 Err(e) => return e.to_compile_error().into(),
22 };
23
24 let hygiene_module = quote::format_ident!("__ankurah_derive_impl_{}", to_snake_case(&desc.name().to_string()));
25 let wasm_imports = if cfg!(feature = "wasm") {
26 quote! {
27 use ::ankurah::derive_deps::wasm_bindgen::prelude::*;
28 use ::ankurah::derive_deps::wasm_bindgen_futures;
29 }
30 } else {
31 quote! {}
32 };
33
34 // Generate implementations using the modular approach
35 let model_impl = model::model::model_impl(&desc);
36 let view_impl = model::view::view_impl(&desc);
37 let mutable_impl = model::mutable::mutable_impl(&desc);
38 #[cfg(feature = "wasm")]
39 let wasm_impl = model::wasm::wasm_impl(&input, &desc);
40 #[cfg(not(feature = "wasm"))]
41 let wasm_impl = quote! {};
42 #[cfg(all(feature = "uniffi", not(feature = "wasm")))]
43 let uniffi_impl = model::uniffi::uniffi_impl(&desc);
44 #[cfg(any(not(feature = "uniffi"), feature = "wasm"))]
45 let uniffi_impl = quote! {};
46
47 let expanded = quote! {
48 mod #hygiene_module {
49 use super::*;
50 #wasm_imports
51
52 #model_impl
53 #view_impl
54 #mutable_impl
55 #wasm_impl
56 #uniffi_impl
57 }
58 pub use #hygiene_module::*;
59 };
60
61 expanded.into()
62}
63
64/// Convert a PascalCase identifier to snake_case
65fn to_snake_case(ident: &str) -> String {
66 ident
67 .chars()
68 .enumerate()
69 .flat_map(|(i, c)| {
70 if c.is_uppercase() && i > 0 {
71 vec!['_', c.to_lowercase().next().unwrap()]
72 } else {
73 vec![c.to_lowercase().next().unwrap()]
74 }
75 })
76 .collect()
77}
78
79#[cfg(feature = "wasm")]
80#[proc_macro_derive(WasmSignal)]
81pub fn derive_wasm_signal(input: TokenStream) -> TokenStream { wasm_signal::derive_wasm_signal_impl(input) }
82
83#[proc_macro_derive(Property)]
84pub fn derive_property(input: TokenStream) -> TokenStream { property::derive_property_impl(input) }
85
86/// Generate a predicate at compile time
87///
88/// This macro supports both quoted and unquoted syntax for building predicates at compile time.
89/// Variable values are captured from the local scope and embedded into the generated predicate.
90///
91/// # Examples
92///
93/// **Unquoted form** - The most terse syntax. Supports inlined variable substitution:
94/// ```rust,ignore
95/// // Expand variables into comparisons of the same name. Equivalent to status = {status}
96/// let result = selection!({status});
97/// // Default comparison is equality but you can prefix with >, <, >=, <=, !=
98/// let result = selection!({name} AND {>age});
99/// // Equivalent to the above
100/// let result = selection!({name} AND age > {age});
101/// ```
102///
103/// **Quoted form** - Required for quoted string literals and positional arguments:
104/// ```rust,ignore
105/// let result = selection!("status = 'active'"); // Pure literals
106/// let result = selection!("status = 'active' AND {name}"); // Mixed: variable + literal
107/// let result = selection!("status = 'active' AND {}", name); // Equivalent to the above
108/// ```
109#[proc_macro]
110pub fn selection(input: TokenStream) -> TokenStream { selection::selection_macro(input) }
111
112/// Convenience macro for fetch operations with predicate syntax.
113///
114/// This macro forwards all arguments (except the context) to the `selection!` macro
115/// and then calls `fetch` on the context with the resulting predicate.
116///
117/// # Examples
118///
119/// **Unquoted form** - The most terse syntax. Supports inlined variable substitution:
120/// ```rust,ignore
121/// // Expand variables into comparisons of the same name. Equivalent to status = {status}
122/// let results = fetch!(ctx, {status}).await?;
123/// // Default comparison is equality but you can prefix with >, <, >=, <=, !=
124/// let results = fetch!(ctx, {name} AND {>age}).await?;
125/// // Equivalent to the above
126/// let results = fetch!(ctx, {name} AND age > {age}).await?;
127/// // Equivalent to:
128/// let results = ctx.fetch(selection!({name} AND {>age})).await?;
129/// ```
130///
131/// **Quoted form** - Required for quoted string literals and positional arguments:
132/// ```rust,ignore
133/// let results = fetch!(ctx, "status = 'active'").await?; // Pure literals
134/// let results = fetch!(ctx, "status = 'active' AND {name}").await?; // Mixed: variable + literal
135/// let results = fetch!(ctx, "status = 'active' AND {}", name).await?; // Equivalent to the above
136/// ```
137///
138/// See [`ankurah_derive::selection!`] documentation for complete syntax details.
139#[proc_macro]
140pub fn fetch(input: TokenStream) -> TokenStream { selection::fetch_macro(input) }
141
142/// Convenience macro for subscribe operations with predicate syntax.
143///
144/// This macro forwards all arguments (except the context and callback) to the `selection!` macro
145/// and then calls `subscribe` on the context with the resulting predicate and callback,
146/// returning a subscription handle.
147///
148/// # Examples
149///
150/// **Unquoted form** - The most terse syntax. Supports inlined variable substitution:
151/// ```rust,ignore
152/// // Expand variables into comparisons of the same name. Equivalent to status = {status}
153/// let handle = livequery!(ctx, callback, {status}).await?;
154/// // Default comparison is equality but you can prefix with >, <, >=, <=, !=
155/// let handle = livequery!(ctx, callback, {name} AND {>age}).await?;
156/// // Equivalent to the above
157/// let handle = livequery!(ctx, callback, {name} AND age > {age}).await?;
158/// // Equivalent to:
159/// let livequery = ctx.query(selection!({name} AND {>age}), callback).await?;
160/// ```
161///
162/// **Quoted form** - Required for quoted string literals and positional arguments:
163/// ```rust,ignore
164/// let handle = livequery!(ctx, callback, "status = 'active'").await?; // Pure literals
165/// let handle = livequery!(ctx, callback, "status = 'active' AND {name}").await?; // Mixed: variable + literal
166/// let handle = livequery!(ctx, callback, "status = 'active' AND {}", name).await?; // Equivalent to the above
167/// ```
168///
169/// See [`ankurah_derive::selection!`] documentation for complete syntax details.
170#[proc_macro]
171pub fn livequery(input: TokenStream) -> TokenStream { selection::subscribe_macro(input) }
172
173/// Generate WASM wrappers for all types marked as "provided" in the backend config
174///
175/// This macro should be called once per property backend to generate standard wrappers.
176/// Example usage in core/src/property/value/lww.rs:
177/// ```rust,ignore
178/// impl_provided_wrapper_types!("src/property/value/lww.ron");
179/// ```
180#[proc_macro]
181pub fn impl_provided_wrapper_types(input: TokenStream) -> TokenStream {
182 let config_filename = parse_macro_input!(input as syn::LitStr);
183 let wasm_result = wrapper_macros::impl_provided_wrapper_types_impl(&config_filename.value());
184 let uniffi_result = wrapper_macros::impl_provided_wrapper_types_uniffi_impl(&config_filename.value());
185
186 match (wasm_result, uniffi_result) {
187 (Ok(wasm_tokens), Ok(uniffi_tokens)) => {
188 let combined = quote::quote! {
189 #wasm_tokens
190 #uniffi_tokens
191 };
192 combined.into()
193 }
194 (Err(err), _) | (_, Err(err)) => err.to_compile_error().into(),
195 }
196}
197
198/// Generate a WASM wrapper for a specific custom type
199///
200/// This macro generates a wrapper for types not covered by the "provided" set.
201/// Example usage:
202/// ```rust,ignore
203/// impl_wrapper_type!(Complex); // Generates LWWComplex, YrsStringComplex, etc.
204/// ```
205#[proc_macro]
206pub fn impl_wrapper_type(input: TokenStream) -> TokenStream {
207 let custom_type = parse_macro_input!(input as syn::Type);
208 let wasm_result = wrapper_macros::impl_wrapper_type_impl(&custom_type);
209 let uniffi_result = wrapper_macros::impl_wrapper_type_uniffi_impl(&custom_type);
210
211 match (wasm_result, uniffi_result) {
212 (Ok(wasm_tokens), Ok(uniffi_tokens)) => {
213 let combined = quote::quote! {
214 #wasm_tokens
215 #uniffi_tokens
216 };
217 combined.into()
218 }
219 (Err(err), _) | (_, Err(err)) => err.to_compile_error().into(),
220 }
221}