Skip to content

Commit

Permalink
feat: improved template2 ergonomics
Browse files Browse the repository at this point in the history
This now has the exact same syntax as the old macro and doesn't need to
have unreactive types explicitly provided to it, because it can infer
them through associated types on the new `MakeRx` and `MakeUnrx` traits.
  • Loading branch information
arctic-hen7 committed Jan 21, 2022
1 parent 2956009 commit c238df9
Show file tree
Hide file tree
Showing 8 changed files with 150 additions and 144 deletions.
2 changes: 1 addition & 1 deletion examples/rx_state/src/about.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ use perseus::{get_render_ctx, Html, Template};
use sycamore::prelude::{view, Signal};
use sycamore::view::View;

#[perseus::template2(component = "AboutPage")]
#[perseus::template2(AboutPage)]
pub fn about_page() -> View<G> {
// Get the page state store manually
// The index page is just an empty string
Expand Down
8 changes: 2 additions & 6 deletions examples/rx_state/src/index.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use perseus::{Html, RenderFnResultWithCause, Template};
use sycamore::prelude::*;

use crate::global_state::{AppState, AppStateRx};
use crate::global_state::AppStateRx;

// We define a normal `struct` and then use `make_rx` (which derives `Serialize`, `Deserialize`, and `Clone` automatically)
// This will generate a new `struct` called `IndexPropsRx` (as we asked it to), in which every field is made reactive with a `Signal`
Expand All @@ -13,11 +13,7 @@ pub struct IndexProps {
// This special macro (normally we'd use `template(IndexProps)`) converts the state we generate elsewhere to a reactive version
// We need to tell it the name of the unreactive properties we created to start with (unfortunately the compiler isn't smart enough to figure that out yet)
// This will also add our reactive properties to the global state store, and, if they're already there, it'll use the existing one
#[perseus::template2(
component = "IndexPage",
unrx_props = "IndexProps",
global_state = "AppState"
)]
#[perseus::template2(IndexPage)]
pub fn index_page(IndexPropsRx { username }: IndexPropsRx, global_state: AppStateRx) -> View<G> {
let username_2 = username.clone(); // This is necessary until Sycamore's new reactive primitives are released
view! {
Expand Down
29 changes: 7 additions & 22 deletions packages/perseus-macro/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -72,38 +72,23 @@ pub fn template(args: TokenStream, input: TokenStream) -> TokenStream {
template::template_impl(parsed, arg).into()
}

/// Exactly the same as `#[template]`, but this expects your state to be reactive (use `#[make_rx]` to make it thus). This will automatically deserialize state and make it reactive,
/// allowing you to use an MVC pattern easily in Perseus. As the second argument, you'll need to provide the name of your unreactive state `struct` (this is unergonomic,
/// but the compiler isn't smart enough to infer it yet).
///
/// Additionally, this macro will add the reactive state to the global state store, and will fetch it from there, allowing template state to persists between page changes. Additionally,
/// that state can be accessed by other templates if necessary.
// TODO Rename this to `template2` and rewrite docs on it with examples
/// The new version of `#[template]` designed for reactive state. This can interface automatically with global state, and will automatically provide Sycamore `#[component]` annotations. To
/// use this, you'll need to provide your component's name (e.g. `IndexPage`) as `#[template2(component_name = )`.
/// use this, you'll need to provide your component's name (e.g. `IndexPage`) as `#[template2(IndexPage)]` (just like with the old macro). You can also provide a custom type parameter
/// name to use for your component (defaults to `G`) as the second argument.
///
/// The first argument your template function can take is state generated for it (e.g. by the *build state* strategy). If you use this, you'll need to provide the key `unrx_props` as well to this
/// macro. The argument your template function takes should be the reactive version of your state `struct` (generated with `#[make_rx]` usually), and then you can tell us the name of unreactive
/// version with `unrx_props = `.
/// The first argument your template function can take is state generated for it (e.g. by the *build state* strategy), but the reactive version (created with `#[make_rx]` usually). From this,
/// Perseus can infer the other required types and automatically make your state reactive for you.
///
/// The second argument your template function can take is a global state generated with the `GlobalStateCreator`. If you provide this, with its type being the reactive version, you'll need to
/// provide the key `global_state = ` being the unreactive version.
/// The second argument your template function can take is a global state generated with the `GlobalStateCreator`. You should also provide the reactive type here, and Perseus will do all the
/// rest in the background.
///
/// **Warning:** this macro is currently exempt from semantic versioning, and breaking changes may be introduced here at any time! If you want stability, use the `#[template]` macro (but you won't
/// get access to Perseus' reactive state platform).
#[proc_macro_attribute]
pub fn template2(args: TokenStream, input: TokenStream) -> TokenStream {
let parsed = syn::parse_macro_input!(input as template2::TemplateFn);
let attr_args = syn::parse_macro_input!(args as syn::AttributeArgs);
// Parse macro arguments with `darling`
let args = match template2::TemplateArgs::from_list(&attr_args) {
Ok(v) => v,
Err(e) => {
return TokenStream::from(e.write_errors());
}
};

template2::template_impl(parsed, args).into()
template2::template_impl(parsed, attr_args).into()
}

/// Labels a function as a Perseus head function, which is very similar to a template, but
Expand Down
20 changes: 12 additions & 8 deletions packages/perseus-macro/src/rx_state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,7 @@ pub fn make_rx_impl(mut orig_struct: ItemStruct, name: Ident) -> TokenStream {
// So that we don't have to worry about unit structs or unnamed fields, we'll just copy the struct and change the parts we want to
let mut new_struct = orig_struct.clone();
let ItemStruct {
vis,
ident,
generics,
..
ident, generics, ..
} = orig_struct.clone();

new_struct.ident = name.clone();
Expand Down Expand Up @@ -158,12 +155,19 @@ pub fn make_rx_impl(mut orig_struct: ItemStruct, name: Ident) -> TokenStream {
// We add a Serde derivation because it will always be necessary for Perseus on the original `struct`, and it's really difficult and brittle to filter it out
#[derive(::serde::Serialize, ::serde::Deserialize, ::std::clone::Clone)]
#orig_struct
impl#generics ::perseus::state::MakeRx for #ident#generics {
type Rx = #name#generics;
fn make_rx(self) -> #name#generics {
#make_rx_fields
}
}
#[derive(::std::clone::Clone)]
#new_struct
impl#generics #ident#generics {
/// Converts an instance of `#ident` into an instance of `#name`, making it reactive. This consumes `self`.
#vis fn make_rx(self) -> #name {
#make_rx_fields
impl#generics ::perseus::state::MakeUnrx for #name#generics {
type Unrx = #ident#generics;
fn make_unrx(self) -> #ident#generics {
todo!()
// #make_rx_fields
}
}
}
Expand Down
89 changes: 44 additions & 45 deletions packages/perseus-macro/src/template2.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
use darling::FromMeta;
use proc_macro2::{Span, TokenStream};
use quote::quote;
use syn::parse::{Parse, ParseStream};
use syn::{
Attribute, Block, FnArg, Generics, Ident, Item, ItemFn, PatType, Result, ReturnType, Type,
Visibility,
Attribute, AttributeArgs, Block, FnArg, Generics, Ident, Item, ItemFn, NestedMeta, PatType,
Result, ReturnType, Type, Visibility,
};

/// A function that can be wrapped in the Perseus test sub-harness.
Expand Down Expand Up @@ -99,22 +98,7 @@ impl Parse for TemplateFn {
}
}

#[derive(FromMeta)]
pub struct TemplateArgs {
/// The name of the component.
component: Ident,
/// The name of the type parameter to use (default to `G`).
#[darling(default)]
type_param: Option<Ident>,
/// The identifier of the global state type, if this template needs it.
#[darling(default)]
global_state: Option<Ident>,
/// The name of the unreactive properties, if there are any.
#[darling(default)]
unrx_props: Option<Ident>,
}

pub fn template_impl(input: TemplateFn, args: TemplateArgs) -> TokenStream {
pub fn template_impl(input: TemplateFn, attr_args: AttributeArgs) -> TokenStream {
let TemplateFn {
block,
// We know that these are all typed (none are `self`)
Expand All @@ -126,34 +110,38 @@ pub fn template_impl(input: TemplateFn, args: TemplateArgs) -> TokenStream {
return_type,
} = input;

let component_name = &args.component;
let type_param = match &args.type_param {
Some(type_param) => type_param.clone(),
None => Ident::new("G", Span::call_site()),
};
// This is only optional if the second argument wasn't provided
let global_state = if fn_args.len() == 2 {
match &args.global_state {
Some(global_state) => global_state.clone(),
None => return syn::Error::new_spanned(&fn_args[0], "template functions with two arguments must declare their global state type (`global_state = `)").to_compile_error()
// We want either one or two arguments
if attr_args.is_empty() || attr_args.len() > 2 {
return quote!(compile_error!(
"this macro takes either one or two arguments"
));
}
// This must always be provided
let component_name = match &attr_args[0] {
NestedMeta::Meta(meta) if meta.path().get_ident().is_some() => {
meta.path().get_ident().unwrap()
}
} else {
match &args.global_state {
Some(global_state) => global_state.clone(),
None => Ident::new("Dummy", Span::call_site()),
nested_meta => {
return syn::Error::new_spanned(
nested_meta,
"first argument must be a component identifier",
)
.to_compile_error()
}
};
// This is only optional if the first argument wasn't provided
let unrx_props = if !fn_args.is_empty() {
match &args.unrx_props {
Some(unrx_props) => unrx_props.clone(),
None => return syn::Error::new_spanned(&fn_args[0], "template functions with one argument or more must declare their unreactive properties type (`unrx_props = `)").to_compile_error()
// But this is optional (we'll use `G` as the default if it's not provided)
let type_param = match &attr_args.get(1) {
Some(NestedMeta::Meta(meta)) if meta.path().get_ident().is_some() => {
meta.path().get_ident().unwrap().clone()
}
} else {
match &args.unrx_props {
Some(unrx_props) => unrx_props.clone(),
None => Ident::new("Dummy", Span::call_site()),
Some(nested_meta) => {
return syn::Error::new_spanned(
nested_meta,
"optional second argument must be a type parameter identifier if it's provided",
)
.to_compile_error()
}
None => Ident::new("G", Span::call_site()),
};

// We create a wrapper function that can be easily provided to `.template()` that does deserialization automatically if needed
Expand All @@ -162,6 +150,10 @@ pub fn template_impl(input: TemplateFn, args: TemplateArgs) -> TokenStream {
// There's an argument for page properties that needs to have state extracted, so the wrapper will deserialize it
// We'll also make it reactive and add it to the page state store
let state_arg = &fn_args[0];
let rx_props_ty = match state_arg {
FnArg::Typed(PatType { ty, .. }) => ty,
FnArg::Receiver(_) => unreachable!(),
};
// There's also a second argument for the global state, which we'll deserialize and make global if it's not already (aka. if any other pages have loaded before this one)
// Sycamore won't let us have more than one argument to a component though, so we sneakily extract it and literally construct it as a variable (this should be fine?)
let global_state_arg = &fn_args[1];
Expand All @@ -171,6 +163,7 @@ pub fn template_impl(input: TemplateFn, args: TemplateArgs) -> TokenStream {
};
quote! {
#vis fn #name<G: ::sycamore::prelude::Html>(props: ::perseus::templates::PageProps) -> ::sycamore::prelude::View<G> {
use ::perseus::state::MakeRx;
// Deserialize the global state, make it reactive, and register it with the `RenderCtx`
// If it's already there, we'll leave it
// This means that we can pass an `Option<String>` around safely and then deal with it at the template site
Expand All @@ -183,7 +176,7 @@ pub fn template_impl(input: TemplateFn, args: TemplateArgs) -> TokenStream {
let mut global_state = global_state_refcell.borrow_mut();
// This will be defined if we're the first page
let global_state_props = &props.global_state.unwrap();
let new_global_state = ::serde_json::from_str::<#global_state>(global_state_props).unwrap().make_rx();
let new_global_state = ::serde_json::from_str::<<#global_state_rx as ::perseus::state::MakeUnrx>::Unrx>(global_state_props).unwrap().make_rx();
*global_state = ::std::boxed::Box::new(new_global_state);
// The component function can now access this in `RenderCtx`
}
Expand Down Expand Up @@ -212,7 +205,7 @@ pub fn template_impl(input: TemplateFn, args: TemplateArgs) -> TokenStream {
::std::option::Option::None => {
// If there are props, they will always be provided, the compiler just doesn't know that
// If the user is using this macro, they sure should be using `#[make_rx(...)]` or similar!
let rx_props = ::serde_json::from_str::<#unrx_props>(&props.state.unwrap()).unwrap().make_rx();
let rx_props: #rx_props_ty = ::serde_json::from_str::<<#rx_props_ty as ::perseus::state::MakeUnrx>::Unrx>(&props.state.unwrap()).unwrap().make_rx();
// They aren't in there, so insert them
pss.add(&props.path, rx_props.clone());
rx_props
Expand All @@ -227,8 +220,13 @@ pub fn template_impl(input: TemplateFn, args: TemplateArgs) -> TokenStream {
// There's an argument for page properties that needs to have state extracted, so the wrapper will deserialize it
// We'll also make it reactive and add it to the page state store
let arg = &fn_args[0];
let rx_props_ty = match arg {
FnArg::Typed(PatType { ty, .. }) => ty,
FnArg::Receiver(_) => unreachable!(),
};
quote! {
#vis fn #name<G: ::sycamore::prelude::Html>(props: ::perseus::templates::PageProps) -> ::sycamore::prelude::View<G> {
use ::perseus::state::MakeRx;
// The user's function, with Sycamore component annotations and the like preserved
// We know this won't be async because Sycamore doesn't allow that
#(#attrs)*
Expand All @@ -247,7 +245,7 @@ pub fn template_impl(input: TemplateFn, args: TemplateArgs) -> TokenStream {
::std::option::Option::None => {
// If there are props, they will always be provided, the compiler just doesn't know that
// If the user is using this macro, they sure should be using `#[make_rx(...)]` or similar!
let rx_props = ::serde_json::from_str::<#unrx_props>(&props.state.unwrap()).unwrap().make_rx();
let rx_props: #rx_props_ty = ::serde_json::from_str::<<#rx_props_ty as ::perseus::state::MakeUnrx>::Unrx>(&props.state.unwrap()).unwrap().make_rx();
// They aren't in there, so insert them
pss.add(&props.path, rx_props.clone());
rx_props
Expand All @@ -262,6 +260,7 @@ pub fn template_impl(input: TemplateFn, args: TemplateArgs) -> TokenStream {
// There are no arguments
quote! {
#vis fn #name<G: ::sycamore::prelude::Html>(props: ::perseus::templates::PageProps) -> ::sycamore::prelude::View<G> {
use ::perseus::state::MakeRx;
// The user's function, with Sycamore component annotations and the like preserved
// We know this won't be async because Sycamore doesn't allow that
#(#attrs)*
Expand Down
2 changes: 2 additions & 0 deletions packages/perseus/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ mod page_data;
mod page_state_store;
mod path_prefix;
mod router;
mod rx_state;
mod server;
mod shell;
mod template;
Expand Down Expand Up @@ -101,6 +102,7 @@ pub mod templates {
pub mod state {
pub use crate::global_state::GlobalStateCreator;
pub use crate::page_state_store::PageStateStore;
pub use crate::rx_state::{MakeRx, MakeUnrx};
}
/// A series of exports that should be unnecessary for nearly all uses of Perseus. These are used principally in developing alternative
/// engines.
Expand Down
Loading

0 comments on commit c238df9

Please sign in to comment.