ankurah_virtual_scroll_derive/
lib.rs1mod 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
35struct ScrollManagerConfig {
37 model_path: Path,
38 view_path: Path,
39 livequery_path: Path,
40 timestamp_field: String,
41}
42
43impl ScrollManagerConfig {
44 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 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 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#[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 let uniffi_impl = uniffi::generate_with_paths(&scroll_manager_name, view_path, livequery_path, timestamp_field);
113
114 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#[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 let uniffi_impl = uniffi::generate(&scroll_manager_name, &view_name, &livequery_name, ×tamp_field);
146
147 let wasm_impl = wasm::generate(&scroll_manager_name, &view_name, &livequery_name, ×tamp_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
190fn 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}