feat: custom_view macro to allow implementing view types on arbitrary data

This commit is contained in:
Greg Johnston 2024-08-04 21:05:04 -04:00
parent 1f4c410f78
commit 2cc6ccaacb
2 changed files with 200 additions and 0 deletions

View file

@ -0,0 +1,166 @@
use proc_macro::TokenStream;
use quote::{quote, ToTokens};
use syn::{parse::Parse, parse_macro_input, ImplItem, ItemImpl};
pub fn custom_view_impl(tokens: TokenStream) -> TokenStream {
let input = parse_macro_input!(tokens as CustomViewMacroInput);
input.into_token_stream().into()
}
#[derive(Debug)]
struct CustomViewMacroInput {
impl_block: ItemImpl,
}
impl Parse for CustomViewMacroInput {
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
let impl_block = input.parse()?;
Ok(CustomViewMacroInput { impl_block })
}
}
impl ToTokens for CustomViewMacroInput {
fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
let ItemImpl {
impl_token,
generics,
self_ty,
items,
..
} = &self.impl_block;
let impl_span = &impl_token;
let view_ty = items
.iter()
.find_map(|item| match item {
ImplItem::Type(ty) => (ty.ident == "View").then_some(&ty.ty),
_ => None,
})
.unwrap_or_else(|| {
proc_macro_error::abort!(
impl_span,
"You must include `type View = ...;` to specify the type. \
In most cases, this will be `type View = AnyView<Rndr>;"
)
});
let view_fn = items
.iter()
.find_map(|item| match item {
ImplItem::Fn(f) => {
(f.sig.ident == "into_view").then_some(&f.block)
}
_ => None,
})
.unwrap_or_else(|| {
proc_macro_error::abort!(
impl_span,
"You must include `fn into_view(self) -> Self::View` to \
specify the view function."
)
});
let generic_params = &generics.params;
let where_preds =
&generics.where_clause.as_ref().map(|wc| &wc.predicates);
tokens.extend(quote! {
#impl_token<#generic_params, Rndr> ::leptos::tachys::view::Render<Rndr> for #self_ty
where Rndr: ::leptos::tachys::renderer::Renderer, #where_preds {
type State = <#view_ty as ::leptos::tachys::view::Render<Rndr>>::State;
fn build(self) -> Self::State {
let view = #view_fn;
view.build()
}
fn rebuild(self, state: &mut Self::State) {
let view = #view_fn;
view.rebuild(state);
}
}
#impl_token<#generic_params, Rndr> ::leptos::tachys::view::add_attr::AddAnyAttr<Rndr> for #self_ty
where Rndr: ::leptos::tachys::renderer::Renderer, #where_preds {
type Output<SomeNewAttr: ::leptos::tachys::html::attribute::Attribute<Rndr>> =
<#view_ty as ::leptos::tachys::view::add_attr::AddAnyAttr<Rndr>>::Output<SomeNewAttr>;
fn add_any_attr<NewAttr: ::leptos::tachys::html::attribute::Attribute<Rndr>>(
self,
attr: NewAttr,
) -> Self::Output<NewAttr>
where
Self::Output<NewAttr>: ::leptos::tachys::view::RenderHtml<Rndr>,
{
let view = #view_fn;
view.add_any_attr(attr)
}
}
#impl_token<#generic_params, Rndr> ::leptos::tachys::view::RenderHtml<Rndr> for #self_ty
where Rndr: ::leptos::tachys::renderer::Renderer, #where_preds {
type AsyncOutput = <#view_ty as ::leptos::tachys::view::RenderHtml<Rndr>>::AsyncOutput;
const MIN_LENGTH: usize = <#view_ty as ::leptos::tachys::view::RenderHtml<Rndr>>::MIN_LENGTH;
async fn resolve(self) -> Self::AsyncOutput {
let view = #view_fn;
::leptos::tachys::view::RenderHtml::<Rndr>::resolve(view).await
}
fn dry_resolve(&mut self) {
// TODO... The problem is that view_fn expects to take self
// dry_resolve is the only one that takes &mut self
// this can only have an effect if walking over the view would read from
// resources that are not otherwise read synchronously, which is an interesting
// edge case to handle but probably (?) irrelevant for most actual use cases of
// this macro
}
fn to_html_with_buf(
self,
buf: &mut String,
position: &mut ::leptos::tachys::view::Position,
escape: bool,
mark_branches: bool,
) {
let view = #view_fn;
::leptos::tachys::view::RenderHtml::<Rndr>::to_html_with_buf(
view,
buf,
position,
escape,
mark_branches
);
}
fn to_html_async_with_buf<const OUT_OF_ORDER: bool>(
self,
buf: &mut ::leptos::tachys::ssr::StreamBuilder,
position: &mut ::leptos::tachys::view::Position,
escape: bool,
mark_branches: bool,
) where
Self: Sized,
{
let view = #view_fn;
::leptos::tachys::view::RenderHtml::<Rndr>::to_html_async_with_buf::<OUT_OF_ORDER>(
view,
buf,
position,
escape,
mark_branches
);
}
fn hydrate<const FROM_SERVER: bool>(
self,
cursor: &::leptos::tachys::hydration::Cursor<Rndr>,
position: &::leptos::tachys::view::PositionState,
) -> Self::State {
let view = #view_fn;
::leptos::tachys::view::RenderHtml::<Rndr>::hydrate::<FROM_SERVER>(
view, cursor, position
)
}
}
});
}
}

View file

@ -19,6 +19,7 @@ mod params;
mod view;
use crate::component::unmodified_fn_name_from_fn_name;
mod component;
mod custom_view;
mod slice;
mod slot;
@ -857,3 +858,36 @@ pub fn params_derive(
pub fn slice(input: TokenStream) -> TokenStream {
slice::slice_impl(input)
}
/// Implements the traits needed to make something [`IntoView`] on some type.
///
/// The renderer relies on the implementation of several traits, implementing which for a custom
/// struct involves significant boilerplate. This macro is intended to make it easier to implement
/// a view type for custom data, by allowing you to provide some custom view logic for the type.
///
/// ```rust
/// use leptos::custom_view;
///
/// struct Foo<T>(T);
///
/// #[custom_view]
/// impl<T> CustomView for Foo<T>
/// where
/// T: ToString + Send + 'static,
/// {
/// // this will usually be `AnyView<Rndr>`, but for simple types
/// // you may be able to specify the output type easily
/// type View = String;
///
/// fn into_view(self) -> Self::View {
/// self.0.to_string().to_ascii_uppercase()
/// }
/// }
#[proc_macro_error::proc_macro_error]
#[proc_macro_attribute]
pub fn custom_view(
_args: proc_macro::TokenStream,
s: TokenStream,
) -> TokenStream {
custom_view::custom_view_impl(s)
}