From eb212996192749ba3cb370a239ffe0f31a6707e8 Mon Sep 17 00:00:00 2001 From: arctic_hen7 Date: Tue, 2 Nov 2021 07:42:48 +1100 Subject: [PATCH] =?UTF-8?q?feat(templates):=20=E2=9C=A8=20added=20`autoser?= =?UTF-8?q?de`=20macro=20to=20improve=20ergonomics?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Further addresses #57. --- examples/basic/src/templates/index.rs | 17 +- examples/i18n/src/templates/post.rs | 10 +- .../showcase/src/templates/amalgamation.rs | 26 ++- examples/showcase/src/templates/index.rs | 1 + examples/showcase/src/templates/ip.rs | 7 +- examples/showcase/src/templates/post.rs | 10 +- examples/showcase/src/templates/time.rs | 10 +- examples/showcase/src/templates/time_root.rs | 10 +- packages/perseus-macro/src/autoserde.rs | 159 ++++++++++++------ packages/perseus-macro/src/template.rs | 8 +- packages/perseus-macro/src/test.rs | 2 +- 11 files changed, 171 insertions(+), 89 deletions(-) diff --git a/examples/basic/src/templates/index.rs b/examples/basic/src/templates/index.rs index 68c34062e2..ece7173806 100644 --- a/examples/basic/src/templates/index.rs +++ b/examples/basic/src/templates/index.rs @@ -31,17 +31,22 @@ pub fn get_template() -> Template { .set_headers_fn(set_headers) } -pub async fn get_build_props(_path: String, _locale: String) -> RenderFnResultWithCause { - Ok(serde_json::to_string(&IndexPageProps { +#[perseus::autoserde(build_state)] +pub async fn get_build_props( + _path: String, + _locale: String, +) -> RenderFnResultWithCause { + Ok(IndexPageProps { greeting: "Hello World!".to_string(), - })?) // This `?` declares the default, that the server is the cause of the error + }) } -pub fn set_headers(_props: Option) -> HeaderMap { +#[perseus::autoserde(set_headers)] +pub fn set_headers(props: Option) -> HeaderMap { let mut map = HeaderMap::new(); map.insert( - HeaderName::from_lowercase(b"x-test").unwrap(), - "custom value".parse().unwrap(), + HeaderName::from_lowercase(b"x-greeting").unwrap(), + props.unwrap().greeting.parse().unwrap(), ); map } diff --git a/examples/i18n/src/templates/post.rs b/examples/i18n/src/templates/post.rs index f48653a30f..1e47187b7f 100644 --- a/examples/i18n/src/templates/post.rs +++ b/examples/i18n/src/templates/post.rs @@ -33,7 +33,11 @@ pub fn get_template() -> Template { .template(post_page) } -pub async fn get_static_props(path: String, _locale: String) -> RenderFnResultWithCause { +#[perseus::autoserde(build_state)] +pub async fn get_static_props( + path: String, + _locale: String, +) -> RenderFnResultWithCause { // This is just an example let title = urlencoding::decode(&path).unwrap(); let content = format!( @@ -41,10 +45,10 @@ pub async fn get_static_props(path: String, _locale: String) -> RenderFnResultWi title, path ); - Ok(serde_json::to_string(&PostPageProps { + Ok(PostPageProps { title: title.to_string(), content, - })?) // This `?` declares the default, that the server is the cause of the error + }) // This `?` declares the default, that the server is the cause of the error } pub async fn get_static_paths() -> RenderFnResult> { diff --git a/examples/showcase/src/templates/amalgamation.rs b/examples/showcase/src/templates/amalgamation.rs index 66cb079047..32b3bda3ee 100644 --- a/examples/showcase/src/templates/amalgamation.rs +++ b/examples/showcase/src/templates/amalgamation.rs @@ -23,35 +23,43 @@ pub fn get_template() -> Template { .template(amalgamation_page) } -pub fn amalgamate_states(states: States) -> RenderFnResultWithCause> { +#[perseus::autoserde(amalgamate_states)] +pub fn amalgamate_states( + states: States, +) -> RenderFnResultWithCause> { // We know they'll both be defined let build_state = serde_json::from_str::(&states.build_state.unwrap())?; let req_state = serde_json::from_str::(&states.request_state.unwrap())?; - Ok(Some(serde_json::to_string(&AmalagamationPageProps { + Ok(Some(AmalagamationPageProps { message: format!( "Hello from the amalgamation! (Build says: '{}', server says: '{}'.)", build_state.message, req_state.message ), - })?)) + })) } -pub async fn get_build_state(_path: String, _locale: String) -> RenderFnResultWithCause { - Ok(serde_json::to_string(&AmalagamationPageProps { +#[perseus::autoserde(build_state)] +pub async fn get_build_state( + _path: String, + _locale: String, +) -> RenderFnResultWithCause { + Ok(AmalagamationPageProps { message: "Hello from the build process!".to_string(), - })?) + }) } +#[perseus::autoserde(request_state)] pub async fn get_request_state( _path: String, _locale: String, _req: Request, -) -> RenderFnResultWithCause { +) -> RenderFnResultWithCause { // Err(perseus::GenericErrorWithCause { // error: "this is a test error!".into(), // cause: perseus::ErrorCause::Client(None) // }) - Ok(serde_json::to_string(&AmalagamationPageProps { + Ok(AmalagamationPageProps { message: "Hello from the server!".to_string(), - })?) + }) } diff --git a/examples/showcase/src/templates/index.rs b/examples/showcase/src/templates/index.rs index a9ddfac7a0..3954c35c23 100644 --- a/examples/showcase/src/templates/index.rs +++ b/examples/showcase/src/templates/index.rs @@ -21,6 +21,7 @@ pub fn get_template() -> Template { .template(index_page) } +#[perseus::autoserde(build_state)] pub async fn get_static_props(_path: String, _locale: String) -> RenderFnResultWithCause { Ok(serde_json::to_string(&IndexPageProps { greeting: "Hello World!".to_string(), diff --git a/examples/showcase/src/templates/ip.rs b/examples/showcase/src/templates/ip.rs index 9d4ba498d1..a86a95b779 100644 --- a/examples/showcase/src/templates/ip.rs +++ b/examples/showcase/src/templates/ip.rs @@ -25,16 +25,17 @@ pub fn get_template() -> Template { .template(ip_page) } +#[perseus::autoserde(request_state)] pub async fn get_request_state( _path: String, _locale: String, req: Request, -) -> RenderFnResultWithCause { +) -> RenderFnResultWithCause { // Err(perseus::GenericErrorWithCause { // error: "this is a test error!".into(), // cause: perseus::ErrorCause::Client(None) // }) - Ok(serde_json::to_string(&IpPageProps { + Ok(IpPageProps { // Gets the client's IP address ip: format!( "{:?}", @@ -42,5 +43,5 @@ pub async fn get_request_state( .get("X-Forwarded-For") .unwrap_or(&perseus::http::HeaderValue::from_str("hidden from view!").unwrap()) ), - })?) + }) } diff --git a/examples/showcase/src/templates/post.rs b/examples/showcase/src/templates/post.rs index 3e5fc5dd7c..c4a4973b77 100644 --- a/examples/showcase/src/templates/post.rs +++ b/examples/showcase/src/templates/post.rs @@ -33,7 +33,11 @@ pub fn get_template() -> Template { .template(post_page) } -pub async fn get_static_props(path: String, _locale: String) -> RenderFnResultWithCause { +#[perseus::autoserde(build_state)] +pub async fn get_static_props( + path: String, + _locale: String, +) -> RenderFnResultWithCause { // This path is illegal, and can't be rendered if path == "post/tests" { return Err(GenericErrorWithCause { @@ -48,10 +52,10 @@ pub async fn get_static_props(path: String, _locale: String) -> RenderFnResultWi title, path ); - Ok(serde_json::to_string(&PostPageProps { + Ok(PostPageProps { title: title.to_string(), content, - })?) + }) } pub async fn get_static_paths() -> RenderFnResult> { diff --git a/examples/showcase/src/templates/time.rs b/examples/showcase/src/templates/time.rs index c73ad700d3..4d6e9d8b25 100644 --- a/examples/showcase/src/templates/time.rs +++ b/examples/showcase/src/templates/time.rs @@ -27,7 +27,11 @@ pub fn get_template() -> Template { .build_paths_fn(get_build_paths) } -pub async fn get_build_state(path: String, _locale: String) -> RenderFnResultWithCause { +#[perseus::autoserde(build_state)] +pub async fn get_build_state( + path: String, + _locale: String, +) -> RenderFnResultWithCause { // This path is illegal, and can't be rendered if path == "timeisr/tests" { return Err(GenericErrorWithCause { @@ -35,9 +39,9 @@ pub async fn get_build_state(path: String, _locale: String) -> RenderFnResultWit cause: ErrorCause::Client(Some(404)), }); } - Ok(serde_json::to_string(&TimePageProps { + Ok(TimePageProps { time: format!("{:?}", std::time::SystemTime::now()), - })?) + }) } pub async fn get_build_paths() -> RenderFnResult> { diff --git a/examples/showcase/src/templates/time_root.rs b/examples/showcase/src/templates/time_root.rs index 12744f698c..190c184e2d 100644 --- a/examples/showcase/src/templates/time_root.rs +++ b/examples/showcase/src/templates/time_root.rs @@ -25,8 +25,12 @@ pub fn get_template() -> Template { .build_state_fn(get_build_state) } -pub async fn get_build_state(_path: String, _locale: String) -> RenderFnResultWithCause { - Ok(serde_json::to_string(&TimePageProps { +#[perseus::autoserde(build_state)] +pub async fn get_build_state( + _path: String, + _locale: String, +) -> RenderFnResultWithCause { + Ok(TimePageProps { time: format!("{:?}", std::time::SystemTime::now()), - })?) + }) } diff --git a/packages/perseus-macro/src/autoserde.rs b/packages/perseus-macro/src/autoserde.rs index c3089615e5..9a20ef867c 100644 --- a/packages/perseus-macro/src/autoserde.rs +++ b/packages/perseus-macro/src/autoserde.rs @@ -1,30 +1,33 @@ use darling::FromMeta; use proc_macro2::TokenStream; -use quote::{quote, ToTokens}; +use quote::quote; use syn::parse::{Parse, ParseStream}; use syn::{ - Attribute, Block, FnArg, Generics, Ident, Item, ItemFn, Result, ReturnType, Type, Visibility, + punctuated::Punctuated, token::Comma, Attribute, Block, FnArg, Generics, Ident, Item, ItemFn, + Result, ReturnType, Type, Visibility, }; /// The arguments that the `autoserde` annotation takes. -#[derive(Debug, FromMeta)] -pub enum AutoserdeArgs { - #[darling(rename = "build_state")] - BuildState, - #[darling(rename = "request_state")] - RequestState, - #[darling(rename = "set_headers")] - SetHeaders, - #[darling(rename = "amalgamate_states")] - AmalgamateStates, +// TODO prevent the user from providing more than one of these +#[derive(Debug, FromMeta, PartialEq, Eq)] +pub struct AutoserdeArgs { + #[darling(default)] + build_state: bool, + #[darling(default)] + request_state: bool, + #[darling(default)] + set_headers: bool, + #[darling(default)] + amalgamate_states: bool, } /// A function that can be wrapped in the Perseus test sub-harness. pub struct AutoserdeFn { /// The body of the function. pub block: Box, - // The possible single argument for custom properties, or there might be no arguments. - pub arg: Option, + /// The arguments that the function takes. We don't need to modify these because we wrap them with a functin that does serializing/ + /// deserializing. + pub args: Punctuated, /// The visibility of the function. pub vis: Visibility, /// Any attributes the function uses. @@ -33,7 +36,7 @@ pub struct AutoserdeFn { pub name: Ident, /// The return type of the function. pub return_type: Box, - /// Any generics the function takes (should be one for the Sycamore `GenericNode`). + /// Any generics the function takes (shouldn't be any, but it's possible). pub generics: Generics, } impl Parse for AutoserdeFn { @@ -49,57 +52,34 @@ impl Parse for AutoserdeFn { block, } = func; // Validate each part of this function to make sure it fulfills the requirements - // Mustn't be async - if sig.asyncness.is_some() { - return Err(syn::Error::new_spanned( - sig.asyncness, - "templates cannot be asynchronous", - )); - } // Can't be const if sig.constness.is_some() { return Err(syn::Error::new_spanned( sig.constness, - "const functions can't be used as templates", + "const functions can't be automatically serialized and deserialized for", )); } // Can't be external if sig.abi.is_some() { return Err(syn::Error::new_spanned( sig.abi, - "external functions can't be used as templates", + "external functions can't be automatically serialized and deserialized for", )); } - // Must return `std::result::Result<(), fantoccini::error::CmdError>` + // Must have an explicit return type let return_type = match sig.output { ReturnType::Default => { return Err(syn::Error::new_spanned( sig, - "test function must return `std::result::Result<(), fantoccini::error::CmdError>`", + "template functions can't return default/inferred type", )) } ReturnType::Type(_, ty) => ty, }; - // Can either accept a single argument for properties or no arguments - let mut inputs = sig.inputs.into_iter(); - let arg: Option = inputs.next(); - // We don't care what the type is, as long as it's not `self` - if let Some(FnArg::Receiver(arg)) = arg { - return Err(syn::Error::new_spanned(arg, "templates can't take `self`")); - } - - // This operates on what's left over after calling `.next()` - if inputs.len() > 0 { - let params: TokenStream = inputs.map(|it| it.to_token_stream()).collect(); - return Err(syn::Error::new_spanned( - params, - "test functions must accept either one argument for custom properties or no arguments", - )); - } Ok(Self { block, - arg, + args: sig.inputs, vis, attrs, name: sig.ident, @@ -115,16 +95,87 @@ impl Parse for AutoserdeFn { } } -pub fn autoserde_impl(input: AutoserdeFn, args: AutoserdeArgs) -> TokenStream { - // let AutoserdeFn { - // block, - // arg, - // generics, - // vis, - // attrs, - // name, - // return_type, - // } = input; +pub fn autoserde_impl(input: AutoserdeFn, fn_type: AutoserdeArgs) -> TokenStream { + let AutoserdeFn { + block, + args, + generics, + vis, + attrs, + name, + return_type, + } = input; - todo!() + if fn_type.build_state { + // This will always be asynchronous + quote! { + #vis async fn #name(path: ::std::string::String, locale: ::std::string::String) -> ::perseus::RenderFnResultWithCause<::std::string::String> { + // The user's function + // We can assume the return type to be `RenderFnResultWithCause` + #(#attrs)* + async fn #name#generics(#args) -> #return_type { + #block + } + // Call the user's function with the usual arguments and then serialize the result to a string + // We only serialize the `Ok` outcome, errors are left as-is + // We also assume that this will serialize correctly + let build_state = #name(path, locale).await; + let build_state_with_str = build_state.map(|val| ::serde_json::to_string(&val).unwrap()); + build_state_with_str + } + } + } else if fn_type.request_state { + // This will always be asynchronous + quote! { + #vis async fn #name(path: ::std::string::String, locale: ::std::string::String, req: ::perseus::Request) -> ::perseus::RenderFnResultWithCause<::std::string::String> { + // The user's function + // We can assume the return type to be `RenderFnResultWithCause` + #(#attrs)* + async fn #name#generics(#args) -> #return_type { + #block + } + // Call the user's function with the usual arguments and then serialize the result to a string + // We only serialize the `Ok` outcome, errors are left as-is + // We also assume that this will serialize correctly + let req_state = #name(path, locale, req).await; + let req_state_with_str = req_state.map(|val| ::serde_json::to_string(&val).unwrap()); + req_state_with_str + } + } + } else if fn_type.set_headers { + // This will always be synchronous + quote! { + #vis fn #name(props: ::std::option::Option<::std::string::String>) -> ::perseus::http::header::HeaderMap { + // The user's function + // We can assume the return type to be `HeaderMap` + #(#attrs)* + fn #name#generics(#args) -> #return_type { + #block + } + // Deserialize the props and then call the user's function + let props_de = props.map(|val| ::serde_json::from_str(&val).unwrap()); + #name(props_de) + } + } + } else if fn_type.amalgamate_states { + // This will always be synchronous + quote! { + #vis fn #name(states: ::perseus::States) -> ::perseus::RenderFnResultWithCause<::std::option::Option<::std::string::String>> { + // The user's function + // We can assume the return type to be `RenderFnResultWithCause>` + #(#attrs)* + fn #name#generics(#args) -> #return_type { + #block + } + // Call the user's function with the usual arguments and then serialize the result to a string + // We only serialize the `Ok(Some(_))` outcome, errors are left as-is + // We also assume that this will serialize correctly + let amalgamated_state = #name(states); + let amalgamated_state_with_str = amalgamated_state.map(|val| val.map(|val| ::serde_json::to_string(&val).unwrap())); + amalgamated_state_with_str + } + } + } else { + todo!() + } } diff --git a/packages/perseus-macro/src/template.rs b/packages/perseus-macro/src/template.rs index 8669bf9520..ee6c053d45 100644 --- a/packages/perseus-macro/src/template.rs +++ b/packages/perseus-macro/src/template.rs @@ -9,7 +9,7 @@ use syn::{ pub struct TemplateFn { /// The body of the function. pub block: Box, - // The possible single argument for custom properties, or there might be no arguments. + /// The possible single argument for custom properties, or there might be no arguments. pub arg: Option, /// The visibility of the function. pub vis: Visibility, @@ -56,12 +56,12 @@ impl Parse for TemplateFn { "external functions can't be used as templates", )); } - // Must return `std::result::Result<(), fantoccini::error::CmdError>` + // Must have an explicit return type let return_type = match sig.output { ReturnType::Default => { return Err(syn::Error::new_spanned( sig, - "test function must return `std::result::Result<(), fantoccini::error::CmdError>`", + "template function can't return default/inferred type", )) } ReturnType::Type(_, ty) => ty, @@ -79,7 +79,7 @@ impl Parse for TemplateFn { let params: TokenStream = inputs.map(|it| it.to_token_stream()).collect(); return Err(syn::Error::new_spanned( params, - "test functions must accept either one argument for custom properties or no arguments", + "template functions must accept either one argument for custom properties or no arguments", )); } diff --git a/packages/perseus-macro/src/test.rs b/packages/perseus-macro/src/test.rs index 459d06f3ed..190b1cc8e1 100644 --- a/packages/perseus-macro/src/test.rs +++ b/packages/perseus-macro/src/test.rs @@ -18,7 +18,7 @@ pub struct TestArgs { pub struct TestFn { /// The body of the function. pub block: Box, - // The single argument for the Fantoccini client. + /// The single argument for the Fantoccini client. pub arg: FnArg, /// The visibility of the function. pub vis: Visibility,