Compare commits

..

1 Commits

Author SHA1 Message Date
Greg Johnston
90c6249067 examples: better practice for view types in todos 2023-04-23 15:15:01 -04:00
44 changed files with 199 additions and 714 deletions

View File

@@ -90,7 +90,6 @@ args = ["make", "verify-flow"]
[env]
RUSTFLAGS = ""
LEPTOS_OUTPUT_NAME="ci" # allows examples to check/build without cargo-leptos
[env.github-actions]
RUSTFLAGS = "-D warnings"

View File

@@ -52,12 +52,6 @@ reactively update when the signal changes.
Now every time I click the button, the text should toggle between red and black as
the number switches between even and odd.
> If youre following along, make sure you go into your `index.html` and add something like this:
>
> ```html
> <style>.red { color: red; }</style>
> ```
## Dynamic Attributes
The same applies to plain attributes. Passing a plain string or primitive value to

View File

@@ -24,7 +24,6 @@ CARGO_MAKE_CRATE_WORKSPACE_MEMBERS = [
"ssr_modes_axum",
"tailwind",
"tailwind_csr_trunk",
"timer",
"todo_app_sqlite",
"todo_app_sqlite_axum",
"todo_app_sqlite_viz",

View File

@@ -12,7 +12,6 @@ leptos = { path = "../../leptos" }
console_log = "1"
log = "0.4"
console_error_panic_hook = "0.1.7"
gloo-timers = { version = "0.2.6", features = ["futures"] }
[dev-dependencies]
wasm-bindgen = "0.2"

View File

@@ -1,46 +1,24 @@
use leptos::*;
fn update_counter_bg(mut value: i32, step: i32, sig: WriteSignal<i32>) {
sig.set(value);
value += step;
if value < 1000 {
leptos::set_timeout(
move || {
update_counter_bg(value, step, sig);
},
std::time::Duration::from_millis(10),
);
}
}
/// A simple counter component.
///
/// You can use doc comments like this to document your component.
#[component]
pub fn SimpleCounter(
cx: Scope,
/// The starting value for the counter
initial_value: i32,
step: i32,
/// The change that should be applied each time the button is clicked.
step: i32
) -> impl IntoView {
let (value, set_value) = create_signal(cx, initial_value);
// update the value signal periodically
update_counter_bg(initial_value, step, set_value);
view! { cx,
<div>
<div>
<button on:click=move |_| set_value(0)>"Clear"</button>
<button on:click=move |_| set_value.update(|value| *value -= step)>"-1"</button>
<span>"Value: " {value} "!"</span>
<button on:click=move |_| set_value.update(|value| *value += step)>"+1"</button>
</div>
<Show when={move || value() % 2 == 0} fallback=|_| ()>
<For each={|| vec![1, 2, 3]} key=|key| *key view={move |cx, k| {
view! {
cx,
<article>{k}</article>
}
}}/>
</Show>
<button on:click=move |_| set_value(0)>"Clear"</button>
<button on:click=move |_| set_value.update(|value| *value -= step)>"-1"</button>
<span>"Value: " {value} "!"</span>
<button on:click=move |_| set_value.update(|value| *value += step)>"+1"</button>
</div>
}
}
}

View File

@@ -17,7 +17,6 @@ console_error_panic_hook = "0.1.7"
wasm-bindgen = "0.2.84"
wasm-bindgen-test = "0.3.34"
pretty_assertions = "1.3.0"
rstest = "0.17.0"
[dev-dependencies.web-sys]
features = ["HtmlElement", "XPathResult"]

View File

@@ -1,9 +1,6 @@
[env]
CARGO_MAKE_WASM_TEST_ARGS = "--headless --chrome"
[tasks.test-all]
dependencies = ["test", "web-test"]
[tasks.web-test]
command = "cargo"
args = ["make", "wasm-pack-test"]

View File

@@ -2,8 +2,8 @@ use leptos::{ev, html::*, *};
/// A simple counter view.
// A component is really just a function call: it runs once to create the DOM and reactive system
pub fn counter(cx: Scope, initial_value: i32, step: u32) -> impl IntoView {
let (count, set_count) = create_signal(cx, Count::new(initial_value, step));
pub fn counter(cx: Scope, initial_value: i32, step: i32) -> impl IntoView {
let (value, set_value) = create_signal(cx, initial_value);
// elements are created by calling a function with a Scope argument
// the function name is the same as the HTML tag name
@@ -16,13 +16,13 @@ pub fn counter(cx: Scope, initial_value: i32, step: u32) -> impl IntoView {
// typed events found in leptos::ev
// 1) prevent typos in event names
// 2) allow for correct type inference in callbacks
.on(ev::click, move |_| set_count.update(|count| count.clear()))
.on(ev::click, move |_| set_value.update(|value| *value = 0))
.child("Clear"),
)
.child(
button(cx)
.on(ev::click, move |_| {
set_count.update(|count| count.decrease())
set_value.update(|value| *value -= step)
})
.child("-1"),
)
@@ -31,45 +31,14 @@ pub fn counter(cx: Scope, initial_value: i32, step: u32) -> impl IntoView {
.child("Value: ")
// reactive values are passed to .child() as a tuple
// (Scope, [child function]) so an effect can be created
.child(move || count.get().value())
.child((cx, move || value.get()))
.child("!"),
)
.child(
button(cx)
.on(ev::click, move |_| {
set_count.update(|count| count.increase())
set_value.update(|value| *value += step)
})
.child("+1"),
)
}
#[derive(Debug, Clone)]
pub struct Count {
value: i32,
step: i32,
}
impl Count {
pub fn new(value: i32, step: u32) -> Self {
Count {
value,
step: step as i32,
}
}
pub fn value(&self) -> i32 {
self.value
}
pub fn increase(&mut self) {
self.value += self.step;
}
pub fn decrease(&mut self) {
self.value += -self.step;
}
pub fn clear(&mut self) {
self.value = 0;
}
}

View File

@@ -1,49 +0,0 @@
mod count {
use counter_without_macros::Count;
use pretty_assertions::assert_eq;
use rstest::rstest;
#[rstest]
#[case(-2, 1)]
#[case(-1, 1)]
#[case(0, 1)]
#[case(1, 1)]
#[case(2, 1)]
#[case(3, 2)]
#[case(4, 3)]
fn should_increase_count(#[case] initial_value: i32, #[case] step: u32) {
let mut count = Count::new(initial_value, step);
count.increase();
assert_eq!(count.value(), initial_value + step as i32);
}
#[rstest]
#[case(-2, 1)]
#[case(-1, 1)]
#[case(0, 1)]
#[case(1, 1)]
#[case(2, 1)]
#[case(3, 2)]
#[case(4, 3)]
#[trace]
fn should_decrease_count(#[case] initial_value: i32, #[case] step: u32) {
let mut count = Count::new(initial_value, step);
count.decrease();
assert_eq!(count.value(), initial_value - step as i32);
}
#[rstest]
#[case(-2, 1)]
#[case(-1, 1)]
#[case(0, 1)]
#[case(1, 1)]
#[case(2, 1)]
#[case(3, 2)]
#[case(4, 3)]
#[trace]
fn should_clear_count(#[case] initial_value: i32, #[case] step: u32) {
let mut count = Count::new(initial_value, step);
count.clear();
assert_eq!(count.value(), 0);
}
}

View File

@@ -72,7 +72,6 @@ pub fn fetch_example(cx: Scope) -> impl IntoView {
// and by using the ErrorBoundary fallback to catch Err(_)
// so we'll just implement our happy path and let the framework handle the rest
let cats_view = move || {
leptos::log!("rendering cats_view");
cats.read(cx).map(|data| {
data.map(|data| {
data.iter()
@@ -95,13 +94,13 @@ pub fn fetch_example(cx: Scope) -> impl IntoView {
}
/>
</label>
//<ErrorBoundary fallback>
<ErrorBoundary fallback>
<Transition fallback=move || {
view! { cx, <div>"Loading (Suspense Fallback)..."</div> }
}>
{cats_view}
</Transition>
//</ErrorBoundary>
</ErrorBoundary>
</div>
}
}

View File

@@ -65,7 +65,7 @@ pub fn Stories(cx: Scope) -> impl IntoView {
}}
</span>
<span>"page " {page}</span>
<Suspense
<Transition
fallback=move || view! { cx, <p>"Loading..."</p> }
>
<span class="page-link"
@@ -78,13 +78,13 @@ pub fn Stories(cx: Scope) -> impl IntoView {
"more >"
</a>
</span>
</Suspense>
</Transition>
</div>
<main class="news-list">
<div>
<Suspense
<Transition
fallback=move || view! { cx, <p>"Loading..."</p> }
//set_pending=set_pending.into()
set_pending=set_pending.into()
>
{move || match stories.read(cx) {
None => None,
@@ -105,7 +105,7 @@ pub fn Stories(cx: Scope) -> impl IntoView {
}.into_any())
}
}}
</Suspense>
</Transition>
</div>
</main>
</div>

View File

@@ -1,15 +1,12 @@
#[cfg(feature = "ssr")]
#[tokio::main]
async fn main() {
use axum::{
extract::{Extension, Path},
routing::{get, post},
Router,
};
async fn main(){
use leptos::*;
use leptos_axum::{generate_route_list, LeptosRoutes};
use ssr_modes_axum::{app::*, fallback::file_and_error_handler};
use axum::{extract::{Extension, Path}, Router, routing::{get, post}};
use std::sync::Arc;
use ssr_modes_axum::fallback::file_and_error_handler;
use ssr_modes_axum::app::*;
let conf = get_configuration(None).await.unwrap();
let addr = conf.leptos_options.site_addr;
@@ -22,11 +19,7 @@ async fn main() {
let app = Router::new()
.route("/api/*fn_name", post(leptos_axum::handle_server_fns))
.leptos_routes(
leptos_options.clone(),
routes,
|cx| view! { cx, <App/> },
)
.leptos_routes(leptos_options.clone(), routes, |cx| view! { cx, <App/> })
.fallback(file_and_error_handler)
.layer(Extension(Arc::new(leptos_options)));

View File

@@ -1,24 +0,0 @@
[package]
name = "timer"
version = "0.1.0"
edition = "2021"
[profile.release]
codegen-units = 1
lto = true
[dependencies]
leptos = { path = "../../leptos" }
console_log = "1"
log = "0.4"
console_error_panic_hook = "0.1.7"
wasm-bindgen = "0.2"
[dependencies.web-sys]
version = "0.3"
features = [
"Window",
]
[dev-dependencies]
wasm-bindgen-test = "0.3.0"

View File

@@ -1,9 +0,0 @@
[tasks.build]
command = "cargo"
args = ["+nightly", "build-all-features"]
install_crate = "cargo-all-features"
[tasks.check]
command = "cargo"
args = ["+nightly", "check-all-features"]
install_crate = "cargo-all-features"

View File

@@ -1,7 +0,0 @@
# Leptos Timer Example
This example creates a simple timer based on `setInterval` in a client side rendered app with Rust and WASM.
To run it, just issue the `trunk serve --open` command in the example root. This will build the app, run it, and open a new browser to serve it.
> If you don't have `trunk` installed, [click here for install instructions.](https://trunkrs.dev/)

View File

@@ -1,8 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<link data-trunk rel="rust" data-wasm-opt="z"/>
<link data-trunk rel="icon" type="image/ico" href="/public/favicon.ico"/>
</head>
<body></body>
</html>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -1,2 +0,0 @@
[toolchain]
channel = "nightly"

View File

@@ -1,61 +0,0 @@
use leptos::{leptos_dom::helpers::IntervalHandle, *};
use std::time::Duration;
/// Timer example, demonstrating the use of `use_interval`.
#[component]
pub fn TimerDemo(cx: Scope) -> impl IntoView {
// count_a updates with a fixed interval of 1000 ms, whereas count_b has a dynamic
// update interval.
let (count_a, set_count_a) = create_signal(cx, 0_i32);
let (count_b, set_count_b) = create_signal(cx, 0_i32);
let (interval, set_interval) = create_signal(cx, 1000);
use_interval(cx, 1000, move || {
set_count_a.update(|c| *c = *c + 1);
});
use_interval(cx, interval, move || {
set_count_b.update(|c| *c = *c + 1);
});
view! { cx,
<div>
<div>"Count A (fixed interval of 1000 ms)"</div>
<div>{count_a}</div>
<div>"Count B (dynamic interval, currently " {interval} " ms)"</div>
<div>{count_b}</div>
<input prop:value=interval on:input=move |ev| {
if let Ok(value) = event_target_value(&ev).parse::<u64>() {
set_interval(value);
}
}/>
</div>
}
}
/// Hook to wrap the underlying `setInterval` call and make it reactive w.r.t.
/// possible changes of the timer interval.
pub fn use_interval<T, F>(cx: Scope, interval_millis: T, f: F)
where
F: Fn() + Clone + 'static,
T: Into<MaybeSignal<u64>> + 'static,
{
let interval_millis = interval_millis.into();
create_effect(cx, move |prev_handle: Option<IntervalHandle>| {
// effects get their previous return value as an argument
// each time the effect runs, it will return the interval handle
// so if we have a previous one, we cancel it
if let Some(prev_handle) = prev_handle {
prev_handle.clear();
};
// here, we return the handle
set_interval_with_handle(
f.clone(),
// this is the only reactive access, so this effect will only
// re-run when the interval changes
Duration::from_millis(interval_millis.get()),
)
.expect("could not create interval")
});
}

View File

@@ -1,12 +0,0 @@
use leptos::*;
use timer::TimerDemo;
pub fn main() {
_ = console_log::init_with_level(log::Level::Debug);
console_error_panic_hook::set_once();
mount_to_body(|cx| {
view! { cx,
<TimerDemo />
}
})
}

View File

@@ -13,9 +13,7 @@ impl TodoAppError {
pub fn status_code(&self) -> StatusCode {
match self {
TodoAppError::NotFound => StatusCode::NOT_FOUND,
TodoAppError::InternalServerError => {
StatusCode::INTERNAL_SERVER_ERROR
}
TodoAppError::InternalServerError => StatusCode::INTERNAL_SERVER_ERROR,
}
}
}

View File

@@ -52,8 +52,7 @@ pub async fn get_todos(cx: Scope) -> Result<Vec<Todo>, ServerFnError> {
let mut conn = db().await?;
let mut todos = Vec::new();
let mut rows =
sqlx::query_as::<_, Todo>("SELECT * FROM todos").fetch(&mut conn);
let mut rows = sqlx::query_as::<_, Todo>("SELECT * FROM todos").fetch(&mut conn);
while let Some(row) = rows
.try_next()
.await
@@ -110,25 +109,19 @@ pub async fn delete_todo(id: u16) -> Result<(), ServerFnError> {
#[derive(Serialize, Deserialize, Debug, Clone, Default)]
pub struct FormData {
hi: String,
hi: String
}
#[server(FormDataHandler, "/api")]
pub async fn form_data(cx: Scope) -> Result<FormData, ServerFnError> {
use axum::extract::FromRequest;
let req = use_context::<leptos_axum::LeptosRequest<axum::body::Body>>(cx)
.and_then(|req| req.take_request())
.unwrap();
let req = use_context::<leptos_axum::LeptosRequest<axum::body::Body>>(cx).and_then(|req| req.take_request()).unwrap();
if req.method() == http::Method::POST {
let form = axum::Form::from_request(req, &())
.await
.map_err(|e| ServerFnError::ServerError(e.to_string()))?;
let form = axum::Form::from_request(req, &()).await.map_err(|e| ServerFnError::ServerError(e.to_string()))?;
Ok(form.0)
} else {
Err(ServerFnError::ServerError(
"wrong form fields submitted".to_string(),
))
Err(ServerFnError::ServerError("wrong form fields submitted".to_string()))
}
}
@@ -153,7 +146,7 @@ pub fn TodoApp(cx: Scope) -> impl IntoView {
</ErrorBoundary>
}/> //Route
<Route path="weird" methods=&[Method::Get, Method::Post]
ssr=SsrMode::Async
ssr=SsrMode::Async
view=|cx| {
let res = create_resource(cx, || (), move |_| async move {
form_data(cx).await

View File

@@ -202,19 +202,6 @@ pub fn TodoMVC(cx: Scope) -> impl IntoView {
}
});
// focus the main input on load
create_effect(cx, move |_| {
if let Some(input) = input_ref.get() {
// We use request_animation_frame here because the NodeRef
// is filled when the element is created, but before it's mounted
// to the DOM. Calling .focus() before it's mounted does nothing.
// So inside, we wait a tick for the browser to mount it, then .focus()
request_animation_frame(move || {
input.focus();
});
}
});
view! { cx,
<main>
<section class="todoapp">

View File

@@ -13,7 +13,7 @@ use actix_web::{
web::Bytes,
*,
};
use futures::{Stream, StreamExt};
use futures::{Future, Stream, StreamExt};
use http::StatusCode;
use leptos::{
leptos_dom::ssr::render_to_stream_with_prefix_undisposed_with_context,
@@ -26,7 +26,7 @@ use leptos_meta::*;
use leptos_router::*;
use parking_lot::RwLock;
use regex::Regex;
use std::{fmt::Display, future::Future, sync::Arc};
use std::sync::Arc;
use tracing::instrument;
/// This struct lets you define headers and override the status of the Response from an Element or a Server Function
/// Typically contained inside of a ResponseOptions. Setting this is useful for cookies and custom responses.
@@ -905,20 +905,6 @@ async fn render_app_async_helper(
pub fn generate_route_list<IV>(
app_fn: impl FnOnce(leptos::Scope) -> IV + 'static,
) -> Vec<RouteListing>
where
IV: IntoView + 'static,
{
generate_route_list_with_exclusions(app_fn, None)
}
/// Generates a list of all routes defined in Leptos's Router in your app. We can then use this to automatically
/// create routes in Actix's App without having to use wildcard matching or fallbacks. Takes in your root app Element
/// as an argument so it can walk you app tree. This version is tailored to generated Actix compatible paths. Adding excluded_routes
/// to this function will stop `.leptos_routes()` from generating a route for it, allowing a custom handler. These need to be in Actix path format
pub fn generate_route_list_with_exclusions<IV>(
app_fn: impl FnOnce(leptos::Scope) -> IV + 'static,
excluded_routes: Option<Vec<String>>,
) -> Vec<RouteListing>
where
IV: IntoView + 'static,
{
@@ -946,7 +932,7 @@ where
// Match `:some_word` but only capture `some_word` in the groups to replace with `{some_word}`
let capture_re = Regex::new(r":((?:[^.,/]+)+)[^/]?").unwrap();
let mut routes = routes
let routes = routes
.into_iter()
.map(|listing| {
let path = wildcard_re
@@ -960,10 +946,6 @@ where
if routes.is_empty() {
vec![RouteListing::new("/", Default::default(), [Method::Get])]
} else {
// Routes to exclude from auto generation
if let Some(excluded_routes) = excluded_routes {
routes.retain(|p| !excluded_routes.iter().any(|e| e == p.path()))
}
routes
}
}
@@ -1112,94 +1094,3 @@ where
router
}
}
/// A helper to make it easier to use Axum extractors in server functions. This takes
/// a handler function as its argument. The handler follows similar rules to an Actix
/// [Handler](actix_web::Handler): it is an async function that receives arguments that
/// will be extracted from the request and returns some value.
///
/// ```rust,ignore
/// use leptos::*;
/// use serde::Deserialize;
/// #[derive(Deserialize)]
/// struct Search {
/// q: String,
/// }
///
/// #[server(ExtractoServerFn, "/api")]
/// pub async fn extractor_server_fn(cx: Scope) -> Result<String, ServerFnError> {
/// use actix_web::dev::ConnectionInfo;
/// use actix_web::web::{Data, Query};
///
/// extract(
/// cx,
/// |data: Data<String>, search: Query<Search>, connection: ConnectionInfo| async move {
/// format!(
/// "data = {}\nsearch = {}\nconnection = {:?}",
/// data.into_inner(),
/// search.q,
/// connection
/// )
/// },
/// )
/// .await
/// }
/// ```
pub async fn extract<F, E>(
cx: leptos::Scope,
f: F,
) -> Result<<<F as Extractor<E>>::Future as Future>::Output, ServerFnError>
where
F: Extractor<E>,
E: actix_web::FromRequest,
<E as actix_web::FromRequest>::Error: Display,
<F as Extractor<E>>::Future: Future,
{
let req = use_context::<actix_web::HttpRequest>(cx)
.expect("HttpRequest should have been provided via context");
let input = E::extract(&req)
.await
.map_err(|e| ServerFnError::ServerError(e.to_string()))?;
Ok(f.call(input).await)
}
// Drawn from the Actix Handler implementation
// https://github.com/actix/actix-web/blob/19c9d858f25e8262e14546f430d713addb397e96/actix-web/src/handler.rs#L124
pub trait Extractor<T> {
type Future;
fn call(&self, args: T) -> Self::Future;
}
macro_rules! factory_tuple ({ $($param:ident)* } => {
impl<Func, Fut, $($param,)*> Extractor<($($param,)*)> for Func
where
Func: Fn($($param),*) -> Fut + Clone + 'static,
Fut: Future,
{
type Future = Fut;
#[inline]
#[allow(non_snake_case)]
fn call(&self, ($($param,)*): ($($param,)*)) -> Self::Future {
(self)($($param,)*)
}
}
});
factory_tuple! {}
factory_tuple! { A }
factory_tuple! { A B }
factory_tuple! { A B C }
factory_tuple! { A B C D }
factory_tuple! { A B C D E }
factory_tuple! { A B C D E F }
factory_tuple! { A B C D E F G }
factory_tuple! { A B C D E F G H }
factory_tuple! { A B C D E F G H I }
factory_tuple! { A B C D E F G H I J }
factory_tuple! { A B C D E F G H I J K }
factory_tuple! { A B C D E F G H I J K L }
factory_tuple! { A B C D E F G H I J K L M }
factory_tuple! { A B C D E F G H I J K L M N }
factory_tuple! { A B C D E F G H I J K L M N O }
factory_tuple! { A B C D E F G H I J K L M N O P }

View File

@@ -19,10 +19,7 @@ use futures::{
channel::mpsc::{Receiver, Sender},
Future, SinkExt, Stream, StreamExt,
};
use http::{
header, method::Method, request::Parts, uri::Uri, version::Version,
Response,
};
use http::{header, method::Method, uri::Uri, version::Version, Response};
use hyper::body;
use leptos::{
leptos_server::{server_fn_by_path, Payload},
@@ -49,21 +46,6 @@ pub struct RequestParts {
pub headers: HeaderMap<HeaderValue>,
pub body: Bytes,
}
/// Convert http::Parts to RequestParts(and vice versa). Body and Extensions will
/// be lost in the conversion
impl From<Parts> for RequestParts {
fn from(parts: Parts) -> Self {
Self {
version: parts.version,
method: parts.method,
uri: parts.uri,
headers: parts.headers,
body: Bytes::default(),
}
}
}
/// This struct lets you define headers and override the status of the Response from an Element or a Server Function
/// Typically contained inside of a ResponseOptions. Setting this is useful for cookies and custom responses.
#[derive(Debug, Clone, Default)]
@@ -1021,21 +1003,6 @@ where
pub async fn generate_route_list<IV>(
app_fn: impl FnOnce(Scope) -> IV + 'static,
) -> Vec<RouteListing>
where
IV: IntoView + 'static,
{
generate_route_list_with_exclusions(app_fn, None).await
}
/// Generates a list of all routes defined in Leptos's Router in your app. We can then use this to automatically
/// create routes in Axum's Router without having to use wildcard matching or fallbacks. Takes in your root app Element
/// as an argument so it can walk you app tree. This version is tailored to generate Axum compatible paths. Adding excluded_routes
/// to this function will stop `.leptos_routes()` from generating a route for it, allowing a custom handler. These need to be in Axum path format
#[tracing::instrument(level = "trace", fields(error), skip_all)]
pub async fn generate_route_list_with_exclusions<IV>(
app_fn: impl FnOnce(Scope) -> IV + 'static,
excluded_routes: Option<Vec<String>>,
) -> Vec<RouteListing>
where
IV: IntoView + 'static,
{
@@ -1062,7 +1029,7 @@ where
let routes = routes.0.read().to_owned();
// Axum's Router defines Root routes as "/" not ""
let mut routes = routes
let routes = routes
.into_iter()
.map(|listing| {
let path = listing.path();
@@ -1085,10 +1052,6 @@ where
[leptos_router::Method::Get],
)]
} else {
// Routes to exclude from auto generation
if let Some(excluded_routes) = excluded_routes {
routes.retain(|p| !excluded_routes.iter().any(|e| e == p.path()))
}
routes
}
}

View File

@@ -51,10 +51,10 @@ pub fn html_parts(
let output_name = &options.output_name;
// Because wasm-pack adds _bg to the end of the WASM filename, and we want to mantain compatibility with it's default options
// we add _bg to the wasm files if cargo-leptos doesn't set the env var LEPTOS_OUTPUT_NAME at compile time
// Otherwise we need to add _bg because wasm_pack always does.
// we add _bg to the wasm files if cargo-leptos doesn't set the env var LEPTOS_OUTPUT_NAME
// Otherwise we need to add _bg because wasm_pack always does. This is not the same as options.output_name, which is set regardless
let mut wasm_output_name = output_name.clone();
if std::option_env!("LEPTOS_OUTPUT_NAME").is_none() {
if std::env::var("LEPTOS_OUTPUT_NAME").is_err() {
wasm_output_name.push_str("_bg");
}

View File

@@ -947,19 +947,6 @@ where
pub async fn generate_route_list<IV>(
app_fn: impl FnOnce(Scope) -> IV + 'static,
) -> Vec<RouteListing>
where
IV: IntoView + 'static,
{
generate_route_list_with_exclusions(app_fn, None).await
}
/// Generates a list of all routes defined in Leptos's Router in your app. We can then use this to automatically
/// create routes in Viz's Router without having to use wildcard matching or fallbacks. Takes in your root app Element
/// as an argument so it can walk you app tree. This version is tailored to generate Viz compatible paths.
pub async fn generate_route_list_with_exclusions<IV>(
app_fn: impl FnOnce(Scope) -> IV + 'static,
excluded_routes: Option<Vec<String>>,
) -> Vec<RouteListing>
where
IV: IntoView + 'static,
{
@@ -986,7 +973,7 @@ where
let routes = routes.0.read().to_owned();
// Viz's Router defines Root routes as "/" not ""
let mut routes = routes
let routes = routes
.into_iter()
.map(|listing| {
let path = listing.path();
@@ -1009,9 +996,6 @@ where
[leptos_router::Method::Get],
)]
} else {
if let Some(excluded_routes) = excluded_routes {
routes.retain(|p| !excluded_routes.iter().any(|e| e == p.path()))
}
routes
}
}

View File

@@ -82,17 +82,8 @@ where
move || {
cfg_if! {
if #[cfg(any(feature = "csr", feature = "hydrate"))] {
let mut child: Option<crate::View> = None;
if context.ready() {
Fragment::lazy(Box::new(|| vec![{
if let Some(child) = &child {
child.clone()
} else {
let first_run_child = orig_child(cx).into_view(cx);
child = Some(first_run_child.clone());
first_run_child
}
}])).into_view(cx)
Fragment::lazy(Box::new(|| vec![orig_child(cx).into_view(cx)])).into_view(cx)
} else {
Fragment::lazy(Box::new(|| vec![fallback().into_view(cx)])).into_view(cx)
}

View File

@@ -53,23 +53,12 @@ pub struct LeptosOptions {
impl LeptosOptions {
fn try_from_env() -> Result<Self, LeptosConfigError> {
let output_name = env_w_default(
"LEPTOS_OUTPUT_NAME",
std::option_env!("LEPTOS_OUTPUT_NAME",).unwrap_or_default(),
)?;
if output_name.is_empty() {
eprintln!(
"It looks like you're trying to compile Leptos without the \
LEPTOS_OUTPUT_NAME environment variable being set. There are \
two options\n 1. cargo-leptos is not being used, but \
get_configuration() is being passed None. This needs to be \
changed to Some(\"Cargo.toml\")\n 2. You are compiling \
Leptos without LEPTOS_OUTPUT_NAME being set with \
cargo-leptos. This shouldn't be possible!"
);
}
Ok(LeptosOptions {
output_name,
output_name: std::env::var("LEPTOS_OUTPUT_NAME").map_err(|e| {
LeptosConfigError::EnvVarError(format!(
"LEPTOS_OUTPUT_NAME: {e}"
))
})?,
site_root: env_w_default("LEPTOS_SITE_ROOT", "target/site")?,
site_pkg_dir: env_w_default("LEPTOS_SITE_PKG_DIR", "pkg")?,
env: Env::default(),

View File

@@ -31,6 +31,9 @@ fn env_w_default_test() {
#[test]
fn try_from_env_test() {
std::env::remove_var("LEPTOS_OUTPUT_NAME");
assert!(LeptosOptions::try_from_env().is_err());
// Test config values from environment variables
std::env::set_var("LEPTOS_OUTPUT_NAME", "app_test");
std::env::set_var("LEPTOS_SITE_ROOT", "my_target/site");
@@ -48,4 +51,19 @@ fn try_from_env_test() {
SocketAddr::from_str("0.0.0.0:80").unwrap()
);
assert_eq!(config.reload_port, 8080);
// Test default config values
std::env::remove_var("LEPTOS_SITE_ROOT");
std::env::remove_var("LEPTOS_SITE_PKG_DIR");
std::env::remove_var("LEPTOS_SITE_ADDR");
std::env::remove_var("LEPTOS_RELOAD_PORT");
let config = LeptosOptions::try_from_env().unwrap();
assert_eq!(config.site_root, "target/site");
assert_eq!(config.site_pkg_dir, "pkg");
assert_eq!(
config.site_addr,
SocketAddr::from_str("127.0.0.1:3000").unwrap()
);
assert_eq!(config.reload_port, 3001);
}

View File

@@ -140,6 +140,9 @@ fn get_config_from_str_content() {
#[tokio::test]
async fn get_config_from_env() {
std::env::remove_var("LEPTOS_OUTPUT_NAME");
assert!(get_configuration(None).await.is_err());
// Test config values from environment variables
std::env::set_var("LEPTOS_OUTPUT_NAME", "app_test");
std::env::set_var("LEPTOS_SITE_ROOT", "my_target/site");

View File

@@ -140,23 +140,18 @@ impl Mountable for ComponentRepr {
self.closing.node.clone()
}
}
impl From<ComponentRepr> for View {
fn from(value: ComponentRepr) -> Self {
#[cfg(all(target_arch = "wasm32", feature = "web"))]
if !HydrationCtx::is_hydrating() {
for child in &value.children {
mount_child(MountKind::Before(&value.closing.node), child);
}
}
View::Component(value)
}
}
impl IntoView for ComponentRepr {
#[cfg_attr(any(debug_assertions, feature = "ssr"), instrument(level = "info", name = "<Component />", skip_all, fields(name = %self.name)))]
fn into_view(self, _: Scope) -> View {
self.into()
#[cfg(all(target_arch = "wasm32", feature = "web"))]
if !HydrationCtx::is_hydrating() {
for child in &self.children {
mount_child(MountKind::Before(&self.closing.node), child);
}
}
View::Component(self)
}
}

View File

@@ -17,11 +17,11 @@ cfg_if! {
#[derive(Clone, PartialEq, Eq)]
pub struct DynChildRepr {
#[cfg(all(target_arch = "wasm32", feature = "web"))]
pub(crate) document_fragment: web_sys::DocumentFragment,
document_fragment: web_sys::DocumentFragment,
#[cfg(debug_assertions)]
opening: Comment,
pub(crate) child: Rc<RefCell<Box<Option<View>>>>,
pub(crate) closing: Comment,
closing: Comment,
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
pub(crate) id: HydrationKey,
}
@@ -278,33 +278,7 @@ where
let start = child.get_opening_node();
let end = &closing;
match child {
View::CoreComponent(
crate::CoreComponent::DynChild(
child,
),
) => {
let start =
child.get_opening_node();
let end = child.closing.node;
prepare_to_move(
&child.document_fragment,
&start,
&end,
);
}
View::Component(child) => {
let start =
child.get_opening_node();
let end = child.closing.node;
prepare_to_move(
&child.document_fragment,
&start,
&end,
);
}
_ => unmount_child(&start, end),
}
unmount_child(&start, end);
}
// Mount the new child

View File

@@ -45,21 +45,6 @@ impl From<View> for Fragment {
}
}
impl From<Fragment> for View {
fn from(value: Fragment) -> Self {
let mut frag = ComponentRepr::new_with_id("", value.id.clone());
#[cfg(debug_assertions)]
{
frag.view_marker = value.view_marker;
}
frag.children = value.nodes;
frag.into()
}
}
impl Fragment {
/// Creates a new [`Fragment`] from a [`Vec<Node>`].
#[inline(always)]
@@ -106,7 +91,16 @@ impl Fragment {
impl IntoView for Fragment {
#[cfg_attr(debug_assertions, instrument(level = "info", name = "</>", skip_all, fields(children = self.nodes.len())))]
fn into_view(self, _: leptos_reactive::Scope) -> View {
self.into()
fn into_view(self, cx: leptos_reactive::Scope) -> View {
let mut frag = ComponentRepr::new_with_id("", self.id.clone());
#[cfg(debug_assertions)]
{
frag.view_marker = self.view_marker;
}
frag.children = self.nodes;
frag.into_view(cx)
}
}

View File

@@ -533,12 +533,6 @@ impl IntoView for &Fragment {
}
}
impl FromIterator<View> for View {
fn from_iter<T: IntoIterator<Item = View>>(iter: T) -> Self {
iter.into_iter().collect::<Fragment>().into()
}
}
#[cfg(all(target_arch = "wasm32", feature = "web"))]
impl Mountable for View {
fn get_mountable_node(&self) -> web_sys::Node {

View File

@@ -208,12 +208,6 @@ impl ToTokens for Model {
}
}
impl #generics ::leptos::IntoView for #props_name #generics #where_clause {
fn into_view(self, cx: ::leptos::Scope) -> ::leptos::View {
#name(cx, self).into_view(cx)
}
}
#docs
#component_fn_prop_docs
#[allow(non_snake_case, clippy::too_many_arguments)]
@@ -464,18 +458,10 @@ fn prop_builder_fields(vis: &Visibility, props: &[Prop]) -> TokenStream {
let builder_docs = prop_to_doc(prop, PropDocStyle::Inline);
// Children won't need documentation in many cases
let allow_missing_docs = if name.ident == "children" {
quote!(#[allow(missing_docs)])
} else {
quote!()
};
quote! {
#docs
#builder_docs
#builder_attrs
#allow_missing_docs
#vis #name: #ty,
}
})
@@ -620,11 +606,12 @@ fn prop_to_doc(
PropDocStyle::List => {
let arg_ty_doc = LitStr::new(
&if !prop_opts.into {
format!("- **{}**: [`{pretty_ty}`]", quote!(#name))
format!("- **{}**: [`{}`]", quote!(#name), pretty_ty)
} else {
format!(
"- **{}**: [`impl Into<{pretty_ty}>`]({pretty_ty})",
"- **{}**: `impl`[`Into<{}>`]",
quote!(#name),
pretty_ty
)
},
name.ident.span(),

View File

@@ -628,7 +628,9 @@ pub fn component(args: proc_macro::TokenStream, s: TokenStream) -> TokenStream {
let is_transparent = if !args.is_empty() {
let transparent = parse_macro_input!(args as syn::Ident);
if transparent != "transparent" {
let transparent_token: syn::Ident = syn::parse_quote!(transparent);
if transparent != transparent_token {
abort!(
transparent,
"only `transparent` is supported";

View File

@@ -81,8 +81,8 @@ impl ToTokens for Model {
#prop_builder_fields
}
impl #generics From<#name #generics> for Vec<#name #generics> #where_clause {
fn from(value: #name #generics) -> Self {
impl From<#name> for Vec<#name> {
fn from(value: #name) -> Self {
vec![value]
}
}

View File

@@ -4,23 +4,16 @@ use leptos::*;
fn missing_scope() {}
#[component]
fn missing_return_type(cx: Scope) {
_ = cx;
}
fn missing_return_type(cx: Scope) {}
#[component]
fn unknown_prop_option(cx: Scope, #[prop(hello)] test: bool) -> impl IntoView {
_ = cx;
_ = test;
}
fn unknown_prop_option(cx: Scope, #[prop(hello)] test: bool) -> impl IntoView {}
#[component]
fn optional_and_optional_no_strip(
cx: Scope,
#[prop(optional, optional_no_strip)] conflicting: bool,
) -> impl IntoView {
_ = cx;
_ = conflicting;
}
#[component]
@@ -28,8 +21,6 @@ fn optional_and_strip_option(
cx: Scope,
#[prop(optional, strip_option)] conflicting: bool,
) -> impl IntoView {
_ = cx;
_ = conflicting;
}
#[component]
@@ -37,8 +28,6 @@ fn optional_no_strip_and_strip_option(
cx: Scope,
#[prop(optional_no_strip, strip_option)] conflicting: bool,
) -> impl IntoView {
_ = cx;
_ = conflicting;
}
#[component]
@@ -46,8 +35,6 @@ fn default_without_value(
cx: Scope,
#[prop(default)] default: bool,
) -> impl IntoView {
_ = cx;
_ = default;
}
#[component]
@@ -55,8 +42,6 @@ fn default_with_invalid_value(
cx: Scope,
#[prop(default= |)] default: bool,
) -> impl IntoView {
_ = cx;
_ = default;
}
fn main() {}

View File

@@ -9,45 +9,45 @@ error: this method requires a `Scope` parameter
error: return type is incorrect
--> tests/ui/component.rs:7:1
|
7 | fn missing_return_type(cx: Scope) {
7 | fn missing_return_type(cx: Scope) {}
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
= help: return signature must be `-> impl IntoView`
error: supported fields are `optional`, `optional_no_strip`, `strip_option`, `default` and `into`
--> tests/ui/component.rs:12:42
--> tests/ui/component.rs:10:42
|
12 | fn unknown_prop_option(cx: Scope, #[prop(hello)] test: bool) -> impl IntoView {
10 | fn unknown_prop_option(cx: Scope, #[prop(hello)] test: bool) -> impl IntoView {}
| ^^^^^
error: `optional` conflicts with mutually exclusive `optional_no_strip`
--> tests/ui/component.rs:20:12
--> tests/ui/component.rs:15:12
|
20 | #[prop(optional, optional_no_strip)] conflicting: bool,
15 | #[prop(optional, optional_no_strip)] conflicting: bool,
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^
error: `optional` conflicts with mutually exclusive `strip_option`
--> tests/ui/component.rs:29:12
--> tests/ui/component.rs:22:12
|
29 | #[prop(optional, strip_option)] conflicting: bool,
22 | #[prop(optional, strip_option)] conflicting: bool,
| ^^^^^^^^^^^^^^^^^^^^^^
error: `optional_no_strip` conflicts with mutually exclusive `strip_option`
--> tests/ui/component.rs:38:12
--> tests/ui/component.rs:29:12
|
38 | #[prop(optional_no_strip, strip_option)] conflicting: bool,
29 | #[prop(optional_no_strip, strip_option)] conflicting: bool,
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
error: unexpected end of input, expected assignment `=`
--> tests/ui/component.rs:47:19
--> tests/ui/component.rs:36:19
|
47 | #[prop(default)] default: bool,
36 | #[prop(default)] default: bool,
| ^
error: unexpected end of input, expected one of: `::`, `<`, `_`, literal, `const`, `ref`, `mut`, `&`, parentheses, square brackets, `..`, `const`
= help: try `#[prop(default=5 * 10)]`
--> tests/ui/component.rs:56:22
--> tests/ui/component.rs:43:22
|
56 | #[prop(default= |)] default: bool,
43 | #[prop(default= |)] default: bool,
| ^

View File

@@ -2,23 +2,16 @@
fn missing_scope() {}
#[::leptos::component]
fn missing_return_type(cx: ::leptos::Scope) {
_ = cx;
}
fn missing_return_type(cx: ::leptos::Scope) {}
#[::leptos::component]
fn unknown_prop_option(cx: ::leptos::Scope, #[prop(hello)] test: bool) -> impl ::leptos::IntoView {
_ = cx;
_ = test;
}
fn unknown_prop_option(cx: ::leptos::Scope, #[prop(hello)] test: bool) -> impl ::leptos::IntoView {}
#[::leptos::component]
fn optional_and_optional_no_strip(
cx: Scope,
#[prop(optional, optional_no_strip)] conflicting: bool,
) -> impl IntoView {
_ = cx;
_ = conflicting;
}
#[::leptos::component]
@@ -26,8 +19,6 @@ fn optional_and_strip_option(
cx: ::leptos::Scope,
#[prop(optional, strip_option)] conflicting: bool,
) -> impl ::leptos::IntoView {
_ = cx;
_ = conflicting;
}
#[::leptos::component]
@@ -35,8 +26,6 @@ fn optional_no_strip_and_strip_option(
cx: ::leptos::Scope,
#[prop(optional_no_strip, strip_option)] conflicting: bool,
) -> impl ::leptos::IntoView {
_ = cx;
_ = conflicting;
}
#[::leptos::component]
@@ -44,8 +33,6 @@ fn default_without_value(
cx: ::leptos::Scope,
#[prop(default)] default: bool,
) -> impl ::leptos::IntoView {
_ = cx;
_ = default;
}
#[::leptos::component]
@@ -53,13 +40,10 @@ fn default_with_invalid_value(
cx: ::leptos::Scope,
#[prop(default= |)] default: bool,
) -> impl ::leptos::IntoView {
_ = cx;
_ = default;
}
#[::leptos::component]
pub fn using_the_view_macro(cx: ::leptos::Scope) -> impl ::leptos::IntoView {
_ = cx;
::leptos::view! { cx,
"ok"
}

View File

@@ -9,45 +9,45 @@ error: this method requires a `Scope` parameter
error: return type is incorrect
--> tests/ui/component_absolute.rs:5:1
|
5 | fn missing_return_type(cx: ::leptos::Scope) {
5 | fn missing_return_type(cx: ::leptos::Scope) {}
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
= help: return signature must be `-> impl IntoView`
error: supported fields are `optional`, `optional_no_strip`, `strip_option`, `default` and `into`
--> tests/ui/component_absolute.rs:10:52
|
10 | fn unknown_prop_option(cx: ::leptos::Scope, #[prop(hello)] test: bool) -> impl ::leptos::IntoView {
| ^^^^^
--> tests/ui/component_absolute.rs:8:52
|
8 | fn unknown_prop_option(cx: ::leptos::Scope, #[prop(hello)] test: bool) -> impl ::leptos::IntoView {}
| ^^^^^
error: `optional` conflicts with mutually exclusive `optional_no_strip`
--> tests/ui/component_absolute.rs:18:12
--> tests/ui/component_absolute.rs:13:12
|
18 | #[prop(optional, optional_no_strip)] conflicting: bool,
13 | #[prop(optional, optional_no_strip)] conflicting: bool,
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^
error: `optional` conflicts with mutually exclusive `strip_option`
--> tests/ui/component_absolute.rs:27:12
--> tests/ui/component_absolute.rs:20:12
|
27 | #[prop(optional, strip_option)] conflicting: bool,
20 | #[prop(optional, strip_option)] conflicting: bool,
| ^^^^^^^^^^^^^^^^^^^^^^
error: `optional_no_strip` conflicts with mutually exclusive `strip_option`
--> tests/ui/component_absolute.rs:36:12
--> tests/ui/component_absolute.rs:27:12
|
36 | #[prop(optional_no_strip, strip_option)] conflicting: bool,
27 | #[prop(optional_no_strip, strip_option)] conflicting: bool,
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
error: unexpected end of input, expected assignment `=`
--> tests/ui/component_absolute.rs:45:19
--> tests/ui/component_absolute.rs:34:19
|
45 | #[prop(default)] default: bool,
34 | #[prop(default)] default: bool,
| ^
error: unexpected end of input, expected one of: `::`, `<`, `_`, literal, `const`, `ref`, `mut`, `&`, parentheses, square brackets, `..`, `const`
= help: try `#[prop(default=5 * 10)]`
--> tests/ui/component_absolute.rs:54:22
--> tests/ui/component_absolute.rs:41:22
|
54 | #[prop(default= |)] default: bool,
41 | #[prop(default= |)] default: bool,
| ^

View File

@@ -1,65 +1,64 @@
#![cfg_attr(not(feature = "stable"), feature(proc_macro_span))]
//! This crate contains the default implementation of the #[macro@crate::server] macro without a context from the server. See the [server_fn_macro] crate for more information.
#![forbid(unsafe_code)]
use proc_macro::TokenStream;
use server_fn_macro::server_macro_impl;
use syn::__private::ToTokens;
/// Declares that a function is a [server function](https://docs.rs/server_fn/).
/// This means that its body will only run on the server, i.e., when the `ssr`
/// feature is enabled.
///
/// You can specify one, two, or three arguments to the server function:
/// 1. **Required**: A type name that will be used to identify and register the server function
/// (e.g., `MyServerFn`).
/// 2. *Optional*: A URL prefix at which the function will be mounted when its registered
/// (e.g., `"/api"`). Defaults to `"/"`.
/// 3. *Optional*: either `"Cbor"` (specifying that it should use the binary `cbor` format for
/// serialization), `"Url"` (specifying that it should be use a URL-encoded form-data string).
/// Defaults to `"Url"`. If you want to use this server function to power a `<form>` that will
/// work without WebAssembly, the encoding must be `"Url"`. If you want to use this server function
/// using Get instead of Post methods, the encoding must be `"GetCbor"` or `"GetJson"`.
///
/// The server function itself can take any number of arguments, each of which should be serializable
/// and deserializable with `serde`.
///
/// ```ignore
/// # use server_fn::*; use serde::{Serialize, Deserialize};
/// # #[derive(Serialize, Deserialize)]
/// # pub struct Post { }
/// #[server(ReadPosts, "/api")]
/// pub async fn read_posts(how_many: u8, query: String) -> Result<Vec<Post>, ServerFnError> {
/// // do some work on the server to access the database
/// todo!()
/// }
/// ```
///
/// Note the following:
/// - You must **register** the server function by calling `T::register()` somewhere in your main function.
/// - **Server functions must be `async`.** Even if the work being done inside the function body
/// can run synchronously on the server, from the clients perspective it involves an asynchronous
/// function call.
/// - **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.
/// - **Return types must implement [Serialize](https://docs.rs/serde/latest/serde/trait.Serialize.html).**
/// 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 [`Serialize`](https://docs.rs/serde/latest/serde/trait.Serialize.html)
/// and [`DeserializeOwned`](https://docs.rs/serde/latest/serde/de/trait.DeserializeOwned.html).**
/// They are serialized as an `application/x-www-form-urlencoded`
/// form data using [`serde_urlencoded`](https://docs.rs/serde_urlencoded/latest/serde_urlencoded/) or as `application/cbor`
/// using [`cbor`](https://docs.rs/cbor/latest/cbor/).
#[proc_macro_attribute]
pub fn server(args: proc_macro::TokenStream, s: TokenStream) -> TokenStream {
match server_macro_impl(
args.into(),
s.into(),
None,
Some(syn::parse_quote!(server_fn)),
) {
Err(e) => e.to_compile_error().into(),
Ok(s) => s.to_token_stream().into(),
}
}
#![cfg_attr(not(feature = "stable"), feature(proc_macro_span))]
//! This crate contains the default implementation of the #[macro@crate::server] macro without a context from the server. See the [server_fn_macro] crate for more information.
#![forbid(unsafe_code)]
use proc_macro::TokenStream;
use server_fn_macro::server_macro_impl;
use syn::__private::ToTokens;
/// Declares that a function is a [server function](https://docs.rs/server_fn/).
/// This means that its body will only run on the server, i.e., when the `ssr`
/// feature is enabled.
///
/// You can specify one, two, or three arguments to the server function:
/// 1. **Required**: A type name that will be used to identify and register the server function
/// (e.g., `MyServerFn`).
/// 2. *Optional*: A URL prefix at which the function will be mounted when its registered
/// (e.g., `"/api"`). Defaults to `"/"`.
/// 3. *Optional*: either `"Cbor"` (specifying that it should use the binary `cbor` format for
/// serialization) or `"Url"` (specifying that it should be use a URL-encoded form-data string).
/// Defaults to `"Url"`. If you want to use this server function to power a `<form>` that will
/// work without WebAssembly, the encoding must be `"Url"`.
///
/// The server function itself can take any number of arguments, each of which should be serializable
/// and deserializable with `serde`.
///
/// ```ignore
/// # use server_fn::*; use serde::{Serialize, Deserialize};
/// # #[derive(Serialize, Deserialize)]
/// # pub struct Post { }
/// #[server(ReadPosts, "/api")]
/// pub async fn read_posts(how_many: u8, query: String) -> Result<Vec<Post>, ServerFnError> {
/// // do some work on the server to access the database
/// todo!()
/// }
/// ```
///
/// Note the following:
/// - You must **register** the server function by calling `T::register()` somewhere in your main function.
/// - **Server functions must be `async`.** Even if the work being done inside the function body
/// can run synchronously on the server, from the clients perspective it involves an asynchronous
/// function call.
/// - **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.
/// - **Return types must implement [Serialize](https://docs.rs/serde/latest/serde/trait.Serialize.html).**
/// 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 [`Serialize`](https://docs.rs/serde/latest/serde/trait.Serialize.html)
/// and [`DeserializeOwned`](https://docs.rs/serde/latest/serde/de/trait.DeserializeOwned.html).**
/// They are serialized as an `application/x-www-form-urlencoded`
/// form data using [`serde_urlencoded`](https://docs.rs/serde_urlencoded/latest/serde_urlencoded/) or as `application/cbor`
/// using [`cbor`](https://docs.rs/cbor/latest/cbor/).
#[proc_macro_attribute]
pub fn server(args: proc_macro::TokenStream, s: TokenStream) -> TokenStream {
match server_macro_impl(
args.into(),
s.into(),
None,
Some(syn::parse_quote!(server_fn)),
) {
Err(e) => e.to_compile_error().into(),
Ok(s) => s.to_token_stream().into(),
}
}

View File

@@ -376,7 +376,7 @@ pub enum ServerFnError {
#[error("error deserializing server function results {0}")]
Deserialization(String),
/// Occurs on the client if there is an error serializing the server function arguments.
#[error("error serializing server function arguments {0}")]
#[error("error serializing server function results {0}")]
Serialization(String),
/// Occurs on the server if there is an error deserializing one of the arguments that's been sent.
#[error("error deserializing server function arguments {0}")]