mod uniffi;
mod wasm;
use proc_macro::TokenStream;
use quote::{format_ident, quote};
use syn::{parse::{Parse, ParseStream}, parse_macro_input, Ident, LitStr, Path, Token};
struct ScrollManagerConfig {
model_path: Path,
view_path: Path,
livequery_path: Path,
timestamp_field: String,
}
impl ScrollManagerConfig {
fn model_name(&self) -> &Ident {
&self.model_path.segments.last().unwrap().ident
}
}
impl Parse for ScrollManagerConfig {
fn parse(input: ParseStream) -> syn::Result<Self> {
let model_path: Path = input.parse()?;
input.parse::<Token![,]>()?;
let view_path: Path = input.parse()?;
input.parse::<Token![,]>()?;
let livequery_path: Path = input.parse()?;
input.parse::<Token![,]>()?;
let key: Ident = input.parse()?;
if key != "timestamp_field" {
return Err(syn::Error::new(key.span(), "expected `timestamp_field`"));
}
input.parse::<Token![=]>()?;
let timestamp_field: LitStr = input.parse()?;
Ok(Self {
model_path,
view_path,
livequery_path,
timestamp_field: timestamp_field.value(),
})
}
}
#[proc_macro]
pub fn generate_scroll_manager(input: TokenStream) -> TokenStream {
let config = parse_macro_input!(input as ScrollManagerConfig);
let model_name = config.model_name();
let scroll_manager_name = format_ident!("{}ScrollManager", model_name);
let view_path = &config.view_path;
let livequery_path = &config.livequery_path;
let timestamp_field = &config.timestamp_field;
let uniffi_impl = uniffi::generate_with_paths(&scroll_manager_name, view_path, livequery_path, timestamp_field);
let wasm_impl = wasm::generate_with_paths(&scroll_manager_name, view_path, livequery_path, timestamp_field);
let expanded = quote! {
#uniffi_impl
#wasm_impl
};
expanded.into()
}
#[proc_macro_derive(VirtualScroll, attributes(virtual_scroll))]
pub fn derive_virtual_scroll(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as syn::DeriveInput);
let timestamp_field = match parse_timestamp_field(&input) {
Ok(field) => field,
Err(e) => return e.to_compile_error().into(),
};
let model_name = &input.ident;
let scroll_manager_name = format_ident!("{}ScrollManager", model_name);
let view_name = format_ident!("{}View", model_name);
let livequery_name = format_ident!("{}LiveQuery", model_name);
let uniffi_impl = uniffi::generate(&scroll_manager_name, &view_name, &livequery_name, ×tamp_field);
let wasm_impl = wasm::generate(&scroll_manager_name, &view_name, &livequery_name, ×tamp_field);
let hygiene_module = format_ident!("__virtual_scroll_impl_{}", to_snake_case(&model_name.to_string()));
let expanded = quote! {
mod #hygiene_module {
use super::*;
#uniffi_impl
#wasm_impl
}
pub use #hygiene_module::*;
};
expanded.into()
}
fn parse_timestamp_field(input: &syn::DeriveInput) -> Result<String, syn::Error> {
for attr in &input.attrs {
if attr.path().is_ident("virtual_scroll") {
let mut timestamp_field = None;
attr.parse_nested_meta(|meta| {
if meta.path.is_ident("timestamp_field") {
let value: LitStr = meta.value()?.parse()?;
timestamp_field = Some(value.value());
Ok(())
} else {
Err(meta.error("unknown attribute"))
}
})?;
if let Some(field) = timestamp_field {
return Ok(field);
}
}
}
Err(syn::Error::new_spanned(
input,
"missing required attribute: #[virtual_scroll(timestamp_field = \"...\")]",
))
}
fn to_snake_case(s: &str) -> String {
s.chars()
.enumerate()
.flat_map(|(i, c)| {
if c.is_uppercase() && i > 0 {
vec!['_', c.to_lowercase().next().unwrap()]
} else {
vec![c.to_lowercase().next().unwrap()]
}
})
.collect()
}