ankurah_virtual_scroll_derive/
lib.rs

1//! Macro for generating typed scroll manager wrappers
2//!
3//! This crate provides the `generate_scroll_manager!` macro which generates
4//! platform-specific scroll manager types for use with UniFFI and WASM.
5//!
6//! # Usage
7//!
8//! Apply in your **bindings crate** (not model crate) to keep models platform-agnostic:
9//!
10//! ```ignore
11//! use ankurah_virtual_scroll_derive::generate_scroll_manager;
12//!
13//! // Re-export model types
14//! pub use my_model_crate::*;
15//!
16//! // Generate scroll manager for Message model
17//! generate_scroll_manager!(
18//!     Message,           // Model type
19//!     MessageView,       // View type
20//!     MessageLiveQuery,  // LiveQuery type
21//!     timestamp_field = "timestamp"
22//! );
23//! ```
24//!
25//! This generates `MessageScrollManager` for the appropriate platform
26//! (UniFFI when `uniffi` feature enabled, WASM when `wasm` feature enabled).
27
28mod uniffi;
29mod wasm;
30
31use proc_macro::TokenStream;
32use quote::{format_ident, quote};
33use syn::{parse::{Parse, ParseStream}, parse_macro_input, Ident, LitStr, Path, Token};
34
35/// Configuration parsed from generate_scroll_manager! macro arguments
36struct ScrollManagerConfig {
37    model_path: Path,
38    view_path: Path,
39    livequery_path: Path,
40    timestamp_field: String,
41}
42
43impl ScrollManagerConfig {
44    /// Get the simple name from a path (last segment)
45    fn model_name(&self) -> &Ident {
46        &self.model_path.segments.last().unwrap().ident
47    }
48}
49
50impl Parse for ScrollManagerConfig {
51    fn parse(input: ParseStream) -> syn::Result<Self> {
52        // Parse: Model, View, LiveQuery, timestamp_field = "field"
53        // Paths can be simple (Message) or qualified (my_crate::Message)
54        let model_path: Path = input.parse()?;
55        input.parse::<Token![,]>()?;
56
57        let view_path: Path = input.parse()?;
58        input.parse::<Token![,]>()?;
59
60        let livequery_path: Path = input.parse()?;
61        input.parse::<Token![,]>()?;
62
63        // Parse timestamp_field = "value"
64        let key: Ident = input.parse()?;
65        if key != "timestamp_field" {
66            return Err(syn::Error::new(key.span(), "expected `timestamp_field`"));
67        }
68        input.parse::<Token![=]>()?;
69        let timestamp_field: LitStr = input.parse()?;
70
71        Ok(Self {
72            model_path,
73            view_path,
74            livequery_path,
75            timestamp_field: timestamp_field.value(),
76        })
77    }
78}
79
80/// Generate a typed scroll manager wrapper for a model
81///
82/// # Arguments
83///
84/// - Model type name (e.g., `Message`)
85/// - View type name (e.g., `MessageView`)
86/// - LiveQuery type name (e.g., `MessageLiveQuery`)
87/// - `timestamp_field = "field_name"` - The timestamp field used for pagination
88///
89/// # Generated Types
90///
91/// For a model named `Message`, this generates:
92/// - `MessageScrollManager` - Platform-specific scroll manager wrapper
93///
94/// The scroll manager wraps `ankurah_virtual_scroll::ScrollManager` and integrates with
95/// the model's `LiveQuery` type for reactive pagination.
96///
97/// # Features
98///
99/// - With `uniffi` feature: generates UniFFI-compatible scroll manager
100/// - With `wasm` feature: generates WASM-compatible scroll manager
101#[proc_macro]
102pub fn generate_scroll_manager(input: TokenStream) -> TokenStream {
103    let config = parse_macro_input!(input as ScrollManagerConfig);
104
105    let model_name = config.model_name();
106    let scroll_manager_name = format_ident!("{}ScrollManager", model_name);
107    let view_path = &config.view_path;
108    let livequery_path = &config.livequery_path;
109    let timestamp_field = &config.timestamp_field;
110
111    // Generate UniFFI implementation
112    let uniffi_impl = uniffi::generate_with_paths(&scroll_manager_name, view_path, livequery_path, timestamp_field);
113
114    // Generate WASM implementation
115    let wasm_impl = wasm::generate_with_paths(&scroll_manager_name, view_path, livequery_path, timestamp_field);
116
117    let expanded = quote! {
118        #uniffi_impl
119        #wasm_impl
120    };
121
122    expanded.into()
123}
124
125// Keep the derive macro for backwards compatibility (deprecated)
126/// **Deprecated**: Use `generate_scroll_manager!` macro in bindings crate instead.
127///
128/// This derive macro is deprecated because it requires ankurah-virtual-scroll dependency
129/// in the model crate, which should remain platform-agnostic.
130#[proc_macro_derive(VirtualScroll, attributes(virtual_scroll))]
131pub fn derive_virtual_scroll(input: TokenStream) -> TokenStream {
132    let input = parse_macro_input!(input as syn::DeriveInput);
133
134    let timestamp_field = match parse_timestamp_field(&input) {
135        Ok(field) => field,
136        Err(e) => return e.to_compile_error().into(),
137    };
138
139    let model_name = &input.ident;
140    let scroll_manager_name = format_ident!("{}ScrollManager", model_name);
141    let view_name = format_ident!("{}View", model_name);
142    let livequery_name = format_ident!("{}LiveQuery", model_name);
143
144    // Generate UniFFI implementation
145    let uniffi_impl = uniffi::generate(&scroll_manager_name, &view_name, &livequery_name, &timestamp_field);
146
147    // Generate WASM implementation
148    let wasm_impl = wasm::generate(&scroll_manager_name, &view_name, &livequery_name, &timestamp_field);
149
150    let hygiene_module = format_ident!("__virtual_scroll_impl_{}", to_snake_case(&model_name.to_string()));
151
152    let expanded = quote! {
153        mod #hygiene_module {
154            use super::*;
155
156            #uniffi_impl
157            #wasm_impl
158        }
159        pub use #hygiene_module::*;
160    };
161
162    expanded.into()
163}
164
165fn parse_timestamp_field(input: &syn::DeriveInput) -> Result<String, syn::Error> {
166    for attr in &input.attrs {
167        if attr.path().is_ident("virtual_scroll") {
168            let mut timestamp_field = None;
169            attr.parse_nested_meta(|meta| {
170                if meta.path.is_ident("timestamp_field") {
171                    let value: LitStr = meta.value()?.parse()?;
172                    timestamp_field = Some(value.value());
173                    Ok(())
174                } else {
175                    Err(meta.error("unknown attribute"))
176                }
177            })?;
178            if let Some(field) = timestamp_field {
179                return Ok(field);
180            }
181        }
182    }
183
184    Err(syn::Error::new_spanned(
185        input,
186        "missing required attribute: #[virtual_scroll(timestamp_field = \"...\")]",
187    ))
188}
189
190/// Convert PascalCase to snake_case
191fn to_snake_case(s: &str) -> String {
192    s.chars()
193        .enumerate()
194        .flat_map(|(i, c)| {
195            if c.is_uppercase() && i > 0 {
196                vec!['_', c.to_lowercase().next().unwrap()]
197            } else {
198                vec![c.to_lowercase().next().unwrap()]
199            }
200        })
201        .collect()
202}