Allow accessing Scope from server functions, which can be used to inject server-only dependencies like HttpRequest

This commit is contained in:
Greg Johnston 2022-11-19 14:44:35 -05:00
parent 8077ae9ead
commit 6ddc720227
9 changed files with 201 additions and 55 deletions

View file

@ -27,6 +27,7 @@ cfg_if! {
let app = move |cx| {
let integration = ServerIntegration { path: path.clone() };
provide_context(cx, RouterIntegrationContext::new(integration));
provide_context(cx, req.clone());
view! { cx, <TodoApp/> }
};
@ -67,7 +68,9 @@ cfg_if! {
if let Some(server_fn) = server_fn_by_path(path.as_str()) {
let body: &[u8] = &body;
match server_fn(&body).await {
let (cx, disposer) = raw_scope_and_disposer();
provide_context(cx, req.clone());
match server_fn(cx, &body).await {
Ok(serialized) => {
// if this is Accept: application/json then send a serialized JSON response
if let Some("application/json") = accept_header {

View file

@ -34,7 +34,12 @@ cfg_if! {
}
#[server(GetTodos, "/api")]
pub async fn get_todos() -> Result<Vec<Todo>, ServerFnError> {
pub async fn get_todos(cx: Scope) -> Result<Vec<Todo>, ServerFnError> {
// this is just an example of how to access server context injected in the handlers
let req =
use_context::<actix_web::HttpRequest>(cx).expect("couldn't get HttpRequest from context");
println!("req.path = {:?}", req.path());
use futures::TryStreamExt;
let mut conn = db().await?;
@ -114,7 +119,11 @@ pub fn Todos(cx: Scope) -> Element {
let todo_deleted = delete_todo.version;
// list of todos is loaded from the server in reaction to changes
let todos = create_resource(cx, move || (add_changed(), todo_deleted()), |_| get_todos());
let todos = create_resource(
cx,
move || (add_changed(), todo_deleted()),
move |_| get_todos(cx),
);
view! {
cx,

View file

@ -7,6 +7,21 @@ use syn::{
*,
};
fn fn_arg_is_cx(f: &syn::FnArg) -> bool {
if let FnArg::Typed(t) = f {
if let Type::Path(path) = &*t.ty {
path.path
.segments
.iter()
.any(|segment| segment.ident == "Scope")
} else {
false
}
} else {
false
}
}
pub fn server_macro_impl(args: proc_macro::TokenStream, s: TokenStream2) -> Result<TokenStream2> {
let ServerFnName {
struct_name,
@ -31,7 +46,7 @@ pub fn server_macro_impl(args: proc_macro::TokenStream, s: TokenStream2) -> Resu
}
}
let fields = body.inputs.iter().map(|f| {
let fields = body.inputs.iter().filter(|f| !fn_arg_is_cx(f)).map(|f| {
let typed_arg = match f {
FnArg::Receiver(_) => panic!("cannot use receiver types in server function macro"),
FnArg::Typed(t) => t,
@ -39,6 +54,28 @@ pub fn server_macro_impl(args: proc_macro::TokenStream, s: TokenStream2) -> Resu
quote! { pub #typed_arg }
});
let cx_arg = body
.inputs
.iter()
.next()
.and_then(|f| if fn_arg_is_cx(f) { Some(f) } else { None });
let cx_assign_statement = if let Some(FnArg::Typed(arg)) = cx_arg {
if let Pat::Ident(id) = &*arg.pat {
quote! {
let #id = cx;
}
} else {
quote! {}
}
} else {
quote! {}
};
let cx_fn_arg = if cx_arg.is_some() {
quote! { cx, }
} else {
quote! {}
};
let fn_args = body.inputs.iter().map(|f| {
let typed_arg = match f {
FnArg::Receiver(_) => panic!("cannot use receiver types in server function macro"),
@ -50,7 +87,13 @@ pub fn server_macro_impl(args: proc_macro::TokenStream, s: TokenStream2) -> Resu
let field_names = body.inputs.iter().filter_map(|f| match f {
FnArg::Receiver(_) => todo!(),
FnArg::Typed(t) => Some(&t.pat),
FnArg::Typed(t) => {
if fn_arg_is_cx(f) {
None
} else {
Some(&t.pat)
}
}
});
let field_names_2 = field_names.clone();
@ -93,15 +136,16 @@ pub fn server_macro_impl(args: proc_macro::TokenStream, s: TokenStream2) -> Resu
}
#[cfg(feature = "ssr")]
fn call_fn(self) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<Self::Output, ::leptos::ServerFnError>> + Send>> {
fn call_fn(self, cx: Scope) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<Self::Output, ::leptos::ServerFnError>>>> {
let #struct_name { #(#field_names),* } = self;
Box::pin(async move { #fn_name( #(#field_names_2),*).await })
#cx_assign_statement;
Box::pin(async move { #fn_name( #cx_fn_arg #(#field_names_2),*).await })
}
#[cfg(not(feature = "ssr"))]
fn call_fn_client(self) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<Self::Output, ::leptos::ServerFnError>>>> {
fn call_fn_client(self, cx: Scope) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<Self::Output, ::leptos::ServerFnError>>>> {
let #struct_name { #(#field_names_3),* } = self;
Box::pin(async move { #fn_name( #(#field_names_4),*).await })
Box::pin(async move { #fn_name( #cx_fn_arg #(#field_names_4),*).await })
}
}

View file

@ -1,12 +1,24 @@
use proc_macro2::{Ident, Span, TokenStream};
use quote::{quote, quote_spanned};
use syn::{spanned::Spanned, ExprPath};
use syn_rsx::{Node, NodeName, NodeElement, NodeAttribute, NodeValueExpr};
use syn_rsx::{Node, NodeAttribute, NodeElement, NodeName, NodeValueExpr};
use uuid::Uuid;
use crate::{is_component_node, Mode};
const NON_BUBBLING_EVENTS: [&str; 11] = ["load", "unload", "scroll", "focus", "blur", "loadstart", "progress", "error", "abort", "load", "loadend"];
const NON_BUBBLING_EVENTS: [&str; 11] = [
"load",
"unload",
"scroll",
"focus",
"blur",
"loadstart",
"progress",
"error",
"abort",
"load",
"loadend",
];
pub(crate) fn render_view(cx: &Ident, nodes: &[Node], mode: Mode) -> TokenStream {
let template_uid = Ident::new(
@ -160,10 +172,13 @@ enum PrevSibChange {
}
fn attributes(node: &NodeElement) -> impl Iterator<Item = &NodeAttribute> {
node
.attributes
.iter()
.filter_map(|node| if let Node::Attribute(attribute) = node { Some(attribute )} else { None })
node.attributes.iter().filter_map(|node| {
if let Node::Attribute(attribute) = node {
Some(attribute)
} else {
None
}
})
}
#[allow(clippy::too_many_arguments)]
@ -210,10 +225,19 @@ fn element_to_tokens(
}
// for SSR: merge all class: attributes and class attribute
if mode == Mode::Ssr {
let class_attr = attributes(node).find(|a| a.key.to_string() == "class")
if mode == Mode::Ssr {
let class_attr = attributes(node)
.find(|a| a.key.to_string() == "class")
.map(|node| {
(node.key.span(), node.value.as_ref().and_then(|n| String::try_from(n).ok()).unwrap_or_default().trim().to_string())
(
node.key.span(),
node.value
.as_ref()
.and_then(|n| String::try_from(n).ok())
.unwrap_or_default()
.trim()
.to_string(),
)
});
let class_attrs = attributes(node).filter_map(|node| {
@ -237,7 +261,7 @@ fn element_to_tokens(
}
})
.collect::<Vec<_>>();
if class_attr.is_some() || !class_attrs.is_empty() {
expressions.push(quote::quote_spanned! {
span => leptos_buffer.push_str(" class=\"");
@ -354,7 +378,7 @@ fn element_to_tokens(
expressions,
multi,
mode,
idx == 0
idx == 0,
);
prev_sib = match curr_id {
@ -393,10 +417,10 @@ fn next_sibling_node(children: &[Node], idx: usize, next_el_id: &mut usize) -> O
} else {
Some(child_ident(*next_el_id + 1, sibling.name.span()))
}
},
}
Node::Block(sibling) => Some(child_ident(*next_el_id + 1, sibling.value.span())),
Node::Text(sibling) => Some(child_ident(*next_el_id + 1, sibling.value.span())),
_ => panic!("expected either an element or a block")
_ => panic!("expected either an element or a block"),
}
}
}
@ -485,7 +509,7 @@ fn attr_to_tokens(
.as_ref();
if mode != Mode::Ssr {
let name = name.replacen("on:", "", 1);
let name = name.replacen("on:", "", 1);
if NON_BUBBLING_EVENTS.contains(&name.as_str()) {
expressions.push(quote_spanned! {
span => ::leptos::add_event_listener_undelegated(#el_id.unchecked_ref(), #name, #handler);
@ -504,23 +528,31 @@ fn attr_to_tokens(
}
}
// Properties
else if name.starts_with("prop:") {
else if name.starts_with("prop:") {
let name = name.replacen("prop:", "", 1);
// can't set properties in SSR
if mode != Mode::Ssr {
let value = node.value.as_ref().expect("prop: blocks need values").as_ref();
if mode != Mode::Ssr {
let value = node
.value
.as_ref()
.expect("prop: blocks need values")
.as_ref();
expressions.push(quote_spanned! {
span => leptos_dom::property(#cx, #el_id.unchecked_ref(), #name, #value.into_property(#cx))
});
}
}
// Classes
else if name.starts_with("class:") {
else if name.starts_with("class:") {
let name = name.replacen("class:", "", 1);
if mode == Mode::Ssr {
// handled separately because they need to be merged
} else {
let value = node.value.as_ref().expect("class: attributes need values").as_ref();
let value = node
.value
.as_ref()
.expect("class: attributes need values")
.as_ref();
expressions.push(quote_spanned! {
span => leptos_dom::class(#cx, #el_id.unchecked_ref(), #name, #value.into_class(#cx))
});
@ -599,7 +631,7 @@ fn child_to_tokens(
expressions: &mut Vec<TokenStream>,
multi: bool,
mode: Mode,
is_first_child: bool
is_first_child: bool,
) -> PrevSibChange {
match node {
Node::Element(node) => {
@ -617,7 +649,7 @@ fn child_to_tokens(
next_co_id,
multi,
mode,
is_first_child
is_first_child,
)
} else {
PrevSibChange::Sib(element_to_tokens(
@ -635,12 +667,34 @@ fn child_to_tokens(
))
}
}
Node::Text(node) => {
block_to_tokens(cx, &node.value, node.value.span(), parent, prev_sib, next_sib, next_el_id, next_co_id, template, expressions, navigations, mode)
}
Node::Block(node) => {
block_to_tokens(cx, &node.value, node.value.span(), parent, prev_sib, next_sib, next_el_id, next_co_id, template, expressions, navigations, mode)
}
Node::Text(node) => block_to_tokens(
cx,
&node.value,
node.value.span(),
parent,
prev_sib,
next_sib,
next_el_id,
next_co_id,
template,
expressions,
navigations,
mode,
),
Node::Block(node) => block_to_tokens(
cx,
&node.value,
node.value.span(),
parent,
prev_sib,
next_sib,
next_el_id,
next_co_id,
template,
expressions,
navigations,
mode,
),
_ => panic!("unexpected child node type"),
}
}
@ -669,7 +723,7 @@ fn block_to_tokens(
syn::Lit::Float(f) => Some(f.base10_digits().to_string()),
_ => None,
},
_ => None
_ => None,
};
let current: Option<Ident> = None;
@ -804,7 +858,7 @@ fn component_to_tokens(
next_co_id: &mut usize,
multi: bool,
mode: Mode,
is_first_child: bool
is_first_child: bool,
) -> PrevSibChange {
let create_component = create_component(cx, node, mode);
let span = node.name.span();
@ -892,11 +946,13 @@ fn component_to_tokens(
match current {
Some(el) => PrevSibChange::Sib(el),
None => if is_first_child {
PrevSibChange::Parent
} else {
PrevSibChange::Skip
},
None => {
if is_first_child {
PrevSibChange::Parent
} else {
PrevSibChange::Skip
}
}
}
}

View file

@ -57,6 +57,13 @@ impl Runtime {
Self::default()
}
pub fn raw_scope_and_disposer(&'static self) -> (Scope, ScopeDisposer) {
let id = { self.scopes.borrow_mut().insert(Default::default()) };
let scope = Scope { runtime: self, id };
let disposer = ScopeDisposer(Box::new(move || scope.dispose()));
(scope, disposer)
}
pub fn run_scope_undisposed<T>(
&'static self,
f: impl FnOnce(Scope) -> T,

View file

@ -13,10 +13,22 @@ use std::{future::Future, pin::Pin};
/// This should usually only be used once, at the root of an application, because its reactive
/// values will not have access to values created under another `create_scope`.
pub fn create_scope(f: impl FnOnce(Scope) + 'static) -> ScopeDisposer {
// TODO leak
let runtime = Box::leak(Box::new(Runtime::new()));
runtime.run_scope_undisposed(f, None).2
}
#[must_use = "Scope will leak memory if the disposer function is never called"]
/// Creates a new reactive system and root reactive scope, and returns them.
///
/// This should usually only be used once, at the root of an application, because its reactive
/// values will not have access to values created under another `create_scope`.
pub fn raw_scope_and_disposer() -> (Scope, ScopeDisposer) {
// TODO leak
let runtime = Box::leak(Box::new(Runtime::new()));
runtime.raw_scope_and_disposer()
}
/// Creates a temporary scope, runs the given function, disposes of the scope,
/// and returns the value returned from the function. This is very useful for short-lived
/// applications like SSR, where actual reactivity is not required beyond the end

View file

@ -264,8 +264,8 @@ where
S: Clone + ServerFn,
{
#[cfg(feature = "ssr")]
let c = |args: &S| S::call_fn(args.clone());
let c = move |args: &S| S::call_fn(args.clone(), cx);
#[cfg(not(feature = "ssr"))]
let c = |args: &S| S::call_fn_client(args.clone());
let c = move |args: &S| S::call_fn_client(args.clone(), cx);
create_action(cx, c).using_server_fn::<S>()
}

View file

@ -31,18 +31,21 @@
//! on the server (i.e., when you have an `ssr` feature in your crate that is enabled).
//!
//! ```rust,ignore
//! # use leptos_reactive::*;
//! #[server(ReadFromDB)]
//! async fn read_posts(how_many: usize, query: String) -> Result<Vec<Posts>, ServerFnError> {
//! async fn read_posts(cx: Scope, how_many: usize, query: String) -> Result<Vec<Posts>, ServerFnError> {
//! // do some server-only work here to access the database
//! let posts = ...;
//! Ok(posts)
//! }
//!
//! // call the function
//! # run_scope(|cx| {
//! spawn_local(async {
//! let posts = read_posts(3, "my search".to_string()).await;
//! log::debug!("posts = {posts{:#?}");
//! })
//! # });
//! ```
//!
//! If you call this function from the client, it will serialize the function arguments and `POST`
@ -55,9 +58,14 @@
//! - **Server functions must return `Result<T, ServerFnError>`.** Even if the work being done
//! inside the function body cant fail, the processes of serialization/deserialization and the
//! network call are fallible.
//! - **Server function arguments and return types must be [Serializable](leptos_reactive::Serializable).**
//! - **Return types must be [Serializable](leptos_reactive::Serializable).**
//! This should be fairly obvious: we have to serialize arguments to send them to the server, and we
//! need to deserialize the result to return it to the client.
//! - **Arguments must be implement [serde::Serialize].** They are serialized as an `application/x-www-form-urlencoded`
//! form data using [`serde_urlencoded`](https://docs.rs/serde_urlencoded/latest/serde_urlencoded/).
//! - **The [Scope](leptos_reactive::Scope) comes from the server.** Optionally, the first argument of a server function
//! can be a Leptos [Scope](leptos_reactive::Scope). This scope can be used to inject dependencies like the HTTP request
//! or response or other server-only dependencies, but it does *not* have access to reactive state that exists in the client.
pub use form_urlencoded;
use leptos_reactive::*;
@ -77,8 +85,9 @@ use std::{
};
#[cfg(any(feature = "ssr", doc))]
type ServerFnTraitObj =
dyn Fn(&[u8]) -> Pin<Box<dyn Future<Output = Result<String, ServerFnError>>>> + Send + Sync;
type ServerFnTraitObj = dyn Fn(Scope, &[u8]) -> Pin<Box<dyn Future<Output = Result<String, ServerFnError>>>>
+ Send
+ Sync;
#[cfg(any(feature = "ssr", doc))]
lazy_static::lazy_static! {
@ -163,18 +172,24 @@ where
/// Runs the function on the server.
#[cfg(any(feature = "ssr", doc))]
fn call_fn(self) -> Pin<Box<dyn Future<Output = Result<Self::Output, ServerFnError>> + Send>>;
fn call_fn(
self,
cx: Scope,
) -> Pin<Box<dyn Future<Output = Result<Self::Output, ServerFnError>>>>;
/// Runs the function on the client by sending an HTTP request to the server.
#[cfg(any(not(feature = "ssr"), doc))]
fn call_fn_client(self) -> Pin<Box<dyn Future<Output = Result<Self::Output, ServerFnError>>>>;
fn call_fn_client(
self,
cx: Scope,
) -> Pin<Box<dyn Future<Output = Result<Self::Output, ServerFnError>>>>;
/// Registers the server function, allowing the server to query it by URL.
#[cfg(any(feature = "ssr", doc))]
fn register() -> Result<(), ServerFnError> {
// create the handler for this server function
// takes a String -> returns its async value
let run_server_fn = Arc::new(|data: &[u8]| {
let run_server_fn = Arc::new(|cx: Scope, data: &[u8]| {
// decode the args
let value = serde_urlencoded::from_bytes::<Self>(data)
.map_err(|e| ServerFnError::Deserialization(e.to_string()));
@ -185,7 +200,7 @@ where
};
// call the function
let result = match value.call_fn().await {
let result = match value.call_fn(cx).await {
Ok(r) => r,
Err(e) => return Err(e),
};

View file

@ -274,8 +274,8 @@ where
S: Clone + ServerFn,
{
#[cfg(feature = "ssr")]
let c = |args: &S| S::call_fn(args.clone());
let c = move |args: &S| S::call_fn(args.clone(), cx);
#[cfg(not(feature = "ssr"))]
let c = |args: &S| S::call_fn_client(args.clone());
let c = move |args: &S| S::call_fn_client(args.clone(), cx);
create_multi_action(cx, c).using_server_fn::<S>()
}