mirror of
https://github.com/leptos-rs/leptos.git
synced 2025-12-27 16:54:41 -05:00
Compare commits
84 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ed86c72598 | ||
|
|
eb87fb6d70 | ||
|
|
0ba713b0d6 | ||
|
|
e9f7af4b72 | ||
|
|
62298dbba2 | ||
|
|
9855bc5165 | ||
|
|
fb7bd2506b | ||
|
|
8d8e9b4129 | ||
|
|
ce86979d70 | ||
|
|
e7cf7cc972 | ||
|
|
4228ebaed3 | ||
|
|
a3142267a9 | ||
|
|
423d0b8e2a | ||
|
|
f934d59821 | ||
|
|
b7483d74bf | ||
|
|
8b5dd99d7f | ||
|
|
22e6a13ca8 | ||
|
|
bc2378e4f7 | ||
|
|
1af8214b1f | ||
|
|
6e52401e22 | ||
|
|
47ac5adae9 | ||
|
|
f5b22f27fc | ||
|
|
7c9ee2dbb4 | ||
|
|
f89ba9bcb3 | ||
|
|
1fb7874579 | ||
|
|
a8ce7d3e6e | ||
|
|
862390ad97 | ||
|
|
4616feea30 | ||
|
|
d8cbda5f6f | ||
|
|
3b6d5a3bdd | ||
|
|
9dc97836b8 | ||
|
|
d87997955d | ||
|
|
8257430d2d | ||
|
|
50bec1c5f8 | ||
|
|
d7755eb5cc | ||
|
|
30f3d67aec | ||
|
|
31cee81a39 | ||
|
|
7a0d67932e | ||
|
|
d5aecdaa43 | ||
|
|
3d2626754c | ||
|
|
2120174c9f | ||
|
|
013f5dbb56 | ||
|
|
b8fdcadc2f | ||
|
|
3124c74060 | ||
|
|
2f81e206e3 | ||
|
|
45dfb96bbe | ||
|
|
ef39eb5e4b | ||
|
|
90f1bd9bfc | ||
|
|
7f505755de | ||
|
|
29db073e22 | ||
|
|
bbb8672927 | ||
|
|
d4bea99f11 | ||
|
|
25832c1388 | ||
|
|
73f8f893e2 | ||
|
|
ed1555f634 | ||
|
|
1cf78a4515 | ||
|
|
ccd88d4932 | ||
|
|
ffa99a0b01 | ||
|
|
2e61b2f241 | ||
|
|
bde80d0d94 | ||
|
|
63cce54cda | ||
|
|
ca84fa6e7a | ||
|
|
badcc9669a | ||
|
|
8317816f64 | ||
|
|
e4eb995d9a | ||
|
|
0ee50ecfd5 | ||
|
|
feaa2adccf | ||
|
|
35f7cba1bc | ||
|
|
a34b13572d | ||
|
|
87cd836934 | ||
|
|
07f5e06850 | ||
|
|
25f9d34d13 | ||
|
|
f02eaaf672 | ||
|
|
82a7dfaadb | ||
|
|
e643cd941c | ||
|
|
44a69bcf0a | ||
|
|
46a64478ba | ||
|
|
1aa3f50dd6 | ||
|
|
5e9fe95992 | ||
|
|
4756d6c434 | ||
|
|
cef2263657 | ||
|
|
c1883d0607 | ||
|
|
ca8ba0ca8d | ||
|
|
a8d98a1fde |
26
Cargo.toml
26
Cargo.toml
@@ -25,22 +25,22 @@ members = [
|
||||
exclude = ["benchmarks", "examples"]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.6.3"
|
||||
version = "0.6.0-beta"
|
||||
|
||||
[workspace.dependencies]
|
||||
leptos = { path = "./leptos", version = "0.6.3" }
|
||||
leptos_dom = { path = "./leptos_dom", version = "0.6.3" }
|
||||
leptos_hot_reload = { path = "./leptos_hot_reload", version = "0.6.3" }
|
||||
leptos_macro = { path = "./leptos_macro", version = "0.6.3" }
|
||||
leptos_reactive = { path = "./leptos_reactive", version = "0.6.3" }
|
||||
leptos_server = { path = "./leptos_server", version = "0.6.3" }
|
||||
server_fn = { path = "./server_fn", version = "0.6.3" }
|
||||
server_fn_macro = { path = "./server_fn_macro", version = "0.6.3" }
|
||||
leptos = { path = "./leptos", version = "0.6.0-beta" }
|
||||
leptos_dom = { path = "./leptos_dom", version = "0.6.0-beta" }
|
||||
leptos_hot_reload = { path = "./leptos_hot_reload", version = "0.6.0-beta" }
|
||||
leptos_macro = { path = "./leptos_macro", version = "0.6.0-beta" }
|
||||
leptos_reactive = { path = "./leptos_reactive", version = "0.6.0-beta" }
|
||||
leptos_server = { path = "./leptos_server", version = "0.6.0-beta" }
|
||||
server_fn = { path = "./server_fn", version = "0.6.0-beta" }
|
||||
server_fn_macro = { path = "./server_fn_macro", version = "0.6.0-beta" }
|
||||
server_fn_macro_default = { path = "./server_fn/server_fn_macro_default", version = "0.6" }
|
||||
leptos_config = { path = "./leptos_config", version = "0.6.3" }
|
||||
leptos_router = { path = "./router", version = "0.6.3" }
|
||||
leptos_meta = { path = "./meta", version = "0.6.3" }
|
||||
leptos_integration_utils = { path = "./integrations/utils", version = "0.6.3" }
|
||||
leptos_config = { path = "./leptos_config", version = "0.6.0-beta" }
|
||||
leptos_router = { path = "./router", version = "0.6.0-beta" }
|
||||
leptos_meta = { path = "./meta", version = "0.6.0-beta" }
|
||||
leptos_integration_utils = { path = "./integrations/utils", version = "0.6.0-beta" }
|
||||
|
||||
[profile.release]
|
||||
codegen-units = 1
|
||||
|
||||
57
README.md
57
README.md
@@ -40,25 +40,6 @@ pub fn SimpleCounter(initial_value: i32) -> impl IntoView {
|
||||
}
|
||||
}
|
||||
|
||||
// we also support a builder syntax rather than the JSX-like `view` macro
|
||||
#[component]
|
||||
pub fn SimpleCounterWithBuilder(initial_value: i32) -> impl IntoView {
|
||||
use leptos::html::*;
|
||||
|
||||
let (value, set_value) = create_signal(initial_value);
|
||||
let clear = move |_| set_value(0);
|
||||
let decrement = move |_| set_value.update(|value| *value -= 1);
|
||||
let increment = move |_| set_value.update(|value| *value += 1);
|
||||
|
||||
// the `view` macro above expands to this builder syntax
|
||||
div().child((
|
||||
button().on(ev::click, clear).child("Clear"),
|
||||
button().on(ev::click, decrement).child("-1"),
|
||||
span().child(("Value: ", value, "!")),
|
||||
button().on(ev::click, increment).child("+1")
|
||||
))
|
||||
}
|
||||
|
||||
// Easy to use with Trunk (trunkrs.dev) or with a simple wasm-bindgen setup
|
||||
pub fn main() {
|
||||
mount_to_body(|| view! {
|
||||
@@ -159,27 +140,35 @@ Sure! Obviously the `view` macro is for generating DOM nodes but you can use the
|
||||
|
||||
I've put together a [very simple GTK example](https://github.com/leptos-rs/leptos/blob/main/examples/gtk/src/main.rs) so you can see what I mean.
|
||||
|
||||
The new rendering approach being developed for 0.7 supports “universal rendering,” i.e., it can use any rendering library that supports a small set of 6-8 functions. (This is intended as a layer over typical retained-mode, OOP-style GUI toolkits like the DOM, GTK, etc.) That future rendering work will allow creating native UI in a way that is much more similar to the declarative approach used by the web framework.
|
||||
### How is this different from Yew/Dioxus?
|
||||
|
||||
### How is this different from Yew?
|
||||
|
||||
Yew is the most-used library for Rust web UI development, but there are several differences between Yew and Leptos, in philosophy, approach, and performance.
|
||||
On the surface level, these libraries may seem similar. Yew is, of course, the most mature Rust library for web UI development and has a huge ecosystem. Dioxus is similar in many ways, being heavily inspired by React. Here are some conceptual differences between Leptos and these frameworks:
|
||||
|
||||
- **VDOM vs. fine-grained:** Yew is built on the virtual DOM (VDOM) model: state changes cause components to re-render, generating a new virtual DOM tree. Yew diffs this against the previous VDOM, and applies those patches to the actual DOM. Component functions rerun whenever state changes. Leptos takes an entirely different approach. Components run once, creating (and returning) actual DOM nodes and setting up a reactive system to update those DOM nodes.
|
||||
- **Performance:** This has huge performance implications: Leptos is simply much faster at both creating and updating the UI than Yew is.
|
||||
- **Server integration:** Yew was created in an era in which browser-rendered single-page apps (SPAs) were the dominant paradigm. While Leptos supports client-side rendering, it also focuses on integrating with the server side of your application via server functions and multiple modes of serving HTML, including out-of-order streaming.
|
||||
- **Performance:** This has huge performance implications: Leptos is simply much faster at both creating and updating the UI than Yew is. (Dioxus has made huge advances in performance with its recent 0.3 release, and is now roughly on par with Leptos.)
|
||||
- **Mental model:** Adopting fine-grained reactivity also tends to simplify the mental model. There are no surprising component re-renders because there are no re-renders. You can call functions, create timeouts, etc. within the body of your component functions because they won’t be re-run. You don’t need to think about manual dependency tracking for effects; fine-grained reactivity tracks dependencies automatically.
|
||||
|
||||
- ### How is this different from Dioxus?
|
||||
### How is this different from Sycamore?
|
||||
|
||||
Like Leptos, Dioxus is a framework for building UIs using web technologies. However, there are significant differences in approach and features.
|
||||
Conceptually, these two frameworks are very similar: because both are built on fine-grained reactivity, most apps will end up looking very similar between the two, and Sycamore or Leptos apps will both look a lot like SolidJS apps, in the same way that Yew or Dioxus can look a lot like React.
|
||||
|
||||
- **VDOM vs. fine-grained:** While Dioxus has a performant virtual DOM (VDOM), it still uses coarse-grained/component-scoped reactivity: changing a stateful value reruns the component function and diffs the old UI against the new one. Leptos components use a different mental model, creating (and returning) actual DOM nodes and setting up a reactive system to update those DOM nodes.
|
||||
- **Web vs. desktop priorities:** Dioxus uses Leptos server functions in its fullstack mode, but does not have the same `<Suspense>`-based support for things like streaming HTML rendering, or share the same focus on holistic web performance. Leptos tends to prioritize holistic web performance (streaming HTML rendering, smaller WASM binary sizes, etc.), whereas Dioxus has an unparalleled experience when building desktop apps, because your application logic runs as a native Rust binary.
|
||||
There are some practical differences that make a significant difference:
|
||||
|
||||
- ### How is this different from Sycamore?
|
||||
- **Templating:** Leptos uses a JSX-like template format (built on [syn-rsx](https://github.com/stoically/syn-rsx)) for its `view` macro. Sycamore offers the choice of its own templating DSL or a builder syntax.
|
||||
- **Server integration:** Leptos provides primitives that encourage HTML streaming and allow for easy async integration and RPC calls, even without WASM enabled, making it easy to opt into integrations between your frontend and backend code without pushing you toward any particular metaframework patterns.
|
||||
- **Read-write segregation:** Leptos, like Solid, encourages read-write segregation between signal getters and setters, so you end up accessing signals with tuples like `let (count, set_count) = create_signal(0);` _(If you prefer or if it's more convenient for your API, you can use [`create_rw_signal`](https://docs.rs/leptos/latest/leptos/fn.create_rw_signal.html) to give a unified read/write signal.)_
|
||||
- **Signals are functions:** In Leptos, you can call a signal to access it rather than calling a specific method (so, `count()` instead of `count.get()`) This creates a more consistent mental model: accessing a reactive value is always a matter of calling a function. For example:
|
||||
|
||||
Sycamore and Leptos are both heavily influenced by SolidJS. At this point, Leptos has a larger community and ecosystem and is more actively developed. Other differences:
|
||||
```rust
|
||||
let (count, set_count) = create_signal(0); // a signal
|
||||
let double_count = move || count() * 2; // a derived signal
|
||||
let memoized_count = create_memo(move |_| count() * 3); // a memo
|
||||
// all are accessed by calling them
|
||||
assert_eq!(count(), 0);
|
||||
assert_eq!(double_count(), 0);
|
||||
assert_eq!(memoized_count(), 0);
|
||||
// this function can accept any of those signals
|
||||
fn do_work_on_signal(my_signal: impl Fn() -> i32) { ... }
|
||||
```
|
||||
|
||||
- **Templating DSLs:** Sycamore uses a custom templating language for its views, while Leptos uses a JSX-like template format.
|
||||
- **`'static` signals:** One of Leptos’s main innovations was the creation of `Copy + 'static` signals, which have excellent ergonomics. Sycamore is in the process of adopting the same pattern, but this is not yet released.
|
||||
- **Perseus vs. server functions:** The Perseus metaframework provides an opinionated way to build Sycamore apps that include server functionality. Leptos instead provides primitives like server functions in the core of the framework.
|
||||
- **Signals and scopes are `'static`:** Both Leptos and Sycamore ease the pain of moving signals in closures (in particular, event listeners) by making them `Copy`, to avoid the `{ let count = count.clone(); move |_| ... }` that's very familiar in Rust UI code. Sycamore does this by using bump allocation to tie the lifetimes of its signals to its scopes: since references are `Copy`, `&'a Signal<T>` can be moved into a closure. Leptos does this by using arena allocation and passing around indices: types like `ReadSignal<T>`, `WriteSignal<T>`, and `Memo<T>` are actually wrappers for indices into an arena. This means that both scopes and signals are both `Copy` and `'static` in Leptos, which means that they can be moved easily into closures without adding lifetime complexity.
|
||||
|
||||
@@ -28,9 +28,7 @@ pub fn App() -> impl IntoView {
|
||||
}
|
||||
|
||||
#[server]
|
||||
async fn do_something(
|
||||
should_error: Option<String>,
|
||||
) -> Result<String, ServerFnError> {
|
||||
async fn do_something(should_error: Option<String>) -> Result<String, ServerFnError> {
|
||||
if should_error.is_none() {
|
||||
Ok(String::from("Successful submit"))
|
||||
} else {
|
||||
@@ -44,12 +42,7 @@ async fn do_something(
|
||||
#[component]
|
||||
fn HomePage() -> impl IntoView {
|
||||
let do_something_action = Action::<DoSomething, _>::server();
|
||||
let value = Signal::derive(move || {
|
||||
do_something_action
|
||||
.value()
|
||||
.get()
|
||||
.unwrap_or_else(|| Ok(String::new()))
|
||||
});
|
||||
let value = Signal::derive(move || do_something_action.value().get().unwrap_or_else(|| Ok(String::new())));
|
||||
|
||||
Effect::new_isomorphic(move |_| {
|
||||
logging::log!("Got value = {:?}", value.get());
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
#[cfg(feature = "ssr")]
|
||||
#[actix_web::main]
|
||||
async fn main() -> std::io::Result<()> {
|
||||
use action_form_error_handling::app::*;
|
||||
use actix_files::Files;
|
||||
use actix_web::*;
|
||||
use leptos::*;
|
||||
use leptos_actix::{generate_route_list, LeptosRoutes};
|
||||
use action_form_error_handling::app::*;
|
||||
|
||||
let conf = get_configuration(None).await.unwrap();
|
||||
let addr = conf.leptos_options.site_addr;
|
||||
@@ -43,8 +43,8 @@ pub fn main() {
|
||||
// a client-side main function is required for using `trunk serve`
|
||||
// prefer using `cargo leptos serve` instead
|
||||
// to run: `trunk serve --open --features csr`
|
||||
use action_form_error_handling::app::*;
|
||||
use leptos::*;
|
||||
use action_form_error_handling::app::*;
|
||||
use wasm_bindgen::prelude::wasm_bindgen;
|
||||
|
||||
console_error_panic_hook::set_once();
|
||||
|
||||
@@ -5,7 +5,7 @@ use leptos_router::*;
|
||||
use tracing::instrument;
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
pub mod ssr_imports {
|
||||
mod ssr_imports {
|
||||
pub use broadcaster::BroadcastChannel;
|
||||
pub use once_cell::sync::OnceCell;
|
||||
pub use std::sync::atomic::{AtomicI32, Ordering};
|
||||
@@ -118,9 +118,9 @@ pub fn Counters() -> impl IntoView {
|
||||
// This is the typical pattern for a CRUD app
|
||||
#[component]
|
||||
pub fn Counter() -> impl IntoView {
|
||||
let dec = create_action(|_: &()| adjust_server_count(-1, "decing".into()));
|
||||
let inc = create_action(|_: &()| adjust_server_count(1, "incing".into()));
|
||||
let clear = create_action(|_: &()| clear_server_count());
|
||||
let dec = create_action(|_| adjust_server_count(-1, "decing".into()));
|
||||
let inc = create_action(|_| adjust_server_count(1, "incing".into()));
|
||||
let clear = create_action(|_| clear_server_count());
|
||||
let counter = create_resource(
|
||||
move || {
|
||||
(
|
||||
@@ -222,10 +222,9 @@ pub fn FormCounter() -> impl IntoView {
|
||||
#[component]
|
||||
pub fn MultiuserCounter() -> impl IntoView {
|
||||
let dec =
|
||||
create_action(|_: &()| adjust_server_count(-1, "dec dec goose".into()));
|
||||
let inc =
|
||||
create_action(|_: &()| adjust_server_count(1, "inc inc moose".into()));
|
||||
let clear = create_action(|_: &()| clear_server_count());
|
||||
create_action(|_| adjust_server_count(-1, "dec dec goose".into()));
|
||||
let inc = create_action(|_| adjust_server_count(1, "inc inc moose".into()));
|
||||
let clear = create_action(|_| clear_server_count());
|
||||
|
||||
#[cfg(not(feature = "ssr"))]
|
||||
let multiplayer_value = {
|
||||
|
||||
@@ -1,54 +1,57 @@
|
||||
mod counters;
|
||||
|
||||
use crate::counters::*;
|
||||
use actix_files::Files;
|
||||
use actix_web::*;
|
||||
use leptos::*;
|
||||
use leptos_actix::{generate_route_list, LeptosRoutes};
|
||||
use leptos::*;
|
||||
use actix_files::{Files};
|
||||
use actix_web::*;
|
||||
use crate::counters::*;
|
||||
use leptos_actix::{generate_route_list, LeptosRoutes};
|
||||
|
||||
#[get("/api/events")]
|
||||
async fn counter_events() -> impl Responder {
|
||||
use crate::counters::ssr_imports::*;
|
||||
use futures::StreamExt;
|
||||
#[get("/api/events")]
|
||||
async fn counter_events() -> impl Responder {
|
||||
use futures::StreamExt;
|
||||
|
||||
let stream = futures::stream::once(async {
|
||||
crate::counters::get_server_count().await.unwrap_or(0)
|
||||
})
|
||||
.chain(COUNT_CHANNEL.clone())
|
||||
.map(|value| {
|
||||
Ok(web::Bytes::from(format!(
|
||||
"event: message\ndata: {value}\n\n"
|
||||
))) as Result<web::Bytes>
|
||||
});
|
||||
HttpResponse::Ok()
|
||||
.insert_header(("Content-Type", "text/event-stream"))
|
||||
.streaming(stream)
|
||||
}
|
||||
|
||||
#[actix_web::main]
|
||||
async fn main() -> std::io::Result<()> {
|
||||
// Setting this to None means we'll be using cargo-leptos and its env vars.
|
||||
// when not using cargo-leptos None must be replaced with Some("Cargo.toml")
|
||||
let conf = get_configuration(None).await.unwrap();
|
||||
|
||||
let addr = conf.leptos_options.site_addr;
|
||||
let routes = generate_route_list(Counters);
|
||||
|
||||
HttpServer::new(move || {
|
||||
let leptos_options = &conf.leptos_options;
|
||||
let site_root = &leptos_options.site_root;
|
||||
|
||||
App::new()
|
||||
.service(counter_events)
|
||||
.leptos_routes(
|
||||
leptos_options.to_owned(),
|
||||
routes.to_owned(),
|
||||
Counters,
|
||||
)
|
||||
.service(Files::new("/", site_root))
|
||||
//.wrap(middleware::Compress::default())
|
||||
})
|
||||
.bind(&addr)?
|
||||
.run()
|
||||
.await
|
||||
let stream =
|
||||
futures::stream::once(async { crate::counters::get_server_count().await.unwrap_or(0) })
|
||||
.chain(COUNT_CHANNEL.clone())
|
||||
.map(|value| {
|
||||
Ok(web::Bytes::from(format!(
|
||||
"event: message\ndata: {value}\n\n"
|
||||
))) as Result<web::Bytes>
|
||||
});
|
||||
HttpResponse::Ok()
|
||||
.insert_header(("Content-Type", "text/event-stream"))
|
||||
.streaming(stream)
|
||||
}
|
||||
|
||||
#[actix_web::main]
|
||||
async fn main() -> std::io::Result<()> {
|
||||
|
||||
// Explicit server function registration is no longer required
|
||||
// on the main branch. On 0.3.0 and earlier, uncomment the lines
|
||||
// below to register the server functions.
|
||||
// _ = GetServerCount::register();
|
||||
// _ = AdjustServerCount::register();
|
||||
// _ = ClearServerCount::register();
|
||||
|
||||
// Setting this to None means we'll be using cargo-leptos and its env vars.
|
||||
// when not using cargo-leptos None must be replaced with Some("Cargo.toml")
|
||||
let conf = get_configuration(None).await.unwrap();
|
||||
|
||||
let addr = conf.leptos_options.site_addr;
|
||||
let routes = generate_route_list(Counters);
|
||||
|
||||
HttpServer::new(move || {
|
||||
let leptos_options = &conf.leptos_options;
|
||||
let site_root = &leptos_options.site_root;
|
||||
|
||||
App::new()
|
||||
.service(counter_events)
|
||||
.leptos_routes(leptos_options.to_owned(), routes.to_owned(), Counters)
|
||||
.service(Files::new("/", site_root))
|
||||
//.wrap(middleware::Compress::default())
|
||||
})
|
||||
.bind(&addr)?
|
||||
.run()
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ async fn main() {
|
||||
|
||||
// run our app with hyper
|
||||
// `axum::Server` is a re-export of `hyper::Server`
|
||||
println!("listening on {}", addr);
|
||||
logging::log!("listening on {}", addr);
|
||||
let listener = tokio::net::TcpListener::bind(&addr).await.unwrap();
|
||||
axum::serve(listener, app.into_make_service())
|
||||
.await
|
||||
|
||||
@@ -11,22 +11,21 @@ codegen-units = 1
|
||||
lto = true
|
||||
|
||||
[dependencies]
|
||||
console_log = "1.0.0"
|
||||
console_error_panic_hook = "0.1.7"
|
||||
cfg-if = "1.0.0"
|
||||
leptos = { version = "0.5", features = ["nightly"] }
|
||||
leptos_axum = { version = "0.5", default-features = false, optional = true }
|
||||
leptos_meta = { version = "0.5", features = ["nightly"] }
|
||||
leptos_router = { version = "0.5", features = ["nightly"] }
|
||||
log = "0.4.17"
|
||||
simple_logger = "4.0.0"
|
||||
serde = { version = "1.0.148", features = ["derive"] }
|
||||
console_log = "1.0"
|
||||
console_error_panic_hook = "0.1"
|
||||
leptos = { path = "../../leptos", features = ["nightly"] }
|
||||
leptos_axum = { path = "../../integrations/axum", default-features = false, optional = true }
|
||||
leptos_meta = { path = "../../meta", features = ["nightly"] }
|
||||
leptos_router = { path = "../../router", features = ["nightly"] }
|
||||
log = "0.4"
|
||||
simple_logger = "4.0"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
tracing = "0.1"
|
||||
gloo-net = { version = "0.4.0", features = ["http"] }
|
||||
reqwest = { version = "0.11.13", features = ["json"] }
|
||||
axum = { version = "0.6", default-features = false, optional = true }
|
||||
tower = { version = "0.4.13", optional = true }
|
||||
http = { version = "0.2.11", optional = true }
|
||||
gloo-net = { version = "0.4", features = ["http"] }
|
||||
reqwest = { version = "0.11", features = ["json"] }
|
||||
axum = { version = "0.7", default-features = false, optional = true }
|
||||
tower = { version = "0.4", optional = true }
|
||||
http = { version = "1.0", optional = true }
|
||||
web-sys = { version = "0.3", features = [
|
||||
"AbortController",
|
||||
"AbortSignal",
|
||||
@@ -34,10 +33,10 @@ web-sys = { version = "0.3", features = [
|
||||
"Response",
|
||||
] }
|
||||
wasm-bindgen = "0.2"
|
||||
wasm-bindgen-futures = { version = "0.4.37", features = [
|
||||
wasm-bindgen-futures = { version = "0.4", features = [
|
||||
"futures-core-03-stream",
|
||||
], optional = true }
|
||||
axum-js-fetch = { version = "0.2.1", optional = true }
|
||||
axum-js-fetch = { version = "0.2", optional = true }
|
||||
lazy_static = "1.4.0"
|
||||
|
||||
[features]
|
||||
|
||||
@@ -2,8 +2,6 @@
|
||||
|
||||
This example uses the basic Hacker News example as its basis, but shows how to run the server side as WASM running in a JS environment. In this example, Deno is used as the runtime.
|
||||
|
||||
**NOTE**: This example is slightly out of date pending an update to [`axum-js-fetch`](https://github.com/seanaye/axum-js-fetch/), which was waiting on a version of `gloo-net` that uses `http` 1.0. It still works with Leptos 0.5 and Axum 0.6, but not with the versions of Leptos (0.6 and later) that support Axum 1.0.
|
||||
|
||||
## Server Side Rendering with Deno
|
||||
|
||||
To run the Deno version, run
|
||||
|
||||
@@ -40,7 +40,7 @@ pub fn hydrate() {
|
||||
#[cfg(feature = "ssr")]
|
||||
mod ssr_imports {
|
||||
use crate::App;
|
||||
use axum::{routing::post, Router};
|
||||
use axum::Router;
|
||||
use leptos::*;
|
||||
use leptos_axum::{generate_route_list, LeptosRoutes};
|
||||
use log::{info, Level};
|
||||
@@ -52,7 +52,7 @@ mod ssr_imports {
|
||||
#[wasm_bindgen]
|
||||
impl Handler {
|
||||
pub async fn new() -> Self {
|
||||
_ = console_log::init_with_level(Level::Debug);
|
||||
console_log::init_with_level(Level::Debug);
|
||||
console_error_panic_hook::set_once();
|
||||
|
||||
let leptos_options = LeptosOptions::builder()
|
||||
@@ -62,9 +62,8 @@ mod ssr_imports {
|
||||
let routes = generate_route_list(App);
|
||||
|
||||
// build our application with a route
|
||||
let app: axum::Router<(), axum::body::Body> = Router::new()
|
||||
.leptos_routes(&leptos_options, routes, || view! { <App/> })
|
||||
.route("/api/*fn_name", post(leptos_axum::handle_server_fns))
|
||||
let app: axum::Router = Router::new()
|
||||
.leptos_routes(&leptos_options, routes, App)
|
||||
.with_state(leptos_options);
|
||||
|
||||
info!("creating handler instance");
|
||||
|
||||
3
examples/pavex_demo/.gitignore
vendored
Normal file
3
examples/pavex_demo/.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
/target
|
||||
.env
|
||||
.direnv
|
||||
92
examples/pavex_demo/Cargo.toml
Normal file
92
examples/pavex_demo/Cargo.toml
Normal file
@@ -0,0 +1,92 @@
|
||||
[workspace]
|
||||
members = ["todo_app_sqlite_pavex", "todo_app_sqlite_pavex_server_sdk", "todo_app_sqlite_pavex_server", "leptos_app"]
|
||||
# By setting `todo_app_sqlite_pavex_server` as the default member, `cargo run` will default to running the server binary
|
||||
# when executed from the root of the workspace.
|
||||
# Otherwise, you would have to use `cargo run --bin api` to run the server binary.
|
||||
default-members = ["todo_app_sqlite_pavex_server"]
|
||||
resolver = "2"
|
||||
|
||||
# need to be applied only to wasm build
|
||||
[profile.wasm_release]
|
||||
codegen-units = 1
|
||||
lto = true
|
||||
opt-level = 'z'
|
||||
|
||||
[workspace.dependencies]
|
||||
leptos = { version = "0.5", features = ["nightly"] }
|
||||
leptos_meta = { version = "0.5", features = ["nightly"] }
|
||||
leptos_router = { version = "0.5", features = ["nightly"] }
|
||||
leptos_pavex = { version = "0.5" }
|
||||
cfg_if = "1"
|
||||
thiserror = "1"
|
||||
|
||||
# See https://github.com/akesson/cargo-leptos for documentation of all the parameters.
|
||||
|
||||
# A leptos project defines which workspace members
|
||||
# that are used together frontend (lib) & server (bin)
|
||||
[[workspace.metadata.leptos]]
|
||||
# this name is used for the wasm, js and css file names
|
||||
name = "start-pavex-workspace"
|
||||
|
||||
# the package in the workspace that contains the server binary (binary crate)
|
||||
bin-package = "server"
|
||||
|
||||
# the package in the workspace that contains the frontend wasm binary (library crate)
|
||||
lib-package = "leptos_frontend"
|
||||
|
||||
# The site root folder is where cargo-leptos generate all output. WARNING: all content of this folder will be erased on a rebuild. Use it in your server setup.
|
||||
site-root = "target/site"
|
||||
|
||||
# The site-root relative folder where all compiled output (JS, WASM and CSS) is written
|
||||
# Defaults to pkg
|
||||
site-pkg-dir = "pkg"
|
||||
|
||||
# [Optional] The source CSS file. If it ends with .sass or .scss then it will be compiled by dart-sass into CSS. The CSS is optimized by Lightning CSS before being written to <site-root>/<site-pkg>/app.css
|
||||
style-file = "style/main.scss"
|
||||
|
||||
# Assets source dir. All files found here will be copied and synchronized to site-root.
|
||||
# The assets-dir cannot have a sub directory with the same name/path as site-pkg-dir.
|
||||
#
|
||||
# Optional. Env: LEPTOS_ASSETS_DIR.
|
||||
assets-dir = "public"
|
||||
|
||||
# The IP and port (ex: 127.0.0.1:3000) where the server serves the content. Use it in your server setup.
|
||||
site-addr = "127.0.0.1:3000"
|
||||
|
||||
# The port to use for automatic reload monitoring
|
||||
reload-port = 3001
|
||||
|
||||
# [Optional] Command to use when running end2end tests. It will run in the end2end dir.
|
||||
# [Windows] for non-WSL use "npx.cmd playwright test"
|
||||
# This binary name can be checked in Powershell with Get-Command npx
|
||||
end2end-cmd = "npx playwright test"
|
||||
end2end-dir = "end2end"
|
||||
|
||||
# The browserlist query used for optimizing the CSS.
|
||||
browserquery = "defaults"
|
||||
|
||||
# Set by cargo-leptos watch when building with that tool. Controls whether autoreload JS will be included in the head
|
||||
watch = false
|
||||
|
||||
# The environment Leptos will run in, usually either "DEV" or "PROD"
|
||||
env = "DEV"
|
||||
|
||||
# The features to use when compiling the bin target
|
||||
#
|
||||
# Optional. Can be over-ridden with the command line parameter --bin-features
|
||||
bin-features = []
|
||||
|
||||
# If the --no-default-features flag should be used when compiling the bin target
|
||||
#
|
||||
# Optional. Defaults to false.
|
||||
bin-default-features = false
|
||||
|
||||
# The features to use when compiling the lib target
|
||||
#
|
||||
# Optional. Can be over-ridden with the command line parameter --lib-features
|
||||
lib-features = []
|
||||
|
||||
# If the --no-default-features flag should be used when compiling the lib target
|
||||
#
|
||||
# Optional. Defaults to false.
|
||||
lib-default-features = false
|
||||
71
examples/pavex_demo/README.md
Normal file
71
examples/pavex_demo/README.md
Normal file
@@ -0,0 +1,71 @@
|
||||
# todo_app_sqlite_pavex
|
||||
|
||||
# Getting started
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Rust (see [here](https://www.rust-lang.org/tools/install) for instructions)
|
||||
- `cargo-px`:
|
||||
```bash
|
||||
cargo install --locked cargo-px --version="~0.1"
|
||||
```
|
||||
- [Pavex](https://pavex.dev)
|
||||
|
||||
## Useful commands
|
||||
|
||||
`todo_app_sqlite_pavex` is built using the [Pavex](https://pavex.dev) web framework, which relies on code generation.
|
||||
You need to use the `cargo px` command instead of `cargo`: it ensures that the
|
||||
`todo_app_sqlite_pavex_server_sdk` crate is correctly regenerated when the
|
||||
application blueprint changes.
|
||||
|
||||
`cargo px` is a wrapper around `cargo` that will automatically regenerate the
|
||||
server SDK when needed. Check out its [documentation](https://github.com/LukeMathWalker/cargo-px)
|
||||
for more details.
|
||||
|
||||
### Build
|
||||
|
||||
```bash
|
||||
cargo px build
|
||||
```
|
||||
|
||||
### Run
|
||||
|
||||
```bash
|
||||
cargo px run
|
||||
```
|
||||
|
||||
### Test
|
||||
|
||||
```bash
|
||||
cargo px test
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
All configurable parameters are listed in `todo_app_sqlite_pavex/src/configuration.rs`.
|
||||
|
||||
Configuration values are loaded from two sources:
|
||||
|
||||
- Configuration files
|
||||
- Environment variables
|
||||
|
||||
Environment variables take precedence over configuration files.
|
||||
|
||||
All configuration files are in the `todo_app_sqlite_pavex_server/configuration` folder.
|
||||
The application can be run in three different profiles: `dev`, `test` and `prod`.
|
||||
The settings that you want to share across all profiles should be placed in `todo_app_sqlite_pavex_server/configuration/base.yml`.
|
||||
Profile-specific configuration files can be then used
|
||||
to override or supply additional values on top of the default settings (e.g. `todo_app_sqlite_pavex_server/configuration/dev.yml`).
|
||||
|
||||
You can specify the app profile that you want to use by setting the `APP_PROFILE` environment variable; e.g.:
|
||||
|
||||
```bash
|
||||
APP_PROFILE=prod cargo px run
|
||||
```
|
||||
|
||||
for running the application with the `prod` profile.
|
||||
|
||||
By default, the `dev` profile is used since `APP_PROFILE` is set to `dev` in the `.env` file at the root of the project.
|
||||
The `.env` file should not be committed to version control: it is meant to be used for local development only,
|
||||
so that each developer can specify their own environment variables for secret values (e.g. database credentials)
|
||||
that shouldn't be stored in configuration files (given their sensitive nature).
|
||||
119
examples/pavex_demo/flake.lock
generated
Normal file
119
examples/pavex_demo/flake.lock
generated
Normal file
@@ -0,0 +1,119 @@
|
||||
{
|
||||
"nodes": {
|
||||
"cargo-pavex-git": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
"lastModified": 1703610192,
|
||||
"narHash": "sha256-+oM6VGRRt/DQdhEFWJFIpKfY29w72V0vRpud8NsOI7c=",
|
||||
"owner": "LukeMathWalker",
|
||||
"repo": "pavex",
|
||||
"rev": "e302f99e3641a55fe5624ba6c8154ce64e732a89",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "LukeMathWalker",
|
||||
"repo": "pavex",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"cargo-px-git": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
"lastModified": 1702137928,
|
||||
"narHash": "sha256-FbwHEOQnIYKhxp4Ne9XBIUJXu1o+ak6y9MhzRenIW40=",
|
||||
"owner": "LukeMathWalker",
|
||||
"repo": "cargo-px",
|
||||
"rev": "d1bb9075c4993130f31f31c95642567a2255bd8e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "LukeMathWalker",
|
||||
"repo": "cargo-px",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"flake-utils": {
|
||||
"inputs": {
|
||||
"systems": "systems"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1701680307,
|
||||
"narHash": "sha256-kAuep2h5ajznlPMD9rnQyffWG8EM/C73lejGofXvdM8=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "4022d587cbbfd70fe950c1e2083a02621806a725",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1703499205,
|
||||
"narHash": "sha256-lF9rK5mSUfIZJgZxC3ge40tp1gmyyOXZ+lRY3P8bfbg=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "e1fa12d4f6c6fe19ccb59cac54b5b3f25e160870",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"ref": "nixpkgs-unstable",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"cargo-pavex-git": "cargo-pavex-git",
|
||||
"cargo-px-git": "cargo-px-git",
|
||||
"flake-utils": "flake-utils",
|
||||
"nixpkgs": "nixpkgs",
|
||||
"rust-overlay": "rust-overlay"
|
||||
}
|
||||
},
|
||||
"rust-overlay": {
|
||||
"inputs": {
|
||||
"flake-utils": [
|
||||
"flake-utils"
|
||||
],
|
||||
"nixpkgs": [
|
||||
"nixpkgs"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1703643208,
|
||||
"narHash": "sha256-UL4KO8JxnD5rOycwHqBAf84lExF1/VnYMDC7b/wpPDU=",
|
||||
"owner": "oxalica",
|
||||
"repo": "rust-overlay",
|
||||
"rev": "ce117f3e0de8262be8cd324ee6357775228687cf",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "oxalica",
|
||||
"repo": "rust-overlay",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"systems": {
|
||||
"locked": {
|
||||
"lastModified": 1681028828,
|
||||
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"type": "github"
|
||||
}
|
||||
}
|
||||
},
|
||||
"root": "root",
|
||||
"version": 7
|
||||
}
|
||||
129
examples/pavex_demo/flake.nix
Normal file
129
examples/pavex_demo/flake.nix
Normal file
@@ -0,0 +1,129 @@
|
||||
{
|
||||
description = "Build Pavex tools";
|
||||
|
||||
inputs = {
|
||||
nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
|
||||
|
||||
flake-utils.url = "github:numtide/flake-utils";
|
||||
|
||||
cargo-px-git = {
|
||||
url = "github:/LukeMathWalker/cargo-px";
|
||||
flake = false;
|
||||
};
|
||||
cargo-pavex-git = {
|
||||
url = "github:LukeMathWalker/pavex";
|
||||
flake = false;
|
||||
};
|
||||
|
||||
rust-overlay = {
|
||||
url = "github:oxalica/rust-overlay";
|
||||
inputs = {
|
||||
nixpkgs.follows = "nixpkgs";
|
||||
flake-utils.follows = "flake-utils";
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
outputs = { self, nixpkgs, flake-utils, rust-overlay, ... } @inputs:
|
||||
flake-utils.lib.eachDefaultSystem
|
||||
(system:
|
||||
let
|
||||
pkgs = import nixpkgs {
|
||||
inherit system;
|
||||
overlays = [ (import rust-overlay) ];
|
||||
};
|
||||
inherit (pkgs) lib;
|
||||
rustTarget = pkgs.rust-bin.selectLatestNightlyWith( toolchain: toolchain.default.override {
|
||||
extensions = [ "rust-src" "rust-analyzer" "rustc-codegen-cranelift-preview" "rust-docs-json"];
|
||||
targets = [ "wasm32-unknown-unknown" ];
|
||||
});
|
||||
|
||||
|
||||
cargo-pavex_cli-git = pkgs.rustPlatform.buildRustPackage rec {
|
||||
pname = "cargo-pavex-cli";
|
||||
version = "0.2.22";
|
||||
#buildFeatures = ["no_downloads"]; # cargo-leptos will try to download Ruby and other things without this feature
|
||||
|
||||
src = inputs.cargo-pavex-git;
|
||||
sourceRoot = "source/libs";
|
||||
cargoLock = {
|
||||
lockFile = inputs.cargo-pavex-git + "/libs/Cargo.lock";
|
||||
outputHashes = {
|
||||
"matchit-0.7.3" = "sha256-1bhbWvLlDb6/UJ4j2FqoG7j3DD1dTOLl6RaiY9kasmQ=";
|
||||
#"pavex-0.1.0" = "sha256-NC7T1pcXJiWPtAWeiMUNzf2MUsYaRYxjLIL9fCqhExo=";
|
||||
};
|
||||
};
|
||||
#buildAndTestSubdir = "libs";
|
||||
cargoSha256 = "";
|
||||
nativeBuildInputs = [pkgs.pkg-config pkgs.openssl pkgs.git];
|
||||
|
||||
buildInputs = with pkgs;
|
||||
[openssl pkg-config git]
|
||||
++ lib.optionals stdenv.isDarwin [
|
||||
Security
|
||||
];
|
||||
|
||||
doCheck = false; # integration tests depend on changing cargo config
|
||||
|
||||
meta = with lib; {
|
||||
description = "An easy-to-use Rust framework for building robust and performant APIs";
|
||||
homepage = "https://github.com/LukeMatthewWalker/pavex";
|
||||
changelog = "https://github.com/LukeMatthewWalker/pavex/blob/v${version}/CHANGELOG.md";
|
||||
license = with licenses; [mit];
|
||||
maintainers = with maintainers; [benwis];
|
||||
};
|
||||
};
|
||||
cargo-px-git = pkgs.rustPlatform.buildRustPackage rec {
|
||||
pname = "cargo-px";
|
||||
version = "0.2.22";
|
||||
#buildFeatures = ["no_downloads"]; # cargo-leptos will try to download Ruby and other things without this feature
|
||||
|
||||
src = inputs.cargo-px-git;
|
||||
|
||||
cargoSha256 ="sha256-+pyeqh0IoZ1JMgbhWxhEJw1MPgG7XeocVrqJoSNjgDA=";
|
||||
|
||||
nativeBuildInputs = [pkgs.pkg-config pkgs.openssl pkgs.git];
|
||||
|
||||
buildInputs = with pkgs;
|
||||
[openssl pkg-config git]
|
||||
++ lib.optionals stdenv.isDarwin [
|
||||
Security
|
||||
];
|
||||
|
||||
doCheck = false; # integration tests depend on changing cargo config
|
||||
|
||||
meta = with lib; {
|
||||
description = "A cargo subcommand that extends cargo's capabilities when it comes to code generation.";
|
||||
homepage = "https://github.com/LukeMatthewWalker/cargo-px";
|
||||
changelog = "https://github.com/LukeMatthewWalker/cargo-px/blob/v${version}/CHANGELOG.md";
|
||||
license = with licenses; [mit];
|
||||
maintainers = with maintainers; [benwis];
|
||||
};
|
||||
};
|
||||
in
|
||||
{
|
||||
|
||||
devShells.default = pkgs.mkShell {
|
||||
|
||||
# Extra inputs can be added here
|
||||
nativeBuildInputs = with pkgs; [
|
||||
#rustTarget
|
||||
rustup
|
||||
openssl
|
||||
pkg-config
|
||||
clang
|
||||
tailwindcss
|
||||
mold-wrapped
|
||||
cargo-px-git
|
||||
cargo-pavex_cli-git
|
||||
];
|
||||
#RUST_SRC_PATH = "${rustTarget}/lib/rustlib/src/rust/library";
|
||||
MOLD_PATH = "${pkgs.mold-wrapped}/bin/mold";
|
||||
|
||||
shellHook = ''
|
||||
sed -i -e '/rustflags = \["-C", "link-arg=-fuse-ld=/ s|ld=.*|ld=${pkgs.mold-wrapped}/bin/mold"]|' .cargo/config.toml
|
||||
'';
|
||||
LIBCLANG_PATH = "${pkgs.llvmPackages.libclang.lib}/lib";
|
||||
};
|
||||
});
|
||||
}
|
||||
21
examples/pavex_demo/leptos_app/Cargo.toml
Normal file
21
examples/pavex_demo/leptos_app/Cargo.toml
Normal file
@@ -0,0 +1,21 @@
|
||||
[package]
|
||||
name = "leptos_app"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
leptos.workspace = true
|
||||
leptos_meta.workspace = true
|
||||
leptos_router.workspace = true
|
||||
leptos_pavex = { workspace = true, optional = true }
|
||||
|
||||
#http.workspace = true
|
||||
cfg_if.workspace = true
|
||||
thiserror.workspace = true
|
||||
|
||||
[features]
|
||||
default = []
|
||||
hydrate = ["leptos/hydrate", "leptos_meta/hydrate", "leptos_router/hydrate"]
|
||||
ssr = ["leptos/ssr", "leptos_meta/ssr", "leptos_router/ssr", "dep:leptos_pavex"]
|
||||
73
examples/pavex_demo/leptos_app/src/error_template.rs
Normal file
73
examples/pavex_demo/leptos_app/src/error_template.rs
Normal file
@@ -0,0 +1,73 @@
|
||||
use cfg_if::cfg_if;
|
||||
use http::status::StatusCode;
|
||||
use leptos::*;
|
||||
#[cfg(feature = "ssr")]
|
||||
use leptos_axum::ResponseOptions;
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Clone, Debug, Error)]
|
||||
pub enum AppError {
|
||||
#[error("Not Found")]
|
||||
NotFound,
|
||||
}
|
||||
|
||||
impl AppError {
|
||||
pub fn status_code(&self) -> StatusCode {
|
||||
match self {
|
||||
AppError::NotFound => StatusCode::NOT_FOUND,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// A basic function to display errors served by the error boundaries.
|
||||
// Feel free to do more complicated things here than just displaying the error.
|
||||
#[component]
|
||||
pub fn ErrorTemplate(
|
||||
#[prop(optional)] outside_errors: Option<Errors>,
|
||||
#[prop(optional)] errors: Option<RwSignal<Errors>>,
|
||||
) -> impl IntoView {
|
||||
let errors = match outside_errors {
|
||||
Some(e) => create_rw_signal(e),
|
||||
None => match errors {
|
||||
Some(e) => e,
|
||||
None => panic!("No Errors found and we expected errors!"),
|
||||
},
|
||||
};
|
||||
// Get Errors from Signal
|
||||
let errors = errors.get_untracked();
|
||||
|
||||
// Downcast lets us take a type that implements `std::error::Error`
|
||||
let errors: Vec<AppError> = errors
|
||||
.into_iter()
|
||||
.filter_map(|(_k, v)| v.downcast_ref::<AppError>().cloned())
|
||||
.collect();
|
||||
println!("Errors: {errors:#?}");
|
||||
|
||||
// Only the response code for the first error is actually sent from the server
|
||||
// this may be customized by the specific application
|
||||
cfg_if! { if #[cfg(feature="ssr")] {
|
||||
let response = use_context::<ResponseOptions>();
|
||||
if let Some(response) = response {
|
||||
response.set_status(errors[0].status_code());
|
||||
}
|
||||
}}
|
||||
|
||||
view! {
|
||||
<h1>{if errors.len() > 1 { "Errors" } else { "Error" }}</h1>
|
||||
<For
|
||||
// a function that returns the items we're iterating over; a signal is fine
|
||||
each=move || { errors.clone().into_iter().enumerate() }
|
||||
// a unique key for each item as a reference
|
||||
key=|(index, _error)| *index
|
||||
// renders each item to a view
|
||||
children=move |error| {
|
||||
let error_string = error.1.to_string();
|
||||
let error_code = error.1.status_code();
|
||||
view! {
|
||||
<h2>{error_code.to_string()}</h2>
|
||||
<p>"Error: " {error_string}</p>
|
||||
}
|
||||
}
|
||||
/>
|
||||
}
|
||||
}
|
||||
45
examples/pavex_demo/leptos_app/src/lib.rs
Normal file
45
examples/pavex_demo/leptos_app/src/lib.rs
Normal file
@@ -0,0 +1,45 @@
|
||||
use crate::error_template::{AppError, ErrorTemplate};
|
||||
use leptos::*;
|
||||
use leptos_meta::*;
|
||||
use leptos_router::*;
|
||||
|
||||
pub mod error_template;
|
||||
|
||||
#[component]
|
||||
pub fn App() -> impl IntoView {
|
||||
// Provides context that manages stylesheets, titles, meta tags, etc.
|
||||
provide_meta_context();
|
||||
|
||||
view! {
|
||||
<Stylesheet id="leptos" href="/pkg/start-axum-workspace.css"/>
|
||||
|
||||
// sets the document title
|
||||
<Title text="Welcome to Leptos"/>
|
||||
|
||||
// content for this welcome page
|
||||
<Router fallback=|| {
|
||||
let mut outside_errors = Errors::default();
|
||||
outside_errors.insert_with_default_key(AppError::NotFound);
|
||||
view! { <ErrorTemplate outside_errors/> }.into_view()
|
||||
}>
|
||||
<main>
|
||||
<Routes>
|
||||
<Route path="" view=HomePage/>
|
||||
</Routes>
|
||||
</main>
|
||||
</Router>
|
||||
}
|
||||
}
|
||||
|
||||
/// Renders the home page of your application.
|
||||
#[component]
|
||||
fn HomePage() -> impl IntoView {
|
||||
// Creates a reactive value to update the button
|
||||
let (count, set_count) = create_signal(0);
|
||||
let on_click = move |_| set_count.update(|count| *count += 1);
|
||||
|
||||
view! {
|
||||
<h1>"Welcome to Leptos on Pavex!"</h1>
|
||||
<button on:click=on_click>"Click Me: " {count}</button>
|
||||
}
|
||||
}
|
||||
8
examples/pavex_demo/leptos_front/Cargo.toml
Normal file
8
examples/pavex_demo/leptos_front/Cargo.toml
Normal file
@@ -0,0 +1,8 @@
|
||||
[package]
|
||||
name = "leptos_front"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
13
examples/pavex_demo/leptos_front/src/lib.rs
Normal file
13
examples/pavex_demo/leptos_front/src/lib.rs
Normal file
@@ -0,0 +1,13 @@
|
||||
use leptos::*;
|
||||
use leptos_app::*;
|
||||
use wasm_bindgen::prelude::wasm_bindgen;
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn hydrate() {
|
||||
// initializes logging using the `log` crate
|
||||
_ = console_log::init_with_level(log::Level::Debug);
|
||||
console_error_panic_hook::set_once();
|
||||
|
||||
leptos::mount_to_body(App);
|
||||
}
|
||||
|
||||
4
examples/pavex_demo/style/main.scss
Normal file
4
examples/pavex_demo/style/main.scss
Normal file
@@ -0,0 +1,4 @@
|
||||
body {
|
||||
font-family: sans-serif;
|
||||
text-align: center;
|
||||
}
|
||||
22
examples/pavex_demo/todo_app_sqlite_pavex/Cargo.toml
Normal file
22
examples/pavex_demo/todo_app_sqlite_pavex/Cargo.toml
Normal file
@@ -0,0 +1,22 @@
|
||||
[package]
|
||||
name = "todo_app_sqlite_pavex"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[[bin]]
|
||||
path = "src/bin/bp.rs"
|
||||
name = "bp"
|
||||
|
||||
[dependencies]
|
||||
cargo_px_env = "0.1"
|
||||
pavex = { git = "https://github.com/LukeMathWalker/pavex", branch = "main" }
|
||||
pavex_cli_client = { git = "https://github.com/LukeMathWalker/pavex", branch = "main" }
|
||||
tracing = "0.1"
|
||||
|
||||
# Configuration
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde-aux = "4"
|
||||
|
||||
# Leptos
|
||||
leptos_pavex.workspace = true
|
||||
|
||||
17
examples/pavex_demo/todo_app_sqlite_pavex/src/bin/bp.rs
Normal file
17
examples/pavex_demo/todo_app_sqlite_pavex/src/bin/bp.rs
Normal file
@@ -0,0 +1,17 @@
|
||||
use cargo_px_env::generated_pkg_manifest_path;
|
||||
use todo_app_sqlite_pavex::blueprint;
|
||||
use pavex_cli_client::Client;
|
||||
use std::error::Error;
|
||||
|
||||
/// Generate the `todo_app_sqlite_pavex_server_sdk` crate using Pavex's CLI.
|
||||
///
|
||||
/// Pavex will automatically wire all our routes, constructors and error handlers
|
||||
/// into the a "server SDK" that can be used by the final API server binary to launch
|
||||
/// the application.
|
||||
fn main() -> Result<(), Box<dyn Error>> {
|
||||
let generated_dir = generated_pkg_manifest_path()?.parent().unwrap().into();
|
||||
Client::new()
|
||||
.generate(blueprint(), generated_dir)
|
||||
.execute()?;
|
||||
Ok(())
|
||||
}
|
||||
98
examples/pavex_demo/todo_app_sqlite_pavex/src/blueprint.rs
Normal file
98
examples/pavex_demo/todo_app_sqlite_pavex/src/blueprint.rs
Normal file
@@ -0,0 +1,98 @@
|
||||
use leptos_pavex::{LeptosOptions, RouteListing};
|
||||
use pavex::{
|
||||
blueprint::{
|
||||
constructor::{CloningStrategy, Lifecycle},
|
||||
router::{ANY, GET},
|
||||
Blueprint,
|
||||
},
|
||||
f,
|
||||
};
|
||||
/// The main blueprint, containing all the routes, constructors and error handlers
|
||||
/// required by our API.
|
||||
pub fn blueprint() -> Blueprint {
|
||||
let mut bp = Blueprint::new();
|
||||
register_common_constructors(&mut bp);
|
||||
|
||||
bp.constructor(
|
||||
f!(crate::user_agent::UserAgent::extract),
|
||||
Lifecycle::RequestScoped,
|
||||
)
|
||||
.error_handler(f!(crate::user_agent::invalid_user_agent));
|
||||
|
||||
add_telemetry_middleware(&mut bp);
|
||||
|
||||
bp.route(GET, "/test/ping", f!(crate::routes::status::ping));
|
||||
bp.route(GET, "/test/greet/:name", f!(crate::routes::greet::greet));
|
||||
// Handle all /api requests as those are Leptos server fns
|
||||
bp.route(ANY, "/api/*fn_name", f!(leptos_pavex::handle_server_fns));
|
||||
bp.route(ANY, "/");
|
||||
bp.fallback(f!(file_handler));
|
||||
bp
|
||||
}
|
||||
|
||||
/// Common constructors used by all routes.
|
||||
fn register_common_constructors(bp: &mut Blueprint) {
|
||||
// Configuration Options
|
||||
bp.constructor(
|
||||
f!(crate::leptos::get_cargo_leptos_conf(), Lifecycle::Singleton),
|
||||
Lifecycle::Singleton,
|
||||
);
|
||||
// List of Routes
|
||||
bp.constructor(
|
||||
f!(crate::leptos::get_app_route_listing(), Lifecycle::Singleton),
|
||||
Lifecycle::Singleton,
|
||||
);
|
||||
|
||||
bp.constructor(
|
||||
f!(leptos_pavex::PavexRequest::extract),
|
||||
LifeCycle::RequestScoped,
|
||||
);
|
||||
// Query parameters
|
||||
bp.constructor(
|
||||
f!(pavex::request::query::QueryParams::extract),
|
||||
Lifecycle::RequestScoped,
|
||||
)
|
||||
.error_handler(f!(
|
||||
pavex::request::query::errors::ExtractQueryParamsError::into_response
|
||||
));
|
||||
|
||||
// Route parameters
|
||||
bp.constructor(
|
||||
f!(pavex::request::route::RouteParams::extract),
|
||||
Lifecycle::RequestScoped,
|
||||
)
|
||||
.error_handler(f!(
|
||||
pavex::request::route::errors::ExtractRouteParamsError::into_response
|
||||
));
|
||||
|
||||
// Json body
|
||||
bp.constructor(
|
||||
f!(pavex::request::body::JsonBody::extract),
|
||||
Lifecycle::RequestScoped,
|
||||
)
|
||||
.error_handler(f!(
|
||||
pavex::request::body::errors::ExtractJsonBodyError::into_response
|
||||
));
|
||||
bp.constructor(
|
||||
f!(pavex::request::body::BufferedBody::extract),
|
||||
Lifecycle::RequestScoped,
|
||||
)
|
||||
.error_handler(f!(
|
||||
pavex::request::body::errors::ExtractBufferedBodyError::into_response
|
||||
));
|
||||
bp.constructor(
|
||||
f!(<pavex::request::body::BodySizeLimit as std::default::Default>::default),
|
||||
Lifecycle::RequestScoped,
|
||||
);
|
||||
}
|
||||
|
||||
/// Add the telemetry middleware, as well as the constructors of its dependencies.
|
||||
fn add_telemetry_middleware(bp: &mut Blueprint) {
|
||||
bp.constructor(
|
||||
f!(crate::telemetry::RootSpan::new),
|
||||
Lifecycle::RequestScoped,
|
||||
)
|
||||
.cloning(CloningStrategy::CloneIfNecessary);
|
||||
|
||||
bp.wrap(f!(crate::telemetry::logger));
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
use pavex::server::IncomingStream;
|
||||
use serde_aux::field_attributes::deserialize_number_from_string;
|
||||
use std::net::SocketAddr;
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
/// The top-level configuration, holding all the values required
|
||||
/// to configure the entire application.
|
||||
pub struct Config {
|
||||
pub server: ServerConfig,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize, Clone)]
|
||||
/// Configuration for the HTTP server used to expose our API
|
||||
/// to users.
|
||||
pub struct ServerConfig {
|
||||
/// The port that the server must listen on.
|
||||
#[serde(deserialize_with = "deserialize_number_from_string")]
|
||||
pub port: u16,
|
||||
/// The network interface that the server must be bound to.
|
||||
///
|
||||
/// E.g. `0.0.0.0` for listening to incoming requests from
|
||||
/// all sources.
|
||||
pub ip: std::net::IpAddr,
|
||||
}
|
||||
|
||||
impl ServerConfig {
|
||||
/// Bind a TCP listener according to the specified parameters.
|
||||
pub async fn listener(&self) -> Result<IncomingStream, std::io::Error> {
|
||||
let addr = SocketAddr::new(self.ip, self.port);
|
||||
IncomingStream::bind(addr).await
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
use app::error_template::AppError;
|
||||
use app::error_template::ErrorTemplate;
|
||||
use app::App;
|
||||
use axum::response::Response as AxumResponse;
|
||||
use axum::{
|
||||
body::{boxed, Body, BoxBody},
|
||||
extract::State,
|
||||
http::{Request, Response, StatusCode, Uri},
|
||||
response::IntoResponse,
|
||||
};
|
||||
use leptos::*;
|
||||
use tower::ServiceExt;
|
||||
use tower_http::services::ServeDir;
|
||||
|
||||
pub async fn file_and_error_handler(
|
||||
uri: Uri,
|
||||
State(options): State<LeptosOptions>,
|
||||
req: Request<Body>,
|
||||
) -> AxumResponse {
|
||||
let root = options.site_root.clone();
|
||||
let res = get_static_file(uri.clone(), &root).await.unwrap();
|
||||
|
||||
if res.status() == StatusCode::OK {
|
||||
res.into_response()
|
||||
} else {
|
||||
let handler = leptos_axum::render_app_to_stream(options.to_owned(), move || view! { <App/> });
|
||||
handler(req).await.into_response()
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_static_file(uri: Uri, root: &str) -> Result<Response<BoxBody>, (StatusCode, String)> {
|
||||
let req = Request::builder()
|
||||
.uri(uri.clone())
|
||||
.body(Body::empty())
|
||||
.unwrap();
|
||||
// `ServeDir` implements `tower::Service` so we can call it with `tower::ServiceExt::oneshot`
|
||||
// This path is relative to the cargo root
|
||||
match ServeDir::new(root).oneshot(req).await {
|
||||
Ok(res) => Ok(res.map(boxed)),
|
||||
Err(err) => Err((
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("Something went wrong: {err}"),
|
||||
)),
|
||||
}
|
||||
}
|
||||
19
examples/pavex_demo/todo_app_sqlite_pavex/src/leptos.rs
Normal file
19
examples/pavex_demo/todo_app_sqlite_pavex/src/leptos.rs
Normal file
@@ -0,0 +1,19 @@
|
||||
use leptos::{get_configuration, leptos_config::ConfFile};
|
||||
use leptos_pavex::generate_route_list;
|
||||
use leptos_router::RouteListing;
|
||||
use pavex::{
|
||||
http::header::{ToStrError, USER_AGENT},
|
||||
request::RequestHead,
|
||||
response::Response,
|
||||
};
|
||||
|
||||
/// Easier to do this to avoid having to register things with Blueprints
|
||||
/// Provide LeptosOptions via env vars provided by cargo-leptos or the user
|
||||
pub fn get_cargo_leptos_conf() -> ConfFile {
|
||||
get_configuration(None)
|
||||
}
|
||||
|
||||
/// Generate all possible non server fn routes for our app
|
||||
pub fn get_app_route_listing() -> Vec<RouteListing> {
|
||||
generate_route_list(TodoApp)
|
||||
}
|
||||
7
examples/pavex_demo/todo_app_sqlite_pavex/src/lib.rs
Normal file
7
examples/pavex_demo/todo_app_sqlite_pavex/src/lib.rs
Normal file
@@ -0,0 +1,7 @@
|
||||
mod blueprint;
|
||||
pub mod configuration;
|
||||
pub mod leptos;
|
||||
pub mod routes;
|
||||
pub mod telemetry;
|
||||
pub mod user_agent;
|
||||
pub use blueprint::blueprint;
|
||||
@@ -0,0 +1,21 @@
|
||||
use crate::user_agent::UserAgent;
|
||||
use pavex::{request::route::RouteParams, response::Response};
|
||||
|
||||
#[RouteParams]
|
||||
pub struct GreetParams {
|
||||
pub name: String,
|
||||
}
|
||||
pub fn greet(
|
||||
params: RouteParams<GreetParams>,
|
||||
user_agent: UserAgent,
|
||||
) -> Response {
|
||||
if let UserAgent::Unknown = user_agent {
|
||||
return Response::unauthorized()
|
||||
.set_typed_body("You must provide a `User-Agent` header")
|
||||
.box_body();
|
||||
}
|
||||
let GreetParams { name } = params.0;
|
||||
Response::ok()
|
||||
.set_typed_body(format!("Hello, {name}!"))
|
||||
.box_body()
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
pub mod greet;
|
||||
pub mod status;
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
use pavex::http::StatusCode;
|
||||
|
||||
/// Respond with a `200 OK` status code to indicate that the server is alive
|
||||
/// and ready to accept new requests.
|
||||
pub fn ping() -> StatusCode {
|
||||
StatusCode::OK
|
||||
}
|
||||
84
examples/pavex_demo/todo_app_sqlite_pavex/src/telemetry.rs
Normal file
84
examples/pavex_demo/todo_app_sqlite_pavex/src/telemetry.rs
Normal file
@@ -0,0 +1,84 @@
|
||||
use pavex::request::route::MatchedRouteTemplate;
|
||||
use pavex::http::Version;
|
||||
use pavex::middleware::Next;
|
||||
use pavex::request::RequestHead;
|
||||
use pavex::response::Response;
|
||||
use std::borrow::Cow;
|
||||
use std::future::IntoFuture;
|
||||
use tracing::Instrument;
|
||||
|
||||
/// A logging middleware that wraps the request pipeline in the root span.
|
||||
/// It takes care to record key information about the request and the response.
|
||||
pub async fn logger<T>(next: Next<T>, root_span: RootSpan) -> Response
|
||||
where
|
||||
T: IntoFuture<Output = Response>,
|
||||
{
|
||||
let response = next
|
||||
.into_future()
|
||||
.instrument(root_span.clone().into_inner())
|
||||
.await;
|
||||
root_span.record_response_data(&response);
|
||||
response
|
||||
}
|
||||
|
||||
/// A root span is the top-level *logical* span for an incoming request.
|
||||
///
|
||||
/// It is not necessarily the top-level *physical* span, as it may be a child of
|
||||
/// another span (e.g. a span representing the underlying HTTP connection).
|
||||
///
|
||||
/// We use the root span to attach as much information as possible about the
|
||||
/// incoming request, and to record the final outcome of the request (success or
|
||||
/// failure).
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct RootSpan(tracing::Span);
|
||||
|
||||
impl RootSpan {
|
||||
/// Create a new root span for the given request.
|
||||
///
|
||||
/// We follow OpenTelemetry's HTTP semantic conventions as closely as
|
||||
/// possible for field naming.
|
||||
pub fn new(request_head: &RequestHead, matched_route: MatchedRouteTemplate) -> Self {
|
||||
let user_agent = request_head
|
||||
.headers
|
||||
.get("User-Agent")
|
||||
.map(|h| h.to_str().unwrap_or_default())
|
||||
.unwrap_or_default();
|
||||
|
||||
let span = tracing::info_span!(
|
||||
"HTTP request",
|
||||
http.method = %request_head.method,
|
||||
http.flavor = %http_flavor(request_head.version),
|
||||
user_agent.original = %user_agent,
|
||||
http.response.status_code = tracing::field::Empty,
|
||||
http.route = %matched_route,
|
||||
http.target = %request_head.uri.path_and_query().map(|p| p.as_str()).unwrap_or(""),
|
||||
);
|
||||
Self(span)
|
||||
}
|
||||
|
||||
pub fn record_response_data(&self, response: &Response) {
|
||||
self.0
|
||||
.record("http.response.status_code", &response.status().as_u16());
|
||||
}
|
||||
|
||||
/// Get a reference to the underlying [`tracing::Span`].
|
||||
pub fn inner(&self) -> &tracing::Span {
|
||||
&self.0
|
||||
}
|
||||
|
||||
/// Deconstruct the root span into its underlying [`tracing::Span`].
|
||||
pub fn into_inner(self) -> tracing::Span {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
fn http_flavor(version: Version) -> Cow<'static, str> {
|
||||
match version {
|
||||
Version::HTTP_09 => "0.9".into(),
|
||||
Version::HTTP_10 => "1.0".into(),
|
||||
Version::HTTP_11 => "1.1".into(),
|
||||
Version::HTTP_2 => "2.0".into(),
|
||||
Version::HTTP_3 => "3.0".into(),
|
||||
other => format!("{other:?}").into(),
|
||||
}
|
||||
}
|
||||
27
examples/pavex_demo/todo_app_sqlite_pavex/src/user_agent.rs
Normal file
27
examples/pavex_demo/todo_app_sqlite_pavex/src/user_agent.rs
Normal file
@@ -0,0 +1,27 @@
|
||||
use pavex::{
|
||||
http::header::{ToStrError, USER_AGENT},
|
||||
request::RequestHead,
|
||||
response::Response,
|
||||
};
|
||||
|
||||
pub enum UserAgent {
|
||||
/// No User-Agent header was provided
|
||||
Unknown,
|
||||
/// The value of the 'User-Agent' header for the incoming request
|
||||
Known(String),
|
||||
}
|
||||
impl UserAgent {
|
||||
pub fn extract(request_head: &RequestHead) -> Result<Self, ToStrError> {
|
||||
let Some(user_agent) = request_head.headers.get(USER_AGENT) else {
|
||||
return Ok(UserAgent::Unknown);
|
||||
};
|
||||
|
||||
user_agent.to_str().map(|s| UserAgent::Known(s.into()))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn invalid_user_agent(_e: &ToStrError) -> Response {
|
||||
Response::bad_request()
|
||||
.set_typed_body("The `User-Agent` header must be a valid UTF-8 string")
|
||||
.box_body()
|
||||
}
|
||||
29
examples/pavex_demo/todo_app_sqlite_pavex_server/Cargo.toml
Normal file
29
examples/pavex_demo/todo_app_sqlite_pavex_server/Cargo.toml
Normal file
@@ -0,0 +1,29 @@
|
||||
[package]
|
||||
name = "todo_app_sqlite_pavex_server"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[[bin]]
|
||||
path = "src/bin/api.rs"
|
||||
name = "api"
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1"
|
||||
pavex = { git = "https://github.com/LukeMathWalker/pavex", branch = "main" }
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
todo_app_sqlite_pavex_server_sdk = { path = "../todo_app_sqlite_pavex_server_sdk" }
|
||||
todo_app_sqlite_pavex = { path = "../todo_app_sqlite_pavex" }
|
||||
|
||||
# Configuration
|
||||
dotenvy = "0.15"
|
||||
figment = { version = "0.10", features = ["env", "yaml"] }
|
||||
serde = { version = "1", features = ["derive"]}
|
||||
|
||||
# Telemetry
|
||||
tracing = "0.1"
|
||||
tracing-bunyan-formatter = "0.3"
|
||||
tracing-panic = "0.1"
|
||||
tracing-subscriber = { version = "0.3", default-features = false, features = ["env-filter", "registry", "smallvec", "std", "tracing-log"] }
|
||||
|
||||
[dev-dependencies]
|
||||
reqwest = "0.11"
|
||||
@@ -0,0 +1,3 @@
|
||||
server:
|
||||
ip: "0.0.0.0"
|
||||
port: 8000
|
||||
@@ -0,0 +1,6 @@
|
||||
# This file contains the configuration for the dev environment.
|
||||
# None of the values here are actually secret, so it's fine
|
||||
# to commit this file to the repository.
|
||||
server:
|
||||
ip: "127.0.0.1"
|
||||
port: 8000
|
||||
@@ -0,0 +1,3 @@
|
||||
server:
|
||||
ip: "0.0.0.0"
|
||||
port: 8000
|
||||
@@ -0,0 +1,8 @@
|
||||
# This file contains the configuration for the API when spawned
|
||||
# in black-box tests.
|
||||
# None of the values here are actually secret, so it's fine
|
||||
# to commit this file to the repository.
|
||||
server:
|
||||
ip: "127.0.0.1"
|
||||
# The OS will assign a random port to the test server.
|
||||
port: 0
|
||||
@@ -0,0 +1,49 @@
|
||||
use anyhow::Context;
|
||||
use todo_app_sqlite_pavex_server::{
|
||||
configuration::load_configuration,
|
||||
telemetry::{get_subscriber, init_telemetry},
|
||||
};
|
||||
use todo_app_sqlite_pavex_server_sdk::{build_application_state, run};
|
||||
use pavex::server::Server;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> anyhow::Result<()> {
|
||||
let subscriber = get_subscriber("todo_app_sqlite_pavex".into(), "info".into(), std::io::stdout);
|
||||
init_telemetry(subscriber)?;
|
||||
|
||||
// We isolate all the server setup and launch logic in a separate function
|
||||
// in order to have a single choke point where we make sure to log fatal errors
|
||||
// that will cause the application to exit.
|
||||
if let Err(e) = _main().await {
|
||||
tracing::error!(
|
||||
error.msg = %e,
|
||||
error.error_chain = ?e,
|
||||
"The application is exiting due to an error"
|
||||
)
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn _main() -> anyhow::Result<()> {
|
||||
// Load environment variables from a .env file, if it exists.
|
||||
let _ = dotenvy::dotenv();
|
||||
|
||||
let config = load_configuration(None)?;
|
||||
let application_state = build_application_state()
|
||||
.await;
|
||||
|
||||
let tcp_listener = config
|
||||
.server
|
||||
.listener()
|
||||
.await
|
||||
.context("Failed to bind the server TCP listener")?;
|
||||
let address = tcp_listener
|
||||
.local_addr()
|
||||
.context("The server TCP listener doesn't have a local socket address")?;
|
||||
let server_builder = Server::new().listen(tcp_listener);
|
||||
|
||||
tracing::info!("Starting to listen for incoming requests at {}", address);
|
||||
run(server_builder, application_state).await;
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
use std::env::VarError;
|
||||
|
||||
use anyhow::Context;
|
||||
use todo_app_sqlite_pavex::configuration::Config;
|
||||
use figment::{
|
||||
providers::{Env, Format, Yaml},
|
||||
Figment,
|
||||
};
|
||||
|
||||
/// Retrieve the application configuration by merging together multiple configuration sources.
|
||||
///
|
||||
/// # Application profiles
|
||||
///
|
||||
/// We use the concept of application profiles to allow for
|
||||
/// different configuration values depending on the type of environment
|
||||
/// the application is running in.
|
||||
///
|
||||
/// We don't rely on `figment`'s built-in support for profiles because
|
||||
/// we want to make sure that values for different profiles are not co-located in
|
||||
/// the same configuration file.
|
||||
/// This makes it easier to avoid leaking sensitive information by mistake (e.g.
|
||||
/// by committing configuration values for the `dev` profile to the repository).
|
||||
///
|
||||
/// You primary mechanism to specify the desired application profile is the `APP_PROFILE`
|
||||
/// environment variable.
|
||||
/// You can pass a `default_profile` value that will be used if the environment variable
|
||||
/// is not set.
|
||||
///
|
||||
/// # Hierarchy
|
||||
///
|
||||
/// The configuration sources are:
|
||||
///
|
||||
/// 1. `base.yml` - Contains the default configuration values, common to all profiles.
|
||||
/// 2. `<profile>.yml` - Contains the configuration values specific to the desired profile.
|
||||
/// 3. Environment variables - Contains the configuration values specific to the current environment.
|
||||
///
|
||||
/// The configuration sources are listed in priority order, i.e.
|
||||
/// the last source in the list will override any previous source.
|
||||
///
|
||||
/// For example, if the same configuration key is defined in both
|
||||
/// the YAML file and the environment, the value from the environment
|
||||
/// will be used.
|
||||
pub fn load_configuration(
|
||||
default_profile: Option<ApplicationProfile>,
|
||||
) -> Result<Config, anyhow::Error> {
|
||||
let application_profile = load_app_profile(default_profile)
|
||||
.context("Failed to load the desired application profile")?;
|
||||
|
||||
let configuration_dir = {
|
||||
let manifest_dir = env!(
|
||||
"CARGO_MANIFEST_DIR",
|
||||
"`CARGO_MANIFEST_DIR` was not set. Are you using a custom build system?"
|
||||
);
|
||||
std::path::Path::new(manifest_dir).join("configuration")
|
||||
};
|
||||
|
||||
let base_filepath = configuration_dir.join("base.yml");
|
||||
|
||||
let profile_filename = format!("{}.yml", application_profile.as_str());
|
||||
let profile_filepath = configuration_dir.join(profile_filename);
|
||||
|
||||
let figment = Figment::new()
|
||||
.merge(Yaml::file(base_filepath))
|
||||
.merge(Yaml::file(profile_filepath))
|
||||
.merge(Env::prefixed("APP_"));
|
||||
|
||||
let configuration: Config = figment
|
||||
.extract()
|
||||
.context("Failed to load hierarchical configuration")?;
|
||||
Ok(configuration)
|
||||
}
|
||||
|
||||
/// Load the application profile from the `APP_PROFILE` environment variable.
|
||||
fn load_app_profile(
|
||||
default_profile: Option<ApplicationProfile>,
|
||||
) -> Result<ApplicationProfile, anyhow::Error> {
|
||||
static PROFILE_ENV_VAR: &str = "APP_PROFILE";
|
||||
|
||||
match std::env::var(PROFILE_ENV_VAR) {
|
||||
Ok(raw_value) => raw_value.parse().with_context(|| {
|
||||
format!("Failed to parse the `{PROFILE_ENV_VAR}` environment variable")
|
||||
}),
|
||||
Err(VarError::NotPresent) if default_profile.is_some() => Ok(default_profile.unwrap()),
|
||||
Err(e) => Err(anyhow::anyhow!(e).context(format!(
|
||||
"Failed to read the `{PROFILE_ENV_VAR}` environment variable"
|
||||
))),
|
||||
}
|
||||
}
|
||||
|
||||
/// The application profile, i.e. the type of environment the application is running in.
|
||||
/// See [`load_configuration`] for more details.
|
||||
pub enum ApplicationProfile {
|
||||
/// Test profile.
|
||||
///
|
||||
/// This is the profile used by the integration test suite.
|
||||
Test,
|
||||
/// Local development profile.
|
||||
///
|
||||
/// This is the profile you should use when running the application locally
|
||||
/// for exploratory testing.
|
||||
///
|
||||
/// The corresponding configuration file is `dev.yml` and it's *never* committed to the repository.
|
||||
Dev,
|
||||
/// Production profile.
|
||||
///
|
||||
/// This is the profile you should use when running the application in production—e.g.
|
||||
/// when deploying it to a staging or production environment, exposed to live traffic.
|
||||
///
|
||||
/// The corresponding configuration file is `prod.yml`.
|
||||
/// It's committed to the repository, but it's meant to contain exclusively
|
||||
/// non-sensitive configuration values.
|
||||
Prod,
|
||||
}
|
||||
|
||||
impl ApplicationProfile {
|
||||
/// Return the environment as a string.
|
||||
pub fn as_str(&self) -> &'static str {
|
||||
match self {
|
||||
ApplicationProfile::Test => "test",
|
||||
ApplicationProfile::Dev => "dev",
|
||||
ApplicationProfile::Prod => "prod",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::str::FromStr for ApplicationProfile {
|
||||
type Err = anyhow::Error;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
match s.to_lowercase().as_str() {
|
||||
"test" => Ok(ApplicationProfile::Test),
|
||||
"dev" | "development" => Ok(ApplicationProfile::Dev),
|
||||
"prod" | "production" => Ok(ApplicationProfile::Prod),
|
||||
s => Err(anyhow::anyhow!(
|
||||
"`{}` is not a valid application profile.\nValid options are: `test`, `dev`, `prod`.",
|
||||
s
|
||||
)),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
pub mod configuration;
|
||||
pub mod telemetry;
|
||||
@@ -0,0 +1,40 @@
|
||||
use anyhow::Context;
|
||||
use tracing::subscriber::set_global_default;
|
||||
use tracing::Subscriber;
|
||||
use tracing_bunyan_formatter::{BunyanFormattingLayer, JsonStorageLayer};
|
||||
use tracing_subscriber::fmt::MakeWriter;
|
||||
use tracing_subscriber::{layer::SubscriberExt, EnvFilter, Registry};
|
||||
|
||||
/// Perform all the required setup steps for our telemetry:
|
||||
///
|
||||
/// - Register a subscriber as global default to process span data
|
||||
/// - Register a panic hook to capture any panic and record its details
|
||||
///
|
||||
/// It should only be called once!
|
||||
pub fn init_telemetry(subscriber: impl Subscriber + Sync + Send) -> Result<(), anyhow::Error> {
|
||||
std::panic::set_hook(Box::new(tracing_panic::panic_hook));
|
||||
set_global_default(subscriber).context("Failed to set a `tracing` global subscriber")
|
||||
}
|
||||
|
||||
/// Compose multiple layers into a `tracing`'s subscriber.
|
||||
///
|
||||
/// # Implementation Notes
|
||||
///
|
||||
/// We are using `impl Subscriber` as return type to avoid having to spell out the actual
|
||||
/// type of the returned subscriber, which is indeed quite complex.
|
||||
pub fn get_subscriber<Sink>(
|
||||
application_name: String,
|
||||
default_env_filter: String,
|
||||
sink: Sink,
|
||||
) -> impl Subscriber + Sync + Send
|
||||
where
|
||||
Sink: for<'a> MakeWriter<'a> + Send + Sync + 'static,
|
||||
{
|
||||
let env_filter =
|
||||
EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new(default_env_filter));
|
||||
let formatting_layer = BunyanFormattingLayer::new(application_name, sink);
|
||||
Registry::default()
|
||||
.with(env_filter)
|
||||
.with(JsonStorageLayer)
|
||||
.with(formatting_layer)
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
use crate::helpers::TestApi;
|
||||
use pavex::http::StatusCode;
|
||||
|
||||
#[tokio::test]
|
||||
async fn greet_happy_path() {
|
||||
let api = TestApi::spawn().await;
|
||||
let name = "Ursula";
|
||||
|
||||
let response = api
|
||||
.api_client
|
||||
.get(&format!("{}/api/greet/{name}", &api.api_address))
|
||||
.header("User-Agent", "Test runner")
|
||||
.send()
|
||||
.await
|
||||
.expect("Failed to execute request.");
|
||||
assert_eq!(response.status().as_u16(), StatusCode::OK.as_u16());
|
||||
assert_eq!(response.text().await.unwrap(), "Hello, Ursula!");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn non_utf8_agent_is_rejected() {
|
||||
let api = TestApi::spawn().await;
|
||||
let name = "Ursula";
|
||||
|
||||
let response = api
|
||||
.api_client
|
||||
.get(&format!("{}/api/greet/{name}", &api.api_address))
|
||||
.header("User-Agent", b"hello\xfa".as_slice())
|
||||
.send()
|
||||
.await
|
||||
.expect("Failed to execute request.");
|
||||
assert_eq!(response.status().as_u16(), StatusCode::BAD_REQUEST.as_u16());
|
||||
assert_eq!(
|
||||
response.text().await.unwrap(),
|
||||
"The `User-Agent` header must be a valid UTF-8 string"
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
use todo_app_sqlite_pavex_server::configuration::{load_configuration, ApplicationProfile};
|
||||
use todo_app_sqlite_pavex_server_sdk::{build_application_state, run};
|
||||
use todo_app_sqlite_pavex::configuration::Config;
|
||||
use pavex::server::Server;
|
||||
|
||||
pub struct TestApi {
|
||||
pub api_address: String,
|
||||
pub api_client: reqwest::Client,
|
||||
}
|
||||
|
||||
impl TestApi {
|
||||
pub async fn spawn() -> Self {
|
||||
let config = Self::get_config();
|
||||
|
||||
let application_state = build_application_state().await;
|
||||
|
||||
let tcp_listener = config
|
||||
.server
|
||||
.listener()
|
||||
.await
|
||||
.expect("Failed to bind the server TCP listener");
|
||||
let address = tcp_listener
|
||||
.local_addr()
|
||||
.expect("The server TCP listener doesn't have a local socket address");
|
||||
let server_builder = Server::new().listen(tcp_listener);
|
||||
|
||||
tokio::spawn(async move {
|
||||
run(server_builder, application_state).await
|
||||
});
|
||||
|
||||
TestApi {
|
||||
api_address: format!("http://{}:{}", config.server.ip, address.port()),
|
||||
api_client: reqwest::Client::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn get_config() -> Config {
|
||||
load_configuration(Some(ApplicationProfile::Test)).expect("Failed to load test configuration")
|
||||
}
|
||||
}
|
||||
|
||||
/// Convenient methods for calling the API under test.
|
||||
impl TestApi {
|
||||
pub async fn get_ping(&self) -> reqwest::Response
|
||||
{
|
||||
self.api_client
|
||||
.get(&format!("{}/api/ping", &self.api_address))
|
||||
.send()
|
||||
.await
|
||||
.expect("Failed to execute request.")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
mod greet;
|
||||
mod helpers;
|
||||
mod ping;
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
use crate::helpers::TestApi;
|
||||
use pavex::http::StatusCode;
|
||||
|
||||
#[tokio::test]
|
||||
async fn ping_works() {
|
||||
let api = TestApi::spawn().await;
|
||||
|
||||
let response = api.get_ping().await;
|
||||
|
||||
assert_eq!(response.status().as_u16(), StatusCode::OK.as_u16());
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
[package]
|
||||
name = "todo_app_sqlite_pavex_server_sdk"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[package.metadata.px.generate]
|
||||
generator_type = "cargo_workspace_binary"
|
||||
generator_name = "bp"
|
||||
|
||||
[lints]
|
||||
clippy = { all = "allow" }
|
||||
|
||||
[dependencies]
|
||||
bytes = { version = "1.5.0", package = "bytes" }
|
||||
http = { version = "1.0.0", package = "http" }
|
||||
http_body_util = { version = "0.1.0", package = "http-body-util" }
|
||||
hyper = { version = "1.1.0", package = "hyper" }
|
||||
matchit = { version = "0.7.3", git = "https://github.com/ibraheemdev/matchit", branch = "master", package = "matchit" }
|
||||
pavex = { version = "0.1.0", git = "https://github.com/LukeMathWalker/pavex", branch = "main", package = "pavex" }
|
||||
thiserror = { version = "1.0.52", package = "thiserror" }
|
||||
todo_app_sqlite_pavex = { version = "0.1.0", path = "../todo_app_sqlite_pavex", package = "todo_app_sqlite_pavex" }
|
||||
@@ -0,0 +1,233 @@
|
||||
(
|
||||
creation_location: (
|
||||
line: 13,
|
||||
column: 18,
|
||||
file: "todo_app_sqlite_pavex/src/blueprint.rs",
|
||||
),
|
||||
constructors: [
|
||||
(
|
||||
constructor: (
|
||||
callable: (
|
||||
registered_at: "todo_app_sqlite_pavex",
|
||||
import_path: "pavex::request::query::QueryParams::extract",
|
||||
),
|
||||
location: (
|
||||
line: 32,
|
||||
column: 8,
|
||||
file: "todo_app_sqlite_pavex/src/blueprint.rs",
|
||||
),
|
||||
),
|
||||
lifecycle: RequestScoped,
|
||||
cloning_strategy: None,
|
||||
error_handler: Some((
|
||||
callable: (
|
||||
registered_at: "todo_app_sqlite_pavex",
|
||||
import_path: "pavex::request::query::errors::ExtractQueryParamsError::into_response",
|
||||
),
|
||||
location: (
|
||||
line: 36,
|
||||
column: 6,
|
||||
file: "todo_app_sqlite_pavex/src/blueprint.rs",
|
||||
),
|
||||
)),
|
||||
),
|
||||
(
|
||||
constructor: (
|
||||
callable: (
|
||||
registered_at: "todo_app_sqlite_pavex",
|
||||
import_path: "pavex::request::route::RouteParams::extract",
|
||||
),
|
||||
location: (
|
||||
line: 41,
|
||||
column: 8,
|
||||
file: "todo_app_sqlite_pavex/src/blueprint.rs",
|
||||
),
|
||||
),
|
||||
lifecycle: RequestScoped,
|
||||
cloning_strategy: None,
|
||||
error_handler: Some((
|
||||
callable: (
|
||||
registered_at: "todo_app_sqlite_pavex",
|
||||
import_path: "pavex::request::route::errors::ExtractRouteParamsError::into_response",
|
||||
),
|
||||
location: (
|
||||
line: 45,
|
||||
column: 6,
|
||||
file: "todo_app_sqlite_pavex/src/blueprint.rs",
|
||||
),
|
||||
)),
|
||||
),
|
||||
(
|
||||
constructor: (
|
||||
callable: (
|
||||
registered_at: "todo_app_sqlite_pavex",
|
||||
import_path: "pavex::request::body::JsonBody::extract",
|
||||
),
|
||||
location: (
|
||||
line: 50,
|
||||
column: 8,
|
||||
file: "todo_app_sqlite_pavex/src/blueprint.rs",
|
||||
),
|
||||
),
|
||||
lifecycle: RequestScoped,
|
||||
cloning_strategy: None,
|
||||
error_handler: Some((
|
||||
callable: (
|
||||
registered_at: "todo_app_sqlite_pavex",
|
||||
import_path: "pavex::request::body::errors::ExtractJsonBodyError::into_response",
|
||||
),
|
||||
location: (
|
||||
line: 54,
|
||||
column: 6,
|
||||
file: "todo_app_sqlite_pavex/src/blueprint.rs",
|
||||
),
|
||||
)),
|
||||
),
|
||||
(
|
||||
constructor: (
|
||||
callable: (
|
||||
registered_at: "todo_app_sqlite_pavex",
|
||||
import_path: "pavex::request::body::BufferedBody::extract",
|
||||
),
|
||||
location: (
|
||||
line: 57,
|
||||
column: 8,
|
||||
file: "todo_app_sqlite_pavex/src/blueprint.rs",
|
||||
),
|
||||
),
|
||||
lifecycle: RequestScoped,
|
||||
cloning_strategy: None,
|
||||
error_handler: Some((
|
||||
callable: (
|
||||
registered_at: "todo_app_sqlite_pavex",
|
||||
import_path: "pavex::request::body::errors::ExtractBufferedBodyError::into_response",
|
||||
),
|
||||
location: (
|
||||
line: 61,
|
||||
column: 6,
|
||||
file: "todo_app_sqlite_pavex/src/blueprint.rs",
|
||||
),
|
||||
)),
|
||||
),
|
||||
(
|
||||
constructor: (
|
||||
callable: (
|
||||
registered_at: "todo_app_sqlite_pavex",
|
||||
import_path: "<pavex::request::body::BodySizeLimit as std::default::Default>::default",
|
||||
),
|
||||
location: (
|
||||
line: 64,
|
||||
column: 8,
|
||||
file: "todo_app_sqlite_pavex/src/blueprint.rs",
|
||||
),
|
||||
),
|
||||
lifecycle: RequestScoped,
|
||||
cloning_strategy: None,
|
||||
error_handler: None,
|
||||
),
|
||||
(
|
||||
constructor: (
|
||||
callable: (
|
||||
registered_at: "todo_app_sqlite_pavex",
|
||||
import_path: "crate::user_agent::UserAgent::extract",
|
||||
),
|
||||
location: (
|
||||
line: 16,
|
||||
column: 8,
|
||||
file: "todo_app_sqlite_pavex/src/blueprint.rs",
|
||||
),
|
||||
),
|
||||
lifecycle: RequestScoped,
|
||||
cloning_strategy: None,
|
||||
error_handler: Some((
|
||||
callable: (
|
||||
registered_at: "todo_app_sqlite_pavex",
|
||||
import_path: "crate::user_agent::invalid_user_agent",
|
||||
),
|
||||
location: (
|
||||
line: 20,
|
||||
column: 6,
|
||||
file: "todo_app_sqlite_pavex/src/blueprint.rs",
|
||||
),
|
||||
)),
|
||||
),
|
||||
(
|
||||
constructor: (
|
||||
callable: (
|
||||
registered_at: "todo_app_sqlite_pavex",
|
||||
import_path: "crate::telemetry::RootSpan::new",
|
||||
),
|
||||
location: (
|
||||
line: 72,
|
||||
column: 8,
|
||||
file: "todo_app_sqlite_pavex/src/blueprint.rs",
|
||||
),
|
||||
),
|
||||
lifecycle: RequestScoped,
|
||||
cloning_strategy: Some(CloneIfNecessary),
|
||||
error_handler: None,
|
||||
),
|
||||
],
|
||||
middlewares: [
|
||||
(
|
||||
middleware: (
|
||||
callable: (
|
||||
registered_at: "todo_app_sqlite_pavex",
|
||||
import_path: "crate::telemetry::logger",
|
||||
),
|
||||
location: (
|
||||
line: 78,
|
||||
column: 8,
|
||||
file: "todo_app_sqlite_pavex/src/blueprint.rs",
|
||||
),
|
||||
),
|
||||
error_handler: None,
|
||||
),
|
||||
],
|
||||
routes: [
|
||||
(
|
||||
path: "/api/ping",
|
||||
method_guard: (
|
||||
inner: Some((
|
||||
bitset: 256,
|
||||
extensions: [],
|
||||
)),
|
||||
),
|
||||
request_handler: (
|
||||
callable: (
|
||||
registered_at: "todo_app_sqlite_pavex",
|
||||
import_path: "crate::routes::status::ping",
|
||||
),
|
||||
location: (
|
||||
line: 24,
|
||||
column: 8,
|
||||
file: "todo_app_sqlite_pavex/src/blueprint.rs",
|
||||
),
|
||||
),
|
||||
error_handler: None,
|
||||
),
|
||||
(
|
||||
path: "/api/greet/:name",
|
||||
method_guard: (
|
||||
inner: Some((
|
||||
bitset: 256,
|
||||
extensions: [],
|
||||
)),
|
||||
),
|
||||
request_handler: (
|
||||
callable: (
|
||||
registered_at: "todo_app_sqlite_pavex",
|
||||
import_path: "crate::routes::greet::greet",
|
||||
),
|
||||
location: (
|
||||
line: 25,
|
||||
column: 8,
|
||||
file: "todo_app_sqlite_pavex/src/blueprint.rs",
|
||||
),
|
||||
),
|
||||
error_handler: None,
|
||||
),
|
||||
],
|
||||
fallback_request_handler: None,
|
||||
nested_blueprints: [],
|
||||
)
|
||||
254
examples/pavex_demo/todo_app_sqlite_pavex_server_sdk/src/lib.rs
Normal file
254
examples/pavex_demo/todo_app_sqlite_pavex_server_sdk/src/lib.rs
Normal file
@@ -0,0 +1,254 @@
|
||||
//! Do NOT edit this code.
|
||||
//! It was automatically generated by Pavex.
|
||||
//! All manual edits will be lost next time the code is generated.
|
||||
extern crate alloc;
|
||||
struct ServerState {
|
||||
router: matchit::Router<u32>,
|
||||
#[allow(dead_code)]
|
||||
application_state: ApplicationState,
|
||||
}
|
||||
pub struct ApplicationState {}
|
||||
pub async fn build_application_state() -> crate::ApplicationState {
|
||||
crate::ApplicationState {}
|
||||
}
|
||||
pub fn run(
|
||||
server_builder: pavex::server::Server,
|
||||
application_state: ApplicationState,
|
||||
) -> pavex::server::ServerHandle {
|
||||
let server_state = std::sync::Arc::new(ServerState {
|
||||
router: build_router(),
|
||||
application_state,
|
||||
});
|
||||
server_builder.serve(route_request, server_state)
|
||||
}
|
||||
fn build_router() -> matchit::Router<u32> {
|
||||
let mut router = matchit::Router::new();
|
||||
router.insert("/api/greet/:name", 0u32).unwrap();
|
||||
router.insert("/api/ping", 1u32).unwrap();
|
||||
router
|
||||
}
|
||||
async fn route_request(
|
||||
request: http::Request<hyper::body::Incoming>,
|
||||
server_state: std::sync::Arc<ServerState>,
|
||||
) -> pavex::response::Response {
|
||||
let (request_head, request_body) = request.into_parts();
|
||||
#[allow(unused)]
|
||||
let request_body = pavex::request::body::RawIncomingBody::from(request_body);
|
||||
let request_head: pavex::request::RequestHead = request_head.into();
|
||||
let matched_route = match server_state.router.at(&request_head.uri.path()) {
|
||||
Ok(m) => m,
|
||||
Err(_) => {
|
||||
let allowed_methods: pavex::router::AllowedMethods = pavex::router::MethodAllowList::from_iter(
|
||||
vec![],
|
||||
)
|
||||
.into();
|
||||
let matched_route_template = pavex::request::route::MatchedRouteTemplate::new(
|
||||
"*",
|
||||
);
|
||||
return route_2::middleware_0(
|
||||
matched_route_template,
|
||||
&allowed_methods,
|
||||
&request_head,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
};
|
||||
let route_id = matched_route.value;
|
||||
#[allow(unused)]
|
||||
let url_params: pavex::request::route::RawRouteParams<'_, '_> = matched_route
|
||||
.params
|
||||
.into();
|
||||
match route_id {
|
||||
0u32 => {
|
||||
let matched_route_template = pavex::request::route::MatchedRouteTemplate::new(
|
||||
"/api/greet/:name",
|
||||
);
|
||||
match &request_head.method {
|
||||
&pavex::http::Method::GET => {
|
||||
route_1::middleware_0(
|
||||
matched_route_template,
|
||||
url_params,
|
||||
&request_head,
|
||||
)
|
||||
.await
|
||||
}
|
||||
_ => {
|
||||
let allowed_methods: pavex::router::AllowedMethods = pavex::router::MethodAllowList::from_iter([
|
||||
pavex::http::Method::GET,
|
||||
])
|
||||
.into();
|
||||
route_2::middleware_0(
|
||||
matched_route_template,
|
||||
&allowed_methods,
|
||||
&request_head,
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
}
|
||||
1u32 => {
|
||||
let matched_route_template = pavex::request::route::MatchedRouteTemplate::new(
|
||||
"/api/ping",
|
||||
);
|
||||
match &request_head.method {
|
||||
&pavex::http::Method::GET => {
|
||||
route_0::middleware_0(matched_route_template, &request_head).await
|
||||
}
|
||||
_ => {
|
||||
let allowed_methods: pavex::router::AllowedMethods = pavex::router::MethodAllowList::from_iter([
|
||||
pavex::http::Method::GET,
|
||||
])
|
||||
.into();
|
||||
route_2::middleware_0(
|
||||
matched_route_template,
|
||||
&allowed_methods,
|
||||
&request_head,
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
}
|
||||
i => unreachable!("Unknown route id: {}", i),
|
||||
}
|
||||
}
|
||||
pub mod route_0 {
|
||||
pub async fn middleware_0(
|
||||
v0: pavex::request::route::MatchedRouteTemplate,
|
||||
v1: &pavex::request::RequestHead,
|
||||
) -> pavex::response::Response {
|
||||
let v2 = todo_app_sqlite_pavex::telemetry::RootSpan::new(v1, v0);
|
||||
let v3 = crate::route_0::Next0 {
|
||||
next: handler,
|
||||
};
|
||||
let v4 = pavex::middleware::Next::new(v3);
|
||||
todo_app_sqlite_pavex::telemetry::logger(v4, v2).await
|
||||
}
|
||||
pub async fn handler() -> pavex::response::Response {
|
||||
let v0 = todo_app_sqlite_pavex::routes::status::ping();
|
||||
<http::StatusCode as pavex::response::IntoResponse>::into_response(v0)
|
||||
}
|
||||
pub struct Next0<T>
|
||||
where
|
||||
T: std::future::Future<Output = pavex::response::Response>,
|
||||
{
|
||||
next: fn() -> T,
|
||||
}
|
||||
impl<T> std::future::IntoFuture for Next0<T>
|
||||
where
|
||||
T: std::future::Future<Output = pavex::response::Response>,
|
||||
{
|
||||
type Output = pavex::response::Response;
|
||||
type IntoFuture = T;
|
||||
fn into_future(self) -> Self::IntoFuture {
|
||||
(self.next)()
|
||||
}
|
||||
}
|
||||
}
|
||||
pub mod route_1 {
|
||||
pub async fn middleware_0(
|
||||
v0: pavex::request::route::MatchedRouteTemplate,
|
||||
v1: pavex::request::route::RawRouteParams<'_, '_>,
|
||||
v2: &pavex::request::RequestHead,
|
||||
) -> pavex::response::Response {
|
||||
let v3 = todo_app_sqlite_pavex::telemetry::RootSpan::new(v2, v0);
|
||||
let v4 = crate::route_1::Next0 {
|
||||
s_0: v1,
|
||||
s_1: v2,
|
||||
next: handler,
|
||||
};
|
||||
let v5 = pavex::middleware::Next::new(v4);
|
||||
todo_app_sqlite_pavex::telemetry::logger(v5, v3).await
|
||||
}
|
||||
pub async fn handler(
|
||||
v0: pavex::request::route::RawRouteParams<'_, '_>,
|
||||
v1: &pavex::request::RequestHead,
|
||||
) -> pavex::response::Response {
|
||||
let v2 = todo_app_sqlite_pavex::user_agent::UserAgent::extract(v1);
|
||||
let v3 = match v2 {
|
||||
Ok(ok) => ok,
|
||||
Err(v3) => {
|
||||
return {
|
||||
let v4 = todo_app_sqlite_pavex::user_agent::invalid_user_agent(&v3);
|
||||
<pavex::response::Response as pavex::response::IntoResponse>::into_response(
|
||||
v4,
|
||||
)
|
||||
};
|
||||
}
|
||||
};
|
||||
let v4 = pavex::request::route::RouteParams::extract(v0);
|
||||
let v5 = match v4 {
|
||||
Ok(ok) => ok,
|
||||
Err(v5) => {
|
||||
return {
|
||||
let v6 = pavex::request::route::errors::ExtractRouteParamsError::into_response(
|
||||
&v5,
|
||||
);
|
||||
<pavex::response::Response<
|
||||
http_body_util::Full<bytes::Bytes>,
|
||||
> as pavex::response::IntoResponse>::into_response(v6)
|
||||
};
|
||||
}
|
||||
};
|
||||
let v6 = todo_app_sqlite_pavex::routes::greet::greet(v5, v3);
|
||||
<pavex::response::Response as pavex::response::IntoResponse>::into_response(v6)
|
||||
}
|
||||
pub struct Next0<'a, 'b, 'c, T>
|
||||
where
|
||||
T: std::future::Future<Output = pavex::response::Response>,
|
||||
{
|
||||
s_0: pavex::request::route::RawRouteParams<'a, 'b>,
|
||||
s_1: &'c pavex::request::RequestHead,
|
||||
next: fn(
|
||||
pavex::request::route::RawRouteParams<'a, 'b>,
|
||||
&'c pavex::request::RequestHead,
|
||||
) -> T,
|
||||
}
|
||||
impl<'a, 'b, 'c, T> std::future::IntoFuture for Next0<'a, 'b, 'c, T>
|
||||
where
|
||||
T: std::future::Future<Output = pavex::response::Response>,
|
||||
{
|
||||
type Output = pavex::response::Response;
|
||||
type IntoFuture = T;
|
||||
fn into_future(self) -> Self::IntoFuture {
|
||||
(self.next)(self.s_0, self.s_1)
|
||||
}
|
||||
}
|
||||
}
|
||||
pub mod route_2 {
|
||||
pub async fn middleware_0(
|
||||
v0: pavex::request::route::MatchedRouteTemplate,
|
||||
v1: &pavex::router::AllowedMethods,
|
||||
v2: &pavex::request::RequestHead,
|
||||
) -> pavex::response::Response {
|
||||
let v3 = todo_app_sqlite_pavex::telemetry::RootSpan::new(v2, v0);
|
||||
let v4 = crate::route_2::Next0 {
|
||||
s_0: v1,
|
||||
next: handler,
|
||||
};
|
||||
let v5 = pavex::middleware::Next::new(v4);
|
||||
todo_app_sqlite_pavex::telemetry::logger(v5, v3).await
|
||||
}
|
||||
pub async fn handler(
|
||||
v0: &pavex::router::AllowedMethods,
|
||||
) -> pavex::response::Response {
|
||||
let v1 = pavex::router::default_fallback(v0).await;
|
||||
<pavex::response::Response as pavex::response::IntoResponse>::into_response(v1)
|
||||
}
|
||||
pub struct Next0<'a, T>
|
||||
where
|
||||
T: std::future::Future<Output = pavex::response::Response>,
|
||||
{
|
||||
s_0: &'a pavex::router::AllowedMethods,
|
||||
next: fn(&'a pavex::router::AllowedMethods) -> T,
|
||||
}
|
||||
impl<'a, T> std::future::IntoFuture for Next0<'a, T>
|
||||
where
|
||||
T: std::future::Future<Output = pavex::response::Response>,
|
||||
{
|
||||
type Output = pavex::response::Response;
|
||||
type IntoFuture = T;
|
||||
fn into_future(self) -> Self::IntoFuture {
|
||||
(self.next)(self.s_0)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -45,6 +45,7 @@ ssr = [
|
||||
"dep:leptos_axum",
|
||||
"dep:notify"
|
||||
]
|
||||
notify = ["dep:notify"]
|
||||
|
||||
[package.metadata.cargo-all-features]
|
||||
denylist = ["axum", "tower", "tower-http", "tokio", "leptos_axum"]
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
extend = [
|
||||
{ path = "../cargo-make/main.toml" },
|
||||
{ path = "../cargo-make/cargo-leptos.toml" },
|
||||
{ path = "../cargo-make/cargo-leptos-webdriver-test.toml" },
|
||||
]
|
||||
|
||||
[env]
|
||||
CLIENT_PROCESS_NAME = "todo_app_sqlite_axum"
|
||||
|
||||
CLIENT_PROCESS_NAME = "server_fns_axum"
|
||||
[tasks.test-ui]
|
||||
cwd = "./e2e"
|
||||
command = "cargo"
|
||||
args = ["make", "test-ui", "${@}"]
|
||||
|
||||
18
examples/server_fns_axum/e2e/Cargo.toml
Normal file
18
examples/server_fns_axum/e2e/Cargo.toml
Normal file
@@ -0,0 +1,18 @@
|
||||
[package]
|
||||
name = "todo_app_sqlite_axum_e2e"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dev-dependencies]
|
||||
anyhow = "1.0.72"
|
||||
async-trait = "0.1.72"
|
||||
cucumber = "0.19.1"
|
||||
fantoccini = "0.19.3"
|
||||
pretty_assertions = "1.4.0"
|
||||
serde_json = "1.0.104"
|
||||
tokio = { version = "1.29.1", features = ["macros", "rt-multi-thread", "time"] }
|
||||
url = "2.4.0"
|
||||
|
||||
[[test]]
|
||||
name = "app_suite"
|
||||
harness = false # Allow Cucumber to print output instead of libtest
|
||||
20
examples/server_fns_axum/e2e/Makefile.toml
Normal file
20
examples/server_fns_axum/e2e/Makefile.toml
Normal file
@@ -0,0 +1,20 @@
|
||||
extend = { path = "../../cargo-make/main.toml" }
|
||||
|
||||
[tasks.test]
|
||||
env = { RUN_AUTOMATICALLY = false }
|
||||
condition = { env_true = ["RUN_AUTOMATICALLY"] }
|
||||
|
||||
[tasks.ci]
|
||||
|
||||
[tasks.test-ui]
|
||||
command = "cargo"
|
||||
args = [
|
||||
"test",
|
||||
"--test",
|
||||
"app_suite",
|
||||
"--",
|
||||
"--retry",
|
||||
"2",
|
||||
"--fail-fast",
|
||||
"${@}",
|
||||
]
|
||||
34
examples/server_fns_axum/e2e/README.md
Normal file
34
examples/server_fns_axum/e2e/README.md
Normal file
@@ -0,0 +1,34 @@
|
||||
# E2E Testing
|
||||
|
||||
This example demonstrates e2e testing with Rust using executable requirements.
|
||||
|
||||
## Testing Stack
|
||||
|
||||
| | Role | Description |
|
||||
|---|---|---|
|
||||
| [Cucumber](https://github.com/cucumber-rs/cucumber/tree/main) | Test Runner | Run [Gherkin](https://cucumber.io/docs/gherkin/reference/) specifications as Rust tests |
|
||||
| [Fantoccini](https://github.com/jonhoo/fantoccini/tree/main) | Browser Client | Interact with web pages through WebDriver |
|
||||
| [Cargo Leptos ](https://github.com/leptos-rs/cargo-leptos) | Build Tool | Compile example and start the server and end-2-end tests |
|
||||
| [chromedriver](https://chromedriver.chromium.org/downloads) | WebDriver | Provide WebDriver for Chrome
|
||||
|
||||
## Testing Organization
|
||||
|
||||
Testing is organized around what a user can do and see/not see. Test scenarios are grouped by the **user action** and the **object** of that action. This makes it easier to locate and reason about requirements.
|
||||
|
||||
Here is a brief overview of how things fit together.
|
||||
|
||||
```bash
|
||||
features
|
||||
└── {action}_{object}.feature # Specify test scenarios
|
||||
tests
|
||||
├── fixtures
|
||||
│ ├── action.rs # Perform a user action (click, type, etc.)
|
||||
│ ├── check.rs # Assert what a user can see/not see
|
||||
│ ├── find.rs # Query page elements
|
||||
│ ├── mod.rs
|
||||
│ └── world
|
||||
│ ├── action_steps.rs # Map Gherkin steps to user actions
|
||||
│ ├── check_steps.rs # Map Gherkin steps to user expectations
|
||||
│ └── mod.rs
|
||||
└── app_suite.rs # Test main
|
||||
```
|
||||
16
examples/server_fns_axum/e2e/features/add_todo.feature
Normal file
16
examples/server_fns_axum/e2e/features/add_todo.feature
Normal file
@@ -0,0 +1,16 @@
|
||||
@add_todo
|
||||
Feature: Add Todo
|
||||
|
||||
Background:
|
||||
Given I see the app
|
||||
|
||||
@add_todo-see
|
||||
Scenario: Should see the todo
|
||||
Given I set the todo as Buy Bread
|
||||
When I click the Add button
|
||||
Then I see the todo named Buy Bread
|
||||
|
||||
@add_todo-style
|
||||
Scenario: Should see the pending todo
|
||||
When I add a todo as Buy Oranges
|
||||
Then I see the pending todo
|
||||
18
examples/server_fns_axum/e2e/features/delete_todo.feature
Normal file
18
examples/server_fns_axum/e2e/features/delete_todo.feature
Normal file
@@ -0,0 +1,18 @@
|
||||
@delete_todo
|
||||
Feature: Delete Todo
|
||||
|
||||
Background:
|
||||
Given I see the app
|
||||
|
||||
@serial
|
||||
@delete_todo-remove
|
||||
Scenario: Should not see the deleted todo
|
||||
Given I add a todo as Buy Yogurt
|
||||
When I delete the todo named Buy Yogurt
|
||||
Then I do not see the todo named Buy Yogurt
|
||||
|
||||
@serial
|
||||
@delete_todo-message
|
||||
Scenario: Should see the empty list message
|
||||
When I empty the todo list
|
||||
Then I see the empty list message is No tasks were found.
|
||||
12
examples/server_fns_axum/e2e/features/open_app.feature
Normal file
12
examples/server_fns_axum/e2e/features/open_app.feature
Normal file
@@ -0,0 +1,12 @@
|
||||
@open_app
|
||||
Feature: Open App
|
||||
|
||||
@open_app-title
|
||||
Scenario: Should see the home page title
|
||||
When I open the app
|
||||
Then I see the page title is My Tasks
|
||||
|
||||
@open_app-label
|
||||
Scenario: Should see the input label
|
||||
When I open the app
|
||||
Then I see the label of the input is Add a Todo
|
||||
14
examples/server_fns_axum/e2e/tests/app_suite.rs
Normal file
14
examples/server_fns_axum/e2e/tests/app_suite.rs
Normal file
@@ -0,0 +1,14 @@
|
||||
mod fixtures;
|
||||
|
||||
use anyhow::Result;
|
||||
use cucumber::World;
|
||||
use fixtures::world::AppWorld;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
AppWorld::cucumber()
|
||||
.fail_on_skipped()
|
||||
.run_and_exit("./features")
|
||||
.await;
|
||||
Ok(())
|
||||
}
|
||||
60
examples/server_fns_axum/e2e/tests/fixtures/action.rs
vendored
Normal file
60
examples/server_fns_axum/e2e/tests/fixtures/action.rs
vendored
Normal file
@@ -0,0 +1,60 @@
|
||||
use super::{find, world::HOST};
|
||||
use anyhow::Result;
|
||||
use fantoccini::Client;
|
||||
use std::result::Result::Ok;
|
||||
use tokio::{self, time};
|
||||
|
||||
pub async fn goto_path(client: &Client, path: &str) -> Result<()> {
|
||||
let url = format!("{}{}", HOST, path);
|
||||
client.goto(&url).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn add_todo(client: &Client, text: &str) -> Result<()> {
|
||||
fill_todo(client, text).await?;
|
||||
click_add_button(client).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn fill_todo(client: &Client, text: &str) -> Result<()> {
|
||||
let textbox = find::todo_input(client).await;
|
||||
textbox.send_keys(text).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn click_add_button(client: &Client) -> Result<()> {
|
||||
let add_button = find::add_button(client).await;
|
||||
add_button.click().await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn empty_todo_list(client: &Client) -> Result<()> {
|
||||
let todos = find::todos(client).await;
|
||||
|
||||
for _todo in todos {
|
||||
let _ = delete_first_todo(client).await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn delete_first_todo(client: &Client) -> Result<()> {
|
||||
if let Some(element) = find::first_delete_button(client).await {
|
||||
element.click().await.expect("Failed to delete todo");
|
||||
time::sleep(time::Duration::from_millis(250)).await;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn delete_todo(client: &Client, text: &str) -> Result<()> {
|
||||
if let Some(element) = find::delete_button(client, text).await {
|
||||
element.click().await?;
|
||||
time::sleep(time::Duration::from_millis(250)).await;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
57
examples/server_fns_axum/e2e/tests/fixtures/check.rs
vendored
Normal file
57
examples/server_fns_axum/e2e/tests/fixtures/check.rs
vendored
Normal file
@@ -0,0 +1,57 @@
|
||||
use super::find;
|
||||
use anyhow::{Ok, Result};
|
||||
use fantoccini::{Client, Locator};
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
pub async fn text_on_element(
|
||||
client: &Client,
|
||||
selector: &str,
|
||||
expected_text: &str,
|
||||
) -> Result<()> {
|
||||
let element = client
|
||||
.wait()
|
||||
.for_element(Locator::Css(selector))
|
||||
.await
|
||||
.expect(
|
||||
format!("Element not found by Css selector `{}`", selector)
|
||||
.as_str(),
|
||||
);
|
||||
|
||||
let actual = element.text().await?;
|
||||
assert_eq!(&actual, expected_text);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn todo_present(
|
||||
client: &Client,
|
||||
text: &str,
|
||||
expected: bool,
|
||||
) -> Result<()> {
|
||||
let todo_present = is_todo_present(client, text).await;
|
||||
|
||||
assert_eq!(todo_present, expected);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn is_todo_present(client: &Client, text: &str) -> bool {
|
||||
let todos = find::todos(client).await;
|
||||
|
||||
for todo in todos {
|
||||
let todo_title = todo.text().await.expect("Todo title not found");
|
||||
if todo_title == text {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
pub async fn todo_is_pending(client: &Client) -> Result<()> {
|
||||
if let None = find::pending_todo(client).await {
|
||||
assert!(false, "Pending todo not found");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
63
examples/server_fns_axum/e2e/tests/fixtures/find.rs
vendored
Normal file
63
examples/server_fns_axum/e2e/tests/fixtures/find.rs
vendored
Normal file
@@ -0,0 +1,63 @@
|
||||
use fantoccini::{elements::Element, Client, Locator};
|
||||
|
||||
pub async fn todo_input(client: &Client) -> Element {
|
||||
let textbox = client
|
||||
.wait()
|
||||
.for_element(Locator::Css("input[name='title"))
|
||||
.await
|
||||
.expect("Todo textbox not found");
|
||||
|
||||
textbox
|
||||
}
|
||||
|
||||
pub async fn add_button(client: &Client) -> Element {
|
||||
let button = client
|
||||
.wait()
|
||||
.for_element(Locator::Css("input[value='Add']"))
|
||||
.await
|
||||
.expect("");
|
||||
|
||||
button
|
||||
}
|
||||
|
||||
pub async fn first_delete_button(client: &Client) -> Option<Element> {
|
||||
if let Ok(element) = client
|
||||
.wait()
|
||||
.for_element(Locator::Css("li:first-child input[value='X']"))
|
||||
.await
|
||||
{
|
||||
return Some(element);
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
pub async fn delete_button(client: &Client, text: &str) -> Option<Element> {
|
||||
let selector = format!("//*[text()='{text}']//input[@value='X']");
|
||||
if let Ok(element) =
|
||||
client.wait().for_element(Locator::XPath(&selector)).await
|
||||
{
|
||||
return Some(element);
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
pub async fn pending_todo(client: &Client) -> Option<Element> {
|
||||
if let Ok(element) =
|
||||
client.wait().for_element(Locator::Css(".pending")).await
|
||||
{
|
||||
return Some(element);
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
pub async fn todos(client: &Client) -> Vec<Element> {
|
||||
let todos = client
|
||||
.find_all(Locator::Css("li"))
|
||||
.await
|
||||
.expect("Todo List not found");
|
||||
|
||||
todos
|
||||
}
|
||||
4
examples/server_fns_axum/e2e/tests/fixtures/mod.rs
vendored
Normal file
4
examples/server_fns_axum/e2e/tests/fixtures/mod.rs
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
pub mod action;
|
||||
pub mod check;
|
||||
pub mod find;
|
||||
pub mod world;
|
||||
57
examples/server_fns_axum/e2e/tests/fixtures/world/action_steps.rs
vendored
Normal file
57
examples/server_fns_axum/e2e/tests/fixtures/world/action_steps.rs
vendored
Normal file
@@ -0,0 +1,57 @@
|
||||
use crate::fixtures::{action, world::AppWorld};
|
||||
use anyhow::{Ok, Result};
|
||||
use cucumber::{given, when};
|
||||
|
||||
#[given("I see the app")]
|
||||
#[when("I open the app")]
|
||||
async fn i_open_the_app(world: &mut AppWorld) -> Result<()> {
|
||||
let client = &world.client;
|
||||
action::goto_path(client, "").await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[given(regex = "^I add a todo as (.*)$")]
|
||||
#[when(regex = "^I add a todo as (.*)$")]
|
||||
async fn i_add_a_todo_titled(world: &mut AppWorld, text: String) -> Result<()> {
|
||||
let client = &world.client;
|
||||
action::add_todo(client, text.as_str()).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[given(regex = "^I set the todo as (.*)$")]
|
||||
async fn i_set_the_todo_as(world: &mut AppWorld, text: String) -> Result<()> {
|
||||
let client = &world.client;
|
||||
action::fill_todo(client, &text).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[when(regex = "I click the Add button$")]
|
||||
async fn i_click_the_button(world: &mut AppWorld) -> Result<()> {
|
||||
let client = &world.client;
|
||||
action::click_add_button(client).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[when(regex = "^I delete the todo named (.*)$")]
|
||||
async fn i_delete_the_todo_named(
|
||||
world: &mut AppWorld,
|
||||
text: String,
|
||||
) -> Result<()> {
|
||||
let client = &world.client;
|
||||
action::delete_todo(client, text.as_str()).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[given("the todo list is empty")]
|
||||
#[when("I empty the todo list")]
|
||||
async fn i_empty_the_todo_list(world: &mut AppWorld) -> Result<()> {
|
||||
let client = &world.client;
|
||||
action::empty_todo_list(client).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
67
examples/server_fns_axum/e2e/tests/fixtures/world/check_steps.rs
vendored
Normal file
67
examples/server_fns_axum/e2e/tests/fixtures/world/check_steps.rs
vendored
Normal file
@@ -0,0 +1,67 @@
|
||||
use crate::fixtures::{check, world::AppWorld};
|
||||
use anyhow::{Ok, Result};
|
||||
use cucumber::then;
|
||||
|
||||
#[then(regex = "^I see the page title is (.*)$")]
|
||||
async fn i_see_the_page_title_is(
|
||||
world: &mut AppWorld,
|
||||
text: String,
|
||||
) -> Result<()> {
|
||||
let client = &world.client;
|
||||
check::text_on_element(client, "h1", &text).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[then(regex = "^I see the label of the input is (.*)$")]
|
||||
async fn i_see_the_label_of_the_input_is(
|
||||
world: &mut AppWorld,
|
||||
text: String,
|
||||
) -> Result<()> {
|
||||
let client = &world.client;
|
||||
check::text_on_element(client, "label", &text).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[then(regex = "^I see the todo named (.*)$")]
|
||||
async fn i_see_the_todo_is_present(
|
||||
world: &mut AppWorld,
|
||||
text: String,
|
||||
) -> Result<()> {
|
||||
let client = &world.client;
|
||||
check::todo_present(client, text.as_str(), true).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[then("I see the pending todo")]
|
||||
async fn i_see_the_pending_todo(world: &mut AppWorld) -> Result<()> {
|
||||
let client = &world.client;
|
||||
|
||||
check::todo_is_pending(client).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[then(regex = "^I see the empty list message is (.*)$")]
|
||||
async fn i_see_the_empty_list_message_is(
|
||||
world: &mut AppWorld,
|
||||
text: String,
|
||||
) -> Result<()> {
|
||||
let client = &world.client;
|
||||
check::text_on_element(client, "ul p", &text).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[then(regex = "^I do not see the todo named (.*)$")]
|
||||
async fn i_do_not_see_the_todo_is_present(
|
||||
world: &mut AppWorld,
|
||||
text: String,
|
||||
) -> Result<()> {
|
||||
let client = &world.client;
|
||||
check::todo_present(client, text.as_str(), false).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
39
examples/server_fns_axum/e2e/tests/fixtures/world/mod.rs
vendored
Normal file
39
examples/server_fns_axum/e2e/tests/fixtures/world/mod.rs
vendored
Normal file
@@ -0,0 +1,39 @@
|
||||
pub mod action_steps;
|
||||
pub mod check_steps;
|
||||
|
||||
use anyhow::Result;
|
||||
use cucumber::World;
|
||||
use fantoccini::{
|
||||
error::NewSessionError, wd::Capabilities, Client, ClientBuilder,
|
||||
};
|
||||
|
||||
pub const HOST: &str = "http://127.0.0.1:3000";
|
||||
|
||||
#[derive(Debug, World)]
|
||||
#[world(init = Self::new)]
|
||||
pub struct AppWorld {
|
||||
pub client: Client,
|
||||
}
|
||||
|
||||
impl AppWorld {
|
||||
async fn new() -> Result<Self, anyhow::Error> {
|
||||
let webdriver_client = build_client().await?;
|
||||
|
||||
Ok(Self {
|
||||
client: webdriver_client,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async fn build_client() -> Result<Client, NewSessionError> {
|
||||
let mut cap = Capabilities::new();
|
||||
let arg = serde_json::from_str("{\"args\": [\"-headless\"]}").unwrap();
|
||||
cap.insert("goog:chromeOptions".to_string(), arg);
|
||||
|
||||
let client = ClientBuilder::native()
|
||||
.capabilities(cap)
|
||||
.connect("http://localhost:4444")
|
||||
.await?;
|
||||
|
||||
Ok(client)
|
||||
}
|
||||
@@ -1,16 +1,10 @@
|
||||
use futures::StreamExt;
|
||||
use http::Method;
|
||||
use leptos::{html::Input, *};
|
||||
use leptos_meta::{provide_meta_context, Link, Meta, Stylesheet};
|
||||
use leptos_router::{ActionForm, Route, Router, Routes};
|
||||
use serde::{de::DeserializeOwned, Deserialize, Serialize};
|
||||
use server_fn::{
|
||||
codec::{
|
||||
Encoding, FromReq, FromRes, GetUrl, IntoReq, IntoRes, MultipartData,
|
||||
MultipartFormData, Rkyv, SerdeLite, StreamingText, TextStream,
|
||||
},
|
||||
request::{ClientReq, Req},
|
||||
response::{ClientRes, Res},
|
||||
use server_fn::codec::{
|
||||
GetUrl, MultipartData, MultipartFormData, Rkyv, SerdeLite, StreamingText,
|
||||
TextStream,
|
||||
};
|
||||
#[cfg(feature = "ssr")]
|
||||
use std::sync::{
|
||||
@@ -56,7 +50,7 @@ pub fn HomePage() -> impl IntoView {
|
||||
<RkyvExample/>
|
||||
<FileUpload/>
|
||||
<FileWatcher/>
|
||||
<CustomEncoding/>
|
||||
<StreamingValues/>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -175,12 +169,15 @@ pub fn WithAnAction() -> impl IntoView {
|
||||
<button
|
||||
on:click=move |_| {
|
||||
let text = input_ref.get().unwrap().value();
|
||||
action.dispatch(text.into());
|
||||
action.dispatch(text);
|
||||
// note: technically, this `action` takes `AddRow` (the server fn type) as its
|
||||
// argument
|
||||
//
|
||||
// however, for any one-argument server functions, `From<_>` is implemented between
|
||||
// the server function type and the type of this single argument
|
||||
// however, `.dispatch()` takes `impl Into<I>`, and for any one-argument server
|
||||
// functions, `From<_>` is implemented between the server function type and the
|
||||
// type of this single argument
|
||||
//
|
||||
// so `action.dispatch(text)` means `action.dispatch(AddRow { text })`
|
||||
}
|
||||
>
|
||||
Submit
|
||||
@@ -511,124 +508,46 @@ pub fn CustomErrorTypes() -> impl IntoView {
|
||||
}
|
||||
}
|
||||
|
||||
/// Server function encodings are just types that implement a few traits.
|
||||
/// This means that you can implement your own encodings, by implementing those traits!
|
||||
///
|
||||
/// Here, we'll create a custom encoding that serializes and deserializes the server fn
|
||||
/// using TOML. Why would you ever want to do this? I don't know, but you can!
|
||||
pub struct Toml;
|
||||
|
||||
/// A newtype wrapper around server fn data that will be TOML-encoded.
|
||||
///
|
||||
/// This is needed because of Rust rules around implementing foreign traits for foreign types.
|
||||
/// It will be fed into the `custom = ` argument to the server fn below.
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct TomlEncoded<T>(T);
|
||||
|
||||
impl Encoding for Toml {
|
||||
const CONTENT_TYPE: &'static str = "application/toml";
|
||||
const METHOD: Method = Method::POST;
|
||||
}
|
||||
|
||||
impl<T, Request, Err> IntoReq<Toml, Request, Err> for TomlEncoded<T>
|
||||
where
|
||||
Request: ClientReq<Err>,
|
||||
T: Serialize,
|
||||
{
|
||||
fn into_req(
|
||||
self,
|
||||
path: &str,
|
||||
accepts: &str,
|
||||
) -> Result<Request, ServerFnError<Err>> {
|
||||
let data = toml::to_string(&self.0)
|
||||
.map_err(|e| ServerFnError::Serialization(e.to_string()))?;
|
||||
Request::try_new_post(path, Toml::CONTENT_TYPE, accepts, data)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, Request, Err> FromReq<Toml, Request, Err> for TomlEncoded<T>
|
||||
where
|
||||
Request: Req<Err> + Send,
|
||||
T: DeserializeOwned,
|
||||
{
|
||||
async fn from_req(req: Request) -> Result<Self, ServerFnError<Err>> {
|
||||
let string_data = req.try_into_string().await?;
|
||||
toml::from_str::<T>(&string_data)
|
||||
.map(TomlEncoded)
|
||||
.map_err(|e| ServerFnError::Args(e.to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, Response, Err> IntoRes<Toml, Response, Err> for TomlEncoded<T>
|
||||
where
|
||||
Response: Res<Err>,
|
||||
T: Serialize + Send,
|
||||
{
|
||||
async fn into_res(self) -> Result<Response, ServerFnError<Err>> {
|
||||
let data = toml::to_string(&self.0)
|
||||
.map_err(|e| ServerFnError::Serialization(e.to_string()))?;
|
||||
Response::try_from_string(Toml::CONTENT_TYPE, data)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, Response, Err> FromRes<Toml, Response, Err> for TomlEncoded<T>
|
||||
where
|
||||
Response: ClientRes<Err> + Send,
|
||||
T: DeserializeOwned,
|
||||
{
|
||||
async fn from_res(res: Response) -> Result<Self, ServerFnError<Err>> {
|
||||
let data = res.try_into_string().await?;
|
||||
toml::from_str(&data)
|
||||
.map(TomlEncoded)
|
||||
.map_err(|e| ServerFnError::Deserialization(e.to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct WhyNotResult {
|
||||
original: String,
|
||||
modified: String,
|
||||
}
|
||||
|
||||
#[server(
|
||||
input = Toml,
|
||||
output = Toml,
|
||||
custom = TomlEncoded
|
||||
)]
|
||||
pub async fn why_not(
|
||||
original: String,
|
||||
addition: String,
|
||||
) -> Result<TomlEncoded<WhyNotResult>, ServerFnError> {
|
||||
// insert a simulated wait
|
||||
tokio::time::sleep(std::time::Duration::from_millis(250)).await;
|
||||
Ok(TomlEncoded(WhyNotResult {
|
||||
modified: format!("{original}{addition}"),
|
||||
original,
|
||||
}))
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn CustomEncoding() -> impl IntoView {
|
||||
let input_ref = NodeRef::<Input>::new();
|
||||
let (result, set_result) = create_signal("foo".to_string());
|
||||
pub fn StreamingValues() -> impl IntoView {
|
||||
use futures::StreamExt;
|
||||
|
||||
/// You can create server functions that accept streaming values by using the encoding
|
||||
/// `Streaming` (with type `ByteStream`) or encoding `StreamingText` (with type `TextStream`)
|
||||
#[server(input = StreamingText, output = StreamingText)]
|
||||
pub async fn streaming(input: TextStream) -> Result<TextStream, ServerFnError> {
|
||||
println!("inside streaming() fn");
|
||||
Ok(TextStream::from(input.into_inner().map(|text| format!("{}!!!", text.unwrap_or_else(|e| e.to_string())))))
|
||||
}
|
||||
|
||||
let mut count = 0;
|
||||
let (tx, rx) = futures::channel::mpsc::unbounded();
|
||||
let (result, set_result) = create_signal("Click me...".to_string());
|
||||
|
||||
|
||||
if cfg!(feature = "hydrate") {
|
||||
spawn_local(async move {
|
||||
logging::log!("calling streaming server fn");
|
||||
match streaming(TextStream::new(rx)).await {
|
||||
Ok(res) => {
|
||||
logging::log!("after calling streaming()");
|
||||
let mut stream = res.into_inner();
|
||||
while let Some(chunk) = stream.next().await {
|
||||
set_result(chunk.unwrap_or_else(|e| e.to_string()));
|
||||
}
|
||||
}, Err(e) => logging::log!("{e}") }
|
||||
})
|
||||
}
|
||||
|
||||
view! {
|
||||
<h3>Custom encodings</h3>
|
||||
<p>
|
||||
"This example creates a custom encoding that sends server fn data using TOML. Why? Well... why not?"
|
||||
</p>
|
||||
<input node_ref=input_ref placeholder="Type something here."/>
|
||||
<h3>Streaming arguments and responses</h3>
|
||||
<button
|
||||
on:click=move |_| {
|
||||
let value = input_ref.get().unwrap().value();
|
||||
spawn_local(async move {
|
||||
let new_value = why_not(value, ", but in TOML!!!".to_string()).await.unwrap();
|
||||
set_result(new_value.0.modified);
|
||||
});
|
||||
count += 1;
|
||||
tx.unbounded_send(Ok(count.to_string())).expect("couldn't send into channel");
|
||||
}
|
||||
>
|
||||
Submit
|
||||
{result}
|
||||
</button>
|
||||
<p>{result}</p>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,39 +7,40 @@ edition = "2021"
|
||||
crate-type = ["cdylib", "rlib"]
|
||||
|
||||
[dependencies]
|
||||
oauth2 = { version = "4.4.2", optional = true }
|
||||
oauth2 = {version="4.4.2",optional=true}
|
||||
anyhow = "1.0.66"
|
||||
console_log = "1.0.0"
|
||||
rand = { version = "0.8.5", features = ["min_const_gen"], optional = true }
|
||||
console_error_panic_hook = "0.1.7"
|
||||
futures = "0.3.25"
|
||||
leptos = { path = "../../leptos" }
|
||||
cfg-if = "1.0.0"
|
||||
leptos = { path = "../../leptos"}
|
||||
leptos_meta = { path = "../../meta" }
|
||||
leptos_axum = { path = "../../integrations/axum", optional = true }
|
||||
leptos_router = { path = "../../router" }
|
||||
leptos_router = { path = "../../router"}
|
||||
log = "0.4.17"
|
||||
simple_logger = "4.0.0"
|
||||
serde = { version = "1.0.148", features = ["derive"] }
|
||||
serde_json = { version = "1.0.108", optional = true }
|
||||
axum = { version = "0.7", optional = true, features = ["macros"] }
|
||||
tower = { version = "0.4", optional = true }
|
||||
tower-http = { version = "0.5", features = ["fs"], optional = true }
|
||||
serde_json = {version="1.0.108", optional = true }
|
||||
axum = { version = "0.6.1", optional = true, features=["macros"] }
|
||||
tower = { version = "0.4.13", optional = true }
|
||||
tower-http = { version = "0.4", features = ["fs"], optional = true }
|
||||
tokio = { version = "1.22.0", features = ["full"], optional = true }
|
||||
http = { version = "1" }
|
||||
sqlx = { version = "0.7", features = [
|
||||
http = { version = "0.2.8" }
|
||||
sqlx = { version = "0.6.2", features = [
|
||||
"runtime-tokio-rustls",
|
||||
"sqlite",
|
||||
], optional = true }
|
||||
thiserror = "1.0.38"
|
||||
wasm-bindgen = "0.2"
|
||||
axum_session_auth = { version = "0.12", features = [
|
||||
axum_session_auth = { version = "0.2.1", features = [
|
||||
"sqlite-rustls",
|
||||
], optional = true }
|
||||
axum_session = { version = "0.12", features = [
|
||||
axum_session = { version = "0.2.3", features = [
|
||||
"sqlite-rustls",
|
||||
], optional = true }
|
||||
async-trait = { version = "0.1.64", optional = true }
|
||||
reqwest = { version = "0.11", optional = true, features = ["json"] }
|
||||
reqwest= {version="0.11",optional=true, features=["json"]}
|
||||
|
||||
[features]
|
||||
hydrate = ["leptos/hydrate", "leptos_meta/hydrate", "leptos_router/hydrate"]
|
||||
@@ -62,9 +63,7 @@ ssr = [
|
||||
"dep:leptos_axum",
|
||||
]
|
||||
|
||||
[package.metadata.cargo-all-features]
|
||||
denylist = ["axum", "tower", "tower-http", "tokio", "sqlx", "leptos_axum"]
|
||||
skip_feature_sets = [["csr", "ssr"], ["csr", "hydrate"], ["ssr", "hydrate"]]
|
||||
|
||||
|
||||
[package.metadata.leptos]
|
||||
# The name used by wasm-bindgen/cargo-leptos for the JS/WASM bundle. Defaults to the crate name
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
extend = [
|
||||
{ path = "../cargo-make/main.toml" },
|
||||
{ path = "../cargo-make/cargo-leptos.toml" },
|
||||
]
|
||||
extend = { path = "../cargo-make/main.toml" }
|
||||
|
||||
[env]
|
||||
[tasks.build]
|
||||
command = "cargo"
|
||||
args = ["+nightly", "build-all-features"]
|
||||
install_crate = "cargo-all-features"
|
||||
|
||||
CLIENT_PROCESS_NAME = "sso_auth_axum"
|
||||
[tasks.check]
|
||||
command = "cargo"
|
||||
args = ["+nightly", "check-all-features"]
|
||||
install_crate = "cargo-all-features"
|
||||
|
||||
@@ -1,6 +1,14 @@
|
||||
use cfg_if::cfg_if;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashSet;
|
||||
|
||||
cfg_if! {
|
||||
if #[cfg(feature = "ssr")] {
|
||||
use sqlx::SqlitePool;
|
||||
use axum_session_auth::{SessionSqlitePool, Authentication, HasPermission};
|
||||
pub type AuthSession = axum_session_auth::AuthSession<User, i64, SessionSqlitePool, SqlitePool>;
|
||||
}}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct User {
|
||||
pub id: i64,
|
||||
@@ -20,56 +28,36 @@ impl Default for User {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
pub mod ssr_imports {
|
||||
use super::User;
|
||||
pub use axum_session_auth::{
|
||||
Authentication, HasPermission, SessionSqlitePool,
|
||||
};
|
||||
pub use sqlx::SqlitePool;
|
||||
use std::collections::HashSet;
|
||||
pub type AuthSession = axum_session_auth::AuthSession<
|
||||
User,
|
||||
i64,
|
||||
SessionSqlitePool,
|
||||
SqlitePool,
|
||||
>;
|
||||
|
||||
cfg_if! {
|
||||
if #[cfg(feature = "ssr")] {
|
||||
use async_trait::async_trait;
|
||||
|
||||
impl User {
|
||||
pub async fn get(id: i64, pool: &SqlitePool) -> Option<Self> {
|
||||
let sqluser = sqlx::query_as::<_, SqlUser>(
|
||||
"SELECT * FROM users WHERE id = ?",
|
||||
)
|
||||
.bind(id)
|
||||
.fetch_one(pool)
|
||||
.await
|
||||
.ok()?;
|
||||
let sqluser = sqlx::query_as::<_, SqlUser>("SELECT * FROM users WHERE id = ?")
|
||||
.bind(id)
|
||||
.fetch_one(pool)
|
||||
.await
|
||||
.ok()?;
|
||||
|
||||
//lets just get all the tokens the user can use, we will only use the full permissions if modifing them.
|
||||
let sql_user_perms = sqlx::query_as::<_, SqlPermissionTokens>(
|
||||
"SELECT token FROM user_permissions WHERE user_id = ?;",
|
||||
)
|
||||
.bind(id)
|
||||
.fetch_all(pool)
|
||||
.await
|
||||
.ok()?;
|
||||
.bind(id)
|
||||
.fetch_all(pool)
|
||||
.await
|
||||
.ok()?;
|
||||
|
||||
Some(sqluser.into_user(Some(sql_user_perms)))
|
||||
}
|
||||
|
||||
pub async fn get_from_email(
|
||||
email: &str,
|
||||
pool: &SqlitePool,
|
||||
) -> Option<Self> {
|
||||
let sqluser = sqlx::query_as::<_, SqlUser>(
|
||||
"SELECT * FROM users WHERE email = ?",
|
||||
)
|
||||
.bind(email)
|
||||
.fetch_one(pool)
|
||||
.await
|
||||
.ok()?;
|
||||
pub async fn get_from_email(email: &str, pool: &SqlitePool) -> Option<Self> {
|
||||
let sqluser = sqlx::query_as::<_, SqlUser>("SELECT * FROM users WHERE email = ?")
|
||||
.bind(email)
|
||||
.fetch_one(pool)
|
||||
.await
|
||||
.ok()?;
|
||||
|
||||
//lets just get all the tokens the user can use, we will only use the full permissions if modifing them.
|
||||
let sql_user_perms = sqlx::query_as::<_, SqlPermissionTokens>(
|
||||
@@ -96,10 +84,7 @@ pub mod ssr_imports {
|
||||
|
||||
#[async_trait]
|
||||
impl Authentication<User, i64, SqlitePool> for User {
|
||||
async fn load_user(
|
||||
userid: i64,
|
||||
pool: Option<&SqlitePool>,
|
||||
) -> Result<User, anyhow::Error> {
|
||||
async fn load_user(userid: i64, pool: Option<&SqlitePool>) -> Result<User, anyhow::Error> {
|
||||
let pool = pool.unwrap();
|
||||
|
||||
User::get(userid, pool)
|
||||
@@ -138,11 +123,9 @@ pub mod ssr_imports {
|
||||
pub secret: String,
|
||||
}
|
||||
|
||||
|
||||
impl SqlUser {
|
||||
pub fn into_user(
|
||||
self,
|
||||
sql_user_perms: Option<Vec<SqlPermissionTokens>>,
|
||||
) -> User {
|
||||
pub fn into_user(self, sql_user_perms: Option<Vec<SqlPermissionTokens>>) -> User {
|
||||
User {
|
||||
id: self.id,
|
||||
email: self.email,
|
||||
@@ -158,3 +141,4 @@ pub mod ssr_imports {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ pub fn error_template(errors: RwSignal<Errors>) -> View {
|
||||
children= move | (_, error)| {
|
||||
let error_string = error.to_string();
|
||||
view! {
|
||||
|
||||
<p>"Error: " {error_string}</p>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,49 +1,47 @@
|
||||
use crate::error_template::error_template;
|
||||
use axum::{
|
||||
body::Body,
|
||||
extract::State,
|
||||
http::{Request, Response, StatusCode, Uri},
|
||||
response::{IntoResponse, Response as AxumResponse},
|
||||
};
|
||||
use leptos::*;
|
||||
use tower::ServiceExt;
|
||||
use tower_http::services::ServeDir;
|
||||
use cfg_if::cfg_if;
|
||||
|
||||
pub async fn file_and_error_handler(
|
||||
uri: Uri,
|
||||
State(options): State<LeptosOptions>,
|
||||
req: Request<Body>,
|
||||
) -> AxumResponse {
|
||||
let root = options.site_root.clone();
|
||||
let res = get_static_file(uri.clone(), &root).await.unwrap();
|
||||
cfg_if! {
|
||||
if #[cfg(feature = "ssr")] {
|
||||
use axum::{
|
||||
body::{boxed, Body, BoxBody},
|
||||
extract::State,
|
||||
response::IntoResponse,
|
||||
http::{Request, Response, StatusCode, Uri},
|
||||
};
|
||||
use axum::response::Response as AxumResponse;
|
||||
use tower::ServiceExt;
|
||||
use tower_http::services::ServeDir;
|
||||
use leptos::*;
|
||||
use crate::error_template::error_template;
|
||||
|
||||
if res.status() == StatusCode::OK {
|
||||
res.into_response()
|
||||
} else {
|
||||
leptos::logging::log!("{:?}:{}", res.status(), uri);
|
||||
let handler =
|
||||
leptos_axum::render_app_to_stream(options.to_owned(), || {
|
||||
error_template(create_rw_signal(leptos::Errors::default()))
|
||||
});
|
||||
handler(req).await.into_response()
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_static_file(
|
||||
uri: Uri,
|
||||
root: &str,
|
||||
) -> Result<Response<Body>, (StatusCode, String)> {
|
||||
let req = Request::builder()
|
||||
.uri(uri.clone())
|
||||
.body(Body::empty())
|
||||
.unwrap();
|
||||
// `ServeDir` implements `tower::Service` so we can call it with `tower::ServiceExt::oneshot`
|
||||
// This path is relative to the cargo root
|
||||
match ServeDir::new(root).oneshot(req).await {
|
||||
Ok(res) => Ok(res.into_response()),
|
||||
Err(err) => Err((
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("Something went wrong: {}", err),
|
||||
)),
|
||||
pub async fn file_and_error_handler(uri: Uri, State(options): State<LeptosOptions>, req: Request<Body>) -> AxumResponse {
|
||||
let root = options.site_root.clone();
|
||||
let res = get_static_file(uri.clone(), &root).await.unwrap();
|
||||
|
||||
if res.status() == StatusCode::OK {
|
||||
res.into_response()
|
||||
} else {
|
||||
leptos::logging::log!("{:?}:{}",res.status(),uri);
|
||||
let handler = leptos_axum::render_app_to_stream(
|
||||
options.to_owned(),
|
||||
|| error_template(create_rw_signal(leptos::Errors::default())
|
||||
)
|
||||
);
|
||||
handler(req).await.into_response()
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_static_file(uri: Uri, root: &str) -> Result<Response<BoxBody>, (StatusCode, String)> {
|
||||
let req = Request::builder().uri(uri.clone()).body(Body::empty()).unwrap();
|
||||
// `ServeDir` implements `tower::Service` so we can call it with `tower::ServiceExt::oneshot`
|
||||
// This path is relative to the cargo root
|
||||
match ServeDir::new(root).oneshot(req).await {
|
||||
Ok(res) => Ok(res.map(boxed)),
|
||||
Err(err) => Err((
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("Something went wrong: {}", err),
|
||||
)),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,30 +1,36 @@
|
||||
use cfg_if::cfg_if;
|
||||
|
||||
pub mod auth;
|
||||
pub mod error_template;
|
||||
#[cfg(feature = "ssr")]
|
||||
pub mod fallback;
|
||||
pub mod sign_in_sign_up;
|
||||
#[cfg(feature = "ssr")]
|
||||
pub mod state;
|
||||
use leptos::{leptos_dom::helpers::TimeoutHandle, *};
|
||||
use leptos_meta::*;
|
||||
use leptos_router::*;
|
||||
use sign_in_sign_up::*;
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
mod ssr_imports {
|
||||
pub use crate::auth::ssr_imports::{AuthSession, SqlRefreshToken};
|
||||
pub use leptos::{use_context, ServerFnError};
|
||||
pub use oauth2::{reqwest::async_http_client, TokenResponse};
|
||||
pub use sqlx::SqlitePool;
|
||||
cfg_if! {
|
||||
if #[cfg(feature = "ssr")] {
|
||||
use crate::{
|
||||
state::AppState,
|
||||
auth::{AuthSession,User,SqlRefreshToken}
|
||||
};
|
||||
use oauth2::{
|
||||
reqwest::async_http_client,
|
||||
TokenResponse
|
||||
};
|
||||
use sqlx::SqlitePool;
|
||||
|
||||
pub fn pool() -> Result<SqlitePool, ServerFnError> {
|
||||
use_context::<SqlitePool>()
|
||||
.ok_or_else(|| ServerFnError::new("Pool missing."))
|
||||
}
|
||||
pub fn pool() -> Result<SqlitePool, ServerFnError> {
|
||||
use_context::<SqlitePool>()
|
||||
.ok_or_else(|| ServerFnError::ServerError("Pool missing.".into()))
|
||||
}
|
||||
|
||||
pub fn auth() -> Result<AuthSession, ServerFnError> {
|
||||
use_context::<AuthSession>()
|
||||
.ok_or_else(|| ServerFnError::new("Auth session missing."))
|
||||
pub fn auth() -> Result<AuthSession, ServerFnError> {
|
||||
use_context::<AuthSession>()
|
||||
.ok_or_else(|| ServerFnError::ServerError("Auth session missing.".into()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,14 +40,11 @@ pub struct Email(RwSignal<Option<String>>);
|
||||
pub struct ExpiresIn(RwSignal<u64>);
|
||||
#[server]
|
||||
pub async fn refresh_token(email: String) -> Result<u64, ServerFnError> {
|
||||
use crate::{auth::User, state::AppState};
|
||||
use ssr_imports::*;
|
||||
|
||||
let pool = pool()?;
|
||||
let oauth_client = expect_context::<AppState>().client;
|
||||
let user = User::get_from_email(&email, &pool)
|
||||
.await
|
||||
.ok_or(ServerFnError::new("User not found"))?;
|
||||
.ok_or(ServerFnError::ServerError("User not found".to_string()))?;
|
||||
|
||||
let refresh_secret = sqlx::query_as::<_, SqlRefreshToken>(
|
||||
"SELECT secret FROM google_refresh_tokens WHERE user_id = ?",
|
||||
@@ -74,7 +77,6 @@ pub async fn refresh_token(email: String) -> Result<u64, ServerFnError> {
|
||||
.await?;
|
||||
Ok(expires_in)
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn App() -> impl IntoView {
|
||||
provide_meta_context();
|
||||
@@ -141,11 +143,20 @@ pub fn App() -> impl IntoView {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "hydrate")]
|
||||
#[wasm_bindgen::prelude::wasm_bindgen]
|
||||
pub fn hydrate() {
|
||||
_ = console_log::init_with_level(log::Level::Debug);
|
||||
console_error_panic_hook::set_once();
|
||||
// Needs to be in lib.rs AFAIK because wasm-bindgen needs us to be compiling a lib. I may be wrong.
|
||||
cfg_if! {
|
||||
if #[cfg(feature = "hydrate")] {
|
||||
use wasm_bindgen::prelude::wasm_bindgen;
|
||||
use leptos::view;
|
||||
|
||||
leptos::mount_to_body(App);
|
||||
#[wasm_bindgen]
|
||||
pub fn hydrate() {
|
||||
_ = console_log::init_with_level(log::Level::Debug);
|
||||
console_error_panic_hook::set_once();
|
||||
|
||||
leptos::mount_to_body(|| {
|
||||
view! { <App/> }
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,154 +1,137 @@
|
||||
use crate::ssr_imports::*;
|
||||
use axum::{
|
||||
body::Body as AxumBody,
|
||||
extract::{Path, State},
|
||||
http::Request,
|
||||
response::IntoResponse,
|
||||
routing::get,
|
||||
Router,
|
||||
};
|
||||
use axum_session::{Key, SessionConfig, SessionLayer, SessionStore};
|
||||
use axum_session_auth::{AuthConfig, AuthSessionLayer, SessionSqlitePool};
|
||||
use leptos::{get_configuration, logging::log, provide_context, view};
|
||||
use leptos_axum::{
|
||||
generate_route_list, handle_server_fns_with_context, LeptosRoutes,
|
||||
};
|
||||
use sqlx::{sqlite::SqlitePoolOptions, SqlitePool};
|
||||
use sso_auth_axum::{
|
||||
auth::*, fallback::file_and_error_handler, state::AppState,
|
||||
};
|
||||
use cfg_if::cfg_if;
|
||||
|
||||
async fn server_fn_handler(
|
||||
State(app_state): State<AppState>,
|
||||
auth_session: AuthSession,
|
||||
path: Path<String>,
|
||||
request: Request<AxumBody>,
|
||||
) -> impl IntoResponse {
|
||||
log!("{:?}", path);
|
||||
// boilerplate to run in different modes
|
||||
cfg_if! {
|
||||
if #[cfg(feature = "ssr")] {
|
||||
use axum::{
|
||||
response::{IntoResponse},
|
||||
routing::get,
|
||||
extract::{Path, State, RawQuery},
|
||||
http::{Request, header::HeaderMap},
|
||||
body::Body as AxumBody,
|
||||
Router,
|
||||
};
|
||||
use sso_auth_axum::auth::*;
|
||||
use sso_auth_axum::state::AppState;
|
||||
use sso_auth_axum::fallback::file_and_error_handler;
|
||||
use leptos_axum::{generate_route_list, handle_server_fns_with_context, LeptosRoutes};
|
||||
use leptos::{logging::log, view, provide_context, get_configuration};
|
||||
use sqlx::{SqlitePool, sqlite::SqlitePoolOptions};
|
||||
use axum_session::{SessionConfig, SessionLayer, SessionStore,Key, SecurityMode};
|
||||
use axum_session_auth::{AuthSessionLayer, AuthConfig, SessionSqlitePool};
|
||||
|
||||
handle_server_fns_with_context(
|
||||
move || {
|
||||
async fn server_fn_handler(State(app_state): State<AppState>, auth_session: AuthSession, path: Path<String>, headers: HeaderMap, raw_query: RawQuery,
|
||||
request: Request<AxumBody>) -> impl IntoResponse {
|
||||
|
||||
log!("{:?}", path);
|
||||
|
||||
handle_server_fns_with_context(path, headers, raw_query, move || {
|
||||
provide_context(app_state.clone());
|
||||
provide_context(auth_session.clone());
|
||||
provide_context(app_state.pool.clone());
|
||||
},
|
||||
request,
|
||||
)
|
||||
.await
|
||||
}
|
||||
}, request).await
|
||||
}
|
||||
|
||||
pub async fn leptos_routes_handler(
|
||||
auth_session: AuthSession,
|
||||
State(app_state): State<AppState>,
|
||||
axum::extract::State(option): axum::extract::State<leptos::LeptosOptions>,
|
||||
request: Request<AxumBody>,
|
||||
) -> axum::response::Response {
|
||||
let handler = leptos_axum::render_app_async_with_context(
|
||||
option.clone(),
|
||||
move || {
|
||||
provide_context(app_state.clone());
|
||||
provide_context(auth_session.clone());
|
||||
provide_context(app_state.pool.clone());
|
||||
},
|
||||
move || view! { <sso_auth_axum::App/> },
|
||||
);
|
||||
pub async fn leptos_routes_handler(
|
||||
auth_session: AuthSession,
|
||||
State(app_state): State<AppState>,
|
||||
axum::extract::State(option): axum::extract::State<leptos::LeptosOptions>,
|
||||
request: Request<AxumBody>,
|
||||
) -> axum::response::Response {
|
||||
let handler = leptos_axum::render_app_async_with_context(
|
||||
option.clone(),
|
||||
move || {
|
||||
provide_context(app_state.clone());
|
||||
provide_context(auth_session.clone());
|
||||
provide_context(app_state.pool.clone());
|
||||
},
|
||||
move || view! { <sso_auth_axum::App/> },
|
||||
);
|
||||
|
||||
handler(request).await.into_response()
|
||||
}
|
||||
handler(request).await.into_response()
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
simple_logger::init_with_level(log::Level::Info)
|
||||
.expect("couldn't initialize logging");
|
||||
|
||||
let pool = SqlitePoolOptions::new()
|
||||
.connect("sqlite:sso.db")
|
||||
.await
|
||||
.expect("Could not make pool.");
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
simple_logger::init_with_level(log::Level::Info).expect("couldn't initialize logging");
|
||||
|
||||
// Auth section
|
||||
let session_config = SessionConfig::default()
|
||||
.with_table_name("sessions_table")
|
||||
.with_key(Key::generate())
|
||||
.with_database_key(Key::generate());
|
||||
// .with_security_mode(SecurityMode::PerSession); // FIXME did this disappear?
|
||||
let pool = SqlitePoolOptions::new()
|
||||
.connect("sqlite:sso.db")
|
||||
.await
|
||||
.expect("Could not make pool.");
|
||||
|
||||
let auth_config = AuthConfig::<i64>::default();
|
||||
let session_store = SessionStore::<SessionSqlitePool>::new(
|
||||
Some(pool.clone().into()),
|
||||
session_config,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
// Auth section
|
||||
let session_config = SessionConfig::default()
|
||||
.with_table_name("sessions_table")
|
||||
.with_key(Key::generate())
|
||||
.with_database_key(Key::generate())
|
||||
.with_security_mode(SecurityMode::PerSession);
|
||||
|
||||
sqlx::migrate!()
|
||||
.run(&pool)
|
||||
.await
|
||||
.expect("could not run SQLx migrations");
|
||||
let auth_config = AuthConfig::<i64>::default();
|
||||
let session_store = SessionStore::<SessionSqlitePool>::new(Some(pool.clone().into()), session_config);
|
||||
session_store.initiate().await.unwrap();
|
||||
|
||||
// Setting this to None means we'll be using cargo-leptos and its env vars
|
||||
let conf = get_configuration(None).await.unwrap();
|
||||
let leptos_options = conf.leptos_options;
|
||||
let addr = leptos_options.site_addr;
|
||||
let routes = generate_route_list(sso_auth_axum::App);
|
||||
sqlx::migrate!()
|
||||
.run(&pool)
|
||||
.await
|
||||
.expect("could not run SQLx migrations");
|
||||
|
||||
// We create our client using provided environment variables.
|
||||
|
||||
|
||||
// Setting this to None means we'll be using cargo-leptos and its env vars
|
||||
let conf = get_configuration(None).await.unwrap();
|
||||
let leptos_options = conf.leptos_options;
|
||||
let addr = leptos_options.site_addr;
|
||||
let routes = generate_route_list(sso_auth_axum::App);
|
||||
|
||||
// We create our client using provided environment variables.
|
||||
let client = oauth2::basic::BasicClient::new(
|
||||
oauth2::ClientId::new(
|
||||
std::env::var("G_AUTH_CLIENT_ID")
|
||||
.expect("G_AUTH_CLIENT Env var to be set."),
|
||||
),
|
||||
Some(oauth2::ClientSecret::new(
|
||||
std::env::var("G_AUTH_SECRET")
|
||||
.expect("G_AUTH_SECRET Env var to be set"),
|
||||
)),
|
||||
oauth2::ClientId::new(std::env::var("G_AUTH_CLIENT_ID").expect("G_AUTH_CLIENT Env var to be set.")),
|
||||
Some(oauth2::ClientSecret::new(std::env::var("G_AUTH_SECRET").expect("G_AUTH_SECRET Env var to be set"))),
|
||||
oauth2::AuthUrl::new(
|
||||
"https://accounts.google.com/o/oauth2/v2/auth".to_string(),
|
||||
)
|
||||
.unwrap(),
|
||||
Some(
|
||||
oauth2::TokenUrl::new(
|
||||
"https://oauth2.googleapis.com/token".to_string(),
|
||||
)
|
||||
.unwrap(),
|
||||
oauth2::TokenUrl::new("https://oauth2.googleapis.com/token".to_string())
|
||||
.unwrap(),
|
||||
),
|
||||
)
|
||||
.set_redirect_uri(
|
||||
oauth2::RedirectUrl::new(
|
||||
std::env::var("REDIRECT_URL")
|
||||
.expect("REDIRECT_URL Env var to be set"),
|
||||
)
|
||||
.unwrap(),
|
||||
);
|
||||
.set_redirect_uri(oauth2::RedirectUrl::new(std::env::var("REDIRECT_URL").expect("REDIRECT_URL Env var to be set")).unwrap());
|
||||
|
||||
let app_state = AppState {
|
||||
leptos_options,
|
||||
pool: pool.clone(),
|
||||
client,
|
||||
};
|
||||
|
||||
// build our application with a route
|
||||
let app = Router::new()
|
||||
.route(
|
||||
"/api/*fn_name",
|
||||
get(server_fn_handler).post(server_fn_handler),
|
||||
)
|
||||
.leptos_routes_with_handler(routes, get(leptos_routes_handler))
|
||||
.fallback(file_and_error_handler)
|
||||
.layer(
|
||||
AuthSessionLayer::<User, i64, SessionSqlitePool, SqlitePool>::new(
|
||||
Some(pool.clone()),
|
||||
)
|
||||
.with_config(auth_config),
|
||||
)
|
||||
.layer(SessionLayer::new(session_store))
|
||||
.with_state(app_state);
|
||||
let app_state = AppState{
|
||||
leptos_options,
|
||||
pool: pool.clone(),
|
||||
client,
|
||||
};
|
||||
|
||||
// run our app with hyper
|
||||
// `axum::Server` is a re-export of `hyper::Server`
|
||||
let listener = tokio::net::TcpListener::bind(&addr).await.unwrap();
|
||||
log!("listening on http://{}", &addr);
|
||||
axum::serve(listener, app.into_make_service())
|
||||
.await
|
||||
.unwrap();
|
||||
// build our application with a route
|
||||
let app = Router::new()
|
||||
.route("/api/*fn_name", get(server_fn_handler).post(server_fn_handler))
|
||||
.leptos_routes_with_handler(routes, get(leptos_routes_handler) )
|
||||
.fallback(file_and_error_handler)
|
||||
.layer(AuthSessionLayer::<User, i64, SessionSqlitePool, SqlitePool>::new(Some(pool.clone()))
|
||||
.with_config(auth_config))
|
||||
.layer(SessionLayer::new(session_store))
|
||||
.with_state(app_state);
|
||||
|
||||
// run our app with hyper
|
||||
// `axum::Server` is a re-export of `hyper::Server`
|
||||
log!("listening on http://{}", &addr);
|
||||
axum::Server::bind(&addr)
|
||||
.serve(app.into_make_service())
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
// client-only stuff for Trunk
|
||||
else {
|
||||
pub fn main() {
|
||||
// This example cannot be built as a trunk standalone CSR-only app.
|
||||
// Only the server may directly connect to the database.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,23 +1,23 @@
|
||||
use super::*;
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
pub mod ssr_imports {
|
||||
pub use crate::{
|
||||
auth::{ssr_imports::SqlCsrfToken, User},
|
||||
state::AppState,
|
||||
};
|
||||
pub use oauth2::{
|
||||
reqwest::async_http_client, AuthorizationCode, CsrfToken, Scope,
|
||||
TokenResponse,
|
||||
};
|
||||
pub use serde_json::Value;
|
||||
cfg_if! {
|
||||
if #[cfg(feature="ssr")]{
|
||||
use oauth2::{
|
||||
AuthorizationCode,
|
||||
TokenResponse,
|
||||
reqwest::async_http_client,
|
||||
CsrfToken,
|
||||
Scope,
|
||||
};
|
||||
use serde_json::Value;
|
||||
use crate::{
|
||||
auth::{User,SqlCsrfToken},
|
||||
state::AppState
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
#[server]
|
||||
pub async fn google_sso() -> Result<String, ServerFnError> {
|
||||
use crate::ssr_imports::*;
|
||||
use ssr_imports::*;
|
||||
|
||||
let oauth_client = expect_context::<AppState>().client;
|
||||
let pool = pool()?;
|
||||
|
||||
@@ -80,9 +80,6 @@ pub async fn handle_g_auth_redirect(
|
||||
provided_csrf: String,
|
||||
code: String,
|
||||
) -> Result<(String, u64), ServerFnError> {
|
||||
use crate::ssr_imports::*;
|
||||
use ssr_imports::*;
|
||||
|
||||
let oauth_client = expect_context::<AppState>().client;
|
||||
let pool = pool()?;
|
||||
let auth_session = auth()?;
|
||||
@@ -93,7 +90,9 @@ pub async fn handle_g_auth_redirect(
|
||||
.bind(provided_csrf)
|
||||
.fetch_one(&pool)
|
||||
.await
|
||||
.map_err(|err| ServerFnError::new(format!("CSRF_TOKEN error : {err:?}")))?;
|
||||
.map_err(|err| {
|
||||
ServerFnError::ServerError(format!("CSRF_TOKEN error : {err:?}"))
|
||||
})?;
|
||||
|
||||
let token_response = oauth_client
|
||||
.exchange_code(AuthorizationCode::new(code.clone()))
|
||||
@@ -119,7 +118,7 @@ pub async fn handle_g_auth_redirect(
|
||||
.expect("email to parse to string")
|
||||
.to_string()
|
||||
} else {
|
||||
return Err(ServerFnError::new(format!(
|
||||
return Err(ServerFnError::ServerError(format!(
|
||||
"Response from google has status of {}",
|
||||
response.status()
|
||||
)));
|
||||
@@ -194,8 +193,6 @@ pub fn HandleGAuth() -> impl IntoView {
|
||||
|
||||
#[server]
|
||||
pub async fn logout() -> Result<(), ServerFnError> {
|
||||
use crate::ssr_imports::*;
|
||||
|
||||
let auth = auth()?;
|
||||
auth.logout_user();
|
||||
leptos_axum::redirect("/");
|
||||
|
||||
@@ -1,12 +1,18 @@
|
||||
use axum::extract::FromRef;
|
||||
use cfg_if::cfg_if;
|
||||
|
||||
cfg_if! {
|
||||
if #[cfg(feature = "ssr")] {
|
||||
use leptos::LeptosOptions;
|
||||
use sqlx::SqlitePool;
|
||||
use axum::extract::FromRef;
|
||||
|
||||
/// This takes advantage of Axum's SubStates feature by deriving FromRef. This is the only way to have more than one
|
||||
/// item in Axum's State. Leptos requires you to have leptosOptions in your State struct for the leptos route handlers
|
||||
#[derive(FromRef, Debug, Clone)]
|
||||
pub struct AppState {
|
||||
pub struct AppState{
|
||||
pub leptos_options: LeptosOptions,
|
||||
pub pool: SqlitePool,
|
||||
pub client: oauth2::basic::BasicClient,
|
||||
pub client:oauth2::basic::BasicClient,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ thiserror = "1"
|
||||
axum = { version = "0.7", optional = true }
|
||||
tower = { version = "0.4", optional = true }
|
||||
tower-http = { version = "0.5", features = ["fs"], optional = true }
|
||||
tokio = { version = "1", features = ["rt-multi-thread", "macros", "time"], optional = true }
|
||||
tokio = { version = "1", features = ["time"], optional = true }
|
||||
wasm-bindgen = "0.2"
|
||||
|
||||
[features]
|
||||
|
||||
@@ -16,7 +16,7 @@ leptos_axum = { path = "../../integrations/axum", optional = true }
|
||||
leptos_router = { path = "../../router", features = ["nightly"] }
|
||||
log = "0.4.17"
|
||||
simple_logger = "4"
|
||||
tokio = { version = "1", features = ["rt-multi-thread", "macros"], optional = true }
|
||||
tokio = { version = "1", optional = true }
|
||||
tower = { version = "0.4", optional = true }
|
||||
tower-http = { version = "0.5", features = ["fs"], optional = true }
|
||||
wasm-bindgen = "0.2"
|
||||
|
||||
@@ -22,9 +22,8 @@ pub mod ssr {
|
||||
}
|
||||
}
|
||||
|
||||
/// This is an example of a server function using an alternative CBOR encoding. Both the function arguments being sent
|
||||
/// to the server and the server response will be encoded with CBOR. Good for binary data that doesn't encode well via the default methods
|
||||
#[server(encoding = "Cbor")]
|
||||
/// Server functions can be given doc comments.
|
||||
#[server(GetTodos, "/api")]
|
||||
pub async fn get_todos() -> Result<Vec<Todo>, ServerFnError> {
|
||||
use self::ssr::*;
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ async fn main() {
|
||||
let app = Router::new()
|
||||
// server function handlers are normally set up by .leptos_routes()
|
||||
// here, we're not actually doing server side rendering, so we set up a manual
|
||||
// handler for the server fns
|
||||
// handler for the server fns
|
||||
// this should include a get() handler if you have any GetUrl-based server fns
|
||||
.route("/api/*fn_name", post(leptos_axum::handle_server_fns))
|
||||
.fallback(file_or_index_handler)
|
||||
|
||||
@@ -1369,7 +1369,8 @@ impl LeptosRoutes for &mut ServiceConfig {
|
||||
}
|
||||
}
|
||||
|
||||
/// A helper to make it easier to use Axum extractors in server functions.
|
||||
/// A helper to make it easier to use Axum extractors in server functions, with a
|
||||
/// simpler API than [`extract()`].
|
||||
///
|
||||
/// It is generic over some type `T` that implements [`FromRequest`] and can
|
||||
/// therefore be used in an extractor. The compiler can often infer this type.
|
||||
@@ -1382,21 +1383,19 @@ impl LeptosRoutes for &mut ServiceConfig {
|
||||
/// pub async fn query_extract() -> Result<MyQuery, ServerFnError> {
|
||||
/// use actix_web::web::Query;
|
||||
/// use leptos_actix::*;
|
||||
///
|
||||
/// let Query(data) = extract().await?;
|
||||
///
|
||||
/// // do something with the data
|
||||
///
|
||||
/// Ok(data)
|
||||
/// }
|
||||
/// ```
|
||||
pub async fn extract<T>() -> Result<T, ServerFnError>
|
||||
pub async fn extract<T, CustErr>() -> Result<T, ServerFnError<CustErr>>
|
||||
where
|
||||
T: actix_web::FromRequest,
|
||||
<T as FromRequest>::Error: Display,
|
||||
{
|
||||
let req = use_context::<HttpRequest>().ok_or_else(|| {
|
||||
ServerFnError::new("HttpRequest should have been provided via context")
|
||||
ServerFnError::ServerError(
|
||||
"HttpRequest should have been provided via context".to_string(),
|
||||
)
|
||||
})?;
|
||||
|
||||
T::extract(&req)
|
||||
|
||||
@@ -34,5 +34,5 @@ tokio = { version = "1", features = ["net"] }
|
||||
[features]
|
||||
nonce = ["leptos/nonce"]
|
||||
wasm = []
|
||||
default = ["tokio/fs", "tokio/sync"]
|
||||
default = ["tokio/full", "axum/macros"]
|
||||
experimental-islands = ["leptos_integration_utils/experimental-islands"]
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
//! To run in this environment, you need to disable the default feature set and enable
|
||||
//! the `wasm` feature on `leptos_axum` in your `Cargo.toml`.
|
||||
//! ```toml
|
||||
//! leptos_axum = { version = "0.6.0", default-features = false, features = ["wasm"] }
|
||||
//! leptos_axum = { version = "0.6.0-beta", default-features = false, features = ["wasm"] }
|
||||
//! ```
|
||||
//!
|
||||
//! ## Features
|
||||
@@ -55,7 +55,10 @@ use leptos_router::*;
|
||||
use once_cell::sync::OnceCell;
|
||||
use parking_lot::RwLock;
|
||||
use server_fn::redirect::REDIRECT_HEADER;
|
||||
use std::{fmt::Debug, io, pin::Pin, sync::Arc, thread::available_parallelism};
|
||||
use std::{
|
||||
error::Error, fmt::Debug, io, pin::Pin, sync::Arc,
|
||||
thread::available_parallelism,
|
||||
};
|
||||
use tokio_util::task::LocalPoolHandle;
|
||||
use tracing::Instrument;
|
||||
|
||||
@@ -161,7 +164,7 @@ pub fn generate_request_and_parts(
|
||||
}
|
||||
|
||||
/// An Axum handlers to listens for a request with Leptos server function arguments in the body,
|
||||
/// run the server function if found, and return the resulting [`Response`].
|
||||
/// run the server function if found, and return the resulting [Response].
|
||||
///
|
||||
/// This can then be set up at an appropriate route in your application:
|
||||
///
|
||||
@@ -221,7 +224,7 @@ macro_rules! spawn_task {
|
||||
}
|
||||
|
||||
/// An Axum handlers to listens for a request with Leptos server function arguments in the body,
|
||||
/// run the server function if found, and return the resulting [`Response`].
|
||||
/// run the server function if found, and return the resulting [Response].
|
||||
///
|
||||
/// This can then be set up at an appropriate route in your application:
|
||||
///
|
||||
@@ -231,16 +234,15 @@ macro_rules! spawn_task {
|
||||
/// of one that should work much like this one.
|
||||
///
|
||||
/// **NOTE**: If your server functions expect a context, make sure to provide it both in
|
||||
/// [`handle_server_fns_with_context`] **and** in
|
||||
/// [`leptos_routes_with_context`](LeptosRoutes::leptos_routes_with_context) (or whatever
|
||||
/// [`handle_server_fns_with_context`] **and** in [`leptos_routes_with_context`] (or whatever
|
||||
/// rendering method you are using). During SSR, server functions are called by the rendering
|
||||
/// method, while subsequent calls from the client are handled by the server function handler.
|
||||
/// The same context needs to be provided to both handlers.
|
||||
///
|
||||
/// ## Provided Context Types
|
||||
/// This function always provides context values including the following types:
|
||||
/// - [`Parts`]
|
||||
/// - [`ResponseOptions`]
|
||||
/// - [RequestParts]
|
||||
/// - [ResponseOptions]
|
||||
#[tracing::instrument(level = "trace", fields(error), skip_all)]
|
||||
pub async fn handle_server_fns_with_context(
|
||||
additional_context: impl Fn() + 'static + Clone + Send,
|
||||
@@ -472,10 +474,10 @@ where
|
||||
///
|
||||
/// ## Provided Context Types
|
||||
/// This function always provides context values including the following types:
|
||||
/// - [`Parts`]
|
||||
/// - [`ResponseOptions`]
|
||||
/// - [`MetaContext`](leptos_meta::MetaContext)
|
||||
/// - [`RouterIntegrationContext`](leptos_router::RouterIntegrationContext)
|
||||
/// - [Parts]
|
||||
/// - [ResponseOptions]
|
||||
/// - [MetaContext](leptos_meta::MetaContext)
|
||||
/// - [RouterIntegrationContext](leptos_router::RouterIntegrationContext)
|
||||
#[tracing::instrument(level = "trace", fields(error), skip_all)]
|
||||
pub fn render_app_to_stream_in_order<IV>(
|
||||
options: LeptosOptions,
|
||||
@@ -513,10 +515,10 @@ where
|
||||
///
|
||||
/// ## Provided Context Types
|
||||
/// This function always provides context values including the following types:
|
||||
/// - [`Parts`]
|
||||
/// - [`ResponseOptions`]
|
||||
/// - [`MetaContext`](leptos_meta::MetaContext)
|
||||
/// - [`RouterIntegrationContext`](leptos_router::RouterIntegrationContext)
|
||||
/// - [Parts]
|
||||
/// - [ResponseOptions]
|
||||
/// - [MetaContext](leptos_meta::MetaContext)
|
||||
/// - [RouterIntegrationContext](leptos_router::RouterIntegrationContext)
|
||||
#[tracing::instrument(level = "trace", fields(error), skip_all)]
|
||||
pub fn render_app_to_stream_with_context<IV>(
|
||||
options: LeptosOptions,
|
||||
@@ -624,10 +626,10 @@ where
|
||||
///
|
||||
/// ## Provided Context Types
|
||||
/// This function always provides context values including the following types:
|
||||
/// - [`Parts`]
|
||||
/// - [`ResponseOptions`]
|
||||
/// - [`MetaContext`](leptos_meta::MetaContext)
|
||||
/// - [`RouterIntegrationContext`](leptos_router::RouterIntegrationContext)
|
||||
/// - [Parts]
|
||||
/// - [ResponseOptions]
|
||||
/// - [MetaContext](leptos_meta::MetaContext)
|
||||
/// - [RouterIntegrationContext](leptos_router::RouterIntegrationContext)
|
||||
#[tracing::instrument(level = "trace", fields(error), skip_all)]
|
||||
pub fn render_app_to_stream_with_context_and_replace_blocks<IV>(
|
||||
options: LeptosOptions,
|
||||
@@ -785,10 +787,10 @@ async fn forward_stream(
|
||||
///
|
||||
/// ## Provided Context Types
|
||||
/// This function always provides context values including the following types:
|
||||
/// - [`Parts`]
|
||||
/// - [`ResponseOptions`]
|
||||
/// - [`MetaContext`](leptos_meta::MetaContext)
|
||||
/// - [`RouterIntegrationContext`](leptos_router::RouterIntegrationContext)
|
||||
/// - [Parts]
|
||||
/// - [ResponseOptions]
|
||||
/// - [MetaContext](leptos_meta::MetaContext)
|
||||
/// - [RouterIntegrationContext](leptos_router::RouterIntegrationContext)
|
||||
#[tracing::instrument(level = "trace", fields(error), skip_all)]
|
||||
pub fn render_app_to_stream_in_order_with_context<IV>(
|
||||
options: LeptosOptions,
|
||||
@@ -915,10 +917,10 @@ fn provide_contexts(
|
||||
///
|
||||
/// ## Provided Context Types
|
||||
/// This function always provides context values including the following types:
|
||||
/// - [`Parts`]
|
||||
/// - [`ResponseOptions`]
|
||||
/// - [`MetaContext`](leptos_meta::MetaContext)
|
||||
/// - [`RouterIntegrationContext`](leptos_router::RouterIntegrationContext)
|
||||
/// - [Parts]
|
||||
/// - [ResponseOptions]
|
||||
/// - [MetaContext](leptos_meta::MetaContext)
|
||||
/// - [RouterIntegrationContext](leptos_router::RouterIntegrationContext)
|
||||
#[tracing::instrument(level = "trace", fields(error), skip_all)]
|
||||
pub fn render_app_async<IV>(
|
||||
options: LeptosOptions,
|
||||
@@ -957,10 +959,10 @@ where
|
||||
///
|
||||
/// ## Provided Context Types
|
||||
/// This function always provides context values including the following types:
|
||||
/// - [`Parts`]
|
||||
/// - [`ResponseOptions`]
|
||||
/// - [`MetaContext`](leptos_meta::MetaContext)
|
||||
/// - [`RouterIntegrationContext`](leptos_router::RouterIntegrationContext)
|
||||
/// - [Parts]
|
||||
/// - [ResponseOptions]
|
||||
/// - [MetaContext](leptos_meta::MetaContext)
|
||||
/// - [RouterIntegrationContext](leptos_router::RouterIntegrationContext)
|
||||
#[tracing::instrument(level = "trace", fields(error), skip_all)]
|
||||
pub fn render_app_async_stream_with_context<IV>(
|
||||
options: LeptosOptions,
|
||||
@@ -1086,10 +1088,10 @@ where
|
||||
///
|
||||
/// ## Provided Context Types
|
||||
/// This function always provides context values including the following types:
|
||||
/// - [`Parts`]
|
||||
/// - [`ResponseOptions`]
|
||||
/// - [`MetaContext`](leptos_meta::MetaContext)
|
||||
/// - [`RouterIntegrationContext`](leptos_router::RouterIntegrationContext)
|
||||
/// - [Parts]
|
||||
/// - [ResponseOptions]
|
||||
/// - [MetaContext](leptos_meta::MetaContext)
|
||||
/// - [RouterIntegrationContext](leptos_router::RouterIntegrationContext)
|
||||
#[tracing::instrument(level = "trace", fields(error), skip_all)]
|
||||
pub fn render_app_async_with_context<IV>(
|
||||
options: LeptosOptions,
|
||||
@@ -1764,17 +1766,18 @@ fn get_leptos_pool() -> LocalPoolHandle {
|
||||
/// pub async fn query_extract() -> Result<MyQuery, ServerFnError> {
|
||||
/// use axum::{extract::Query, http::Method};
|
||||
/// use leptos_axum::*;
|
||||
/// let Query(query) = extract().await?;
|
||||
/// let Query(query) = extractor().await?;
|
||||
///
|
||||
/// Ok(query)
|
||||
/// }
|
||||
/// ```
|
||||
pub async fn extract<T>() -> Result<T, ServerFnError>
|
||||
pub async fn extractor<T, CustErr>() -> Result<T, ServerFnError>
|
||||
where
|
||||
T: Sized + FromRequestParts<()>,
|
||||
T::Rejection: Debug,
|
||||
CustErr: Error + 'static,
|
||||
{
|
||||
extract_with_state::<T, ()>(&()).await
|
||||
extractor_with_state::<T, (), CustErr>(&()).await
|
||||
}
|
||||
|
||||
/// A helper to make it easier to use Axum extractors in server functions. This
|
||||
@@ -1791,22 +1794,26 @@ where
|
||||
/// pub async fn query_extract() -> Result<MyQuery, ServerFnError> {
|
||||
/// use axum::{extract::Query, http::Method};
|
||||
/// use leptos_axum::*;
|
||||
/// let Query(query) = extract().await?;
|
||||
/// let Query(query) = extractor().await?;
|
||||
///
|
||||
/// Ok(query)
|
||||
/// }
|
||||
/// ```
|
||||
pub async fn extract_with_state<T, S>(state: &S) -> Result<T, ServerFnError>
|
||||
pub async fn extractor_with_state<T, S, CustErr>(
|
||||
state: &S,
|
||||
) -> Result<T, ServerFnError>
|
||||
where
|
||||
T: Sized + FromRequestParts<S>,
|
||||
T::Rejection: Debug,
|
||||
CustErr: Error + 'static,
|
||||
{
|
||||
let mut parts = use_context::<Parts>().ok_or_else(|| {
|
||||
ServerFnError::new(
|
||||
"should have had Parts provided by the leptos_axum integration",
|
||||
ServerFnError::ServerError::<CustErr>(
|
||||
"should have had Parts provided by the leptos_axum integration"
|
||||
.to_string(),
|
||||
)
|
||||
})?;
|
||||
T::from_request_parts(&mut parts, state)
|
||||
T::from_request_parts(&mut parts, &state)
|
||||
.await
|
||||
.map_err(|e| ServerFnError::ServerError(format!("{e:?}")))
|
||||
}
|
||||
|
||||
24
integrations/pavex/Cargo.toml
Normal file
24
integrations/pavex/Cargo.toml
Normal file
@@ -0,0 +1,24 @@
|
||||
[package]
|
||||
name = "leptos_pavex"
|
||||
version = { workspace = true }
|
||||
edition = "2021"
|
||||
authors = ["Ben Wishovich"]
|
||||
license = "MIT"
|
||||
repository = "https://github.com/leptos-rs/leptos"
|
||||
description = "Pavex integrations for the Leptos web framework."
|
||||
|
||||
[dependencies]
|
||||
pavex = { git = "https://github.com/LukeMathWalker/pavex", branch = "main" }
|
||||
futures = "0.3"
|
||||
leptos = { workspace = true, features = ["ssr"] }
|
||||
leptos_meta = { workspace = true, features = ["ssr"] }
|
||||
leptos_router = { workspace = true, features = ["ssr"] }
|
||||
leptos_integration_utils = { workspace = true }
|
||||
parking_lot = "0.12.1"
|
||||
regex = "1.7.0"
|
||||
tracing = "0.1.37"
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
|
||||
[features]
|
||||
nonce = ["leptos/nonce"]
|
||||
experimental-islands = ["leptos_integration_utils/experimental-islands"]
|
||||
4
integrations/pavex/Makefile.toml
Normal file
4
integrations/pavex/Makefile.toml
Normal file
@@ -0,0 +1,4 @@
|
||||
extend = { path = "../../cargo-make/main.toml" }
|
||||
|
||||
[tasks.check-format]
|
||||
env = { LEPTOS_PROJECT_DIRECTORY = "../../" }
|
||||
1554
integrations/pavex/src/lib.rs
Normal file
1554
integrations/pavex/src/lib.rs
Normal file
File diff suppressed because it is too large
Load Diff
@@ -13,7 +13,7 @@ use typed_builder::TypedBuilder;
|
||||
|
||||
/// A Struct to allow us to parse LeptosOptions from the file. Not really needed, most interactions should
|
||||
/// occur with LeptosOptions
|
||||
#[derive(Clone, Debug, serde::Deserialize, Default)]
|
||||
#[derive(Clone, Debug, serde::Deserialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub struct ConfFile {
|
||||
pub leptos_options: LeptosOptions,
|
||||
@@ -27,7 +27,7 @@ pub struct ConfFile {
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub struct LeptosOptions {
|
||||
/// The name of the WASM and JS files generated by wasm-bindgen. Defaults to the crate name with underscores instead of dashes
|
||||
#[builder(setter(into), default=default_output_name())]
|
||||
#[builder(setter(into))]
|
||||
pub output_name: String,
|
||||
/// The path of the all the files generated by cargo-leptos. This defaults to '.' for convenience when integrating with other
|
||||
/// tools.
|
||||
@@ -112,16 +112,6 @@ impl LeptosOptions {
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for LeptosOptions {
|
||||
fn default() -> Self {
|
||||
LeptosOptions::builder().build()
|
||||
}
|
||||
}
|
||||
|
||||
fn default_output_name() -> String {
|
||||
env!("CARGO_CRATE_NAME").replace('-', "_")
|
||||
}
|
||||
|
||||
fn default_site_root() -> String {
|
||||
".".to_string()
|
||||
}
|
||||
|
||||
@@ -363,13 +363,6 @@ fn fragments_to_chunks(
|
||||
|
||||
impl View {
|
||||
/// Consumes the node and renders it into an HTML string.
|
||||
///
|
||||
/// This is __NOT__ the same as [`render_to_string`]. This
|
||||
/// functions differs in that it assumes a runtime is in scope.
|
||||
/// [`render_to_string`] creates, and disposes of a runtime for you.
|
||||
///
|
||||
/// # Panics
|
||||
/// When called in a scope without a runtime. Use [`render_to_string`] instead.
|
||||
#[cfg_attr(
|
||||
any(debug_assertions, feature = "ssr"),
|
||||
instrument(level = "trace", skip_all,)
|
||||
|
||||
@@ -6,7 +6,7 @@ use convert_case::{
|
||||
use itertools::Itertools;
|
||||
use leptos_hot_reload::parsing::value_to_string;
|
||||
use proc_macro2::{Ident, Span, TokenStream};
|
||||
use quote::{format_ident, quote, quote_spanned, ToTokens, TokenStreamExt};
|
||||
use quote::{format_ident, quote_spanned, ToTokens, TokenStreamExt};
|
||||
use syn::{
|
||||
parse::Parse, parse_quote, spanned::Spanned,
|
||||
AngleBracketedGenericArguments, Attribute, FnArg, GenericArgument, Item,
|
||||
|
||||
@@ -2,9 +2,6 @@
|
||||
#![forbid(unsafe_code)]
|
||||
// to prevent warnings from popping up when a nightly feature is stabilized
|
||||
#![allow(stable_features)]
|
||||
// FIXME? every use of quote! {} is warning here -- false positive?
|
||||
#![allow(unknown_lints)]
|
||||
#![allow(private_macro_use)]
|
||||
|
||||
#[macro_use]
|
||||
extern crate proc_macro_error;
|
||||
@@ -12,7 +9,7 @@ extern crate proc_macro_error;
|
||||
use component::DummyModel;
|
||||
use proc_macro::TokenStream;
|
||||
use proc_macro2::{Span, TokenTree};
|
||||
use quote::{quote, ToTokens};
|
||||
use quote::ToTokens;
|
||||
use rstml::{node::KeyedAttribute, parse};
|
||||
use syn::{parse_macro_input, spanned::Spanned, token::Pub, Visibility};
|
||||
|
||||
@@ -874,15 +871,12 @@ pub fn slot(args: proc_macro::TokenStream, s: TokenStream) -> TokenStream {
|
||||
/// relative to the prefix (defaults to the function name followed by unique hash)
|
||||
/// - `input`: the encoding for the arguments (defaults to `PostUrl`)
|
||||
/// - `output`: the encoding for the response (defaults to `Json`)
|
||||
/// - `client`: a custom `Client` implementation that will be used for this server fn
|
||||
/// - `encoding`: (legacy, may be deprecated in future) specifies the encoding, which may be one
|
||||
/// of the following (not case sensitive)
|
||||
/// - `"Url"`: `POST` request with URL-encoded arguments and JSON response
|
||||
/// - `"GetUrl"`: `GET` request with URL-encoded arguments and JSON response
|
||||
/// - `"Cbor"`: `POST` request with CBOR-encoded arguments and response
|
||||
/// - `"GetCbor"`: `GET` request with URL-encoded arguments and CBOR response
|
||||
/// - `req` and `res` specify the HTTP request and response types to be used on the server (these
|
||||
/// should usually only be necessary if you are integrating with a server other than Actix/Axum)
|
||||
///
|
||||
/// ```rust,ignore
|
||||
/// #[server(
|
||||
@@ -915,16 +909,6 @@ pub fn slot(args: proc_macro::TokenStream, s: TokenStream) -> TokenStream {
|
||||
/// Whatever encoding is provided to `input` should implement `IntoReq` and `FromReq`. Whatever encoding is provided
|
||||
/// to `output` should implement `IntoRes` and `FromRes`.
|
||||
///
|
||||
/// ## Default Values for Parameters
|
||||
///
|
||||
/// Individual function parameters can be annotated with `#[server(default)]`, which will pass
|
||||
/// through `#[serde(default)]`. This is useful for the empty values of arguments with some
|
||||
/// encodings. The URL encoding, for example, omits a field entirely if it is an empty `Vec<_>`,
|
||||
/// but this causes a deserialization error: the correct solution is to add `#[server(default)]`.
|
||||
/// ```rust,ignore
|
||||
/// pub async fn with_default_value(#[server(default)] values: Vec<u32>) /* etc. */
|
||||
/// ```
|
||||
///
|
||||
/// ## Important Notes
|
||||
/// - **Server functions must be `async`.** Even if the work being done inside the function body
|
||||
/// can run synchronously on the server, from the client’s perspective it involves an asynchronous
|
||||
@@ -965,8 +949,6 @@ pub fn server(args: proc_macro::TokenStream, s: TokenStream) -> TokenStream {
|
||||
s.into(),
|
||||
Some(syn::parse_quote!(::leptos::server_fn)),
|
||||
"/api",
|
||||
None,
|
||||
None,
|
||||
) {
|
||||
Err(e) => e.to_compile_error().into(),
|
||||
Ok(s) => s.to_token_stream().into(),
|
||||
|
||||
@@ -3,7 +3,7 @@ use crate::component::{
|
||||
};
|
||||
use attribute_derive::Attribute as AttributeDerive;
|
||||
use proc_macro2::{Ident, TokenStream};
|
||||
use quote::{quote, ToTokens, TokenStreamExt};
|
||||
use quote::{ToTokens, TokenStreamExt};
|
||||
use syn::{
|
||||
parse::Parse, parse_quote, Field, ItemStruct, LitStr, Meta, Type,
|
||||
Visibility,
|
||||
|
||||
@@ -357,12 +357,7 @@ impl<T> SignalWithUntracked for Memo<T> {
|
||||
#[inline]
|
||||
fn try_with_untracked<O>(&self, f: impl FnOnce(&T) -> O) -> Option<O> {
|
||||
with_runtime(|runtime| {
|
||||
self.id
|
||||
.try_with_no_subscription(runtime, |v: &Option<T>| {
|
||||
v.as_ref().map(f)
|
||||
})
|
||||
.ok()
|
||||
.flatten()
|
||||
self.id.try_with_no_subscription(runtime, |v: &T| f(v)).ok()
|
||||
})
|
||||
.ok()
|
||||
.flatten()
|
||||
|
||||
@@ -8,8 +8,8 @@ use crate::{
|
||||
signal_prelude::format_signal_warning, spawn::spawn_local,
|
||||
suspense::LocalStatus, use_context, GlobalSuspenseContext, Memo,
|
||||
ReadSignal, ScopeProperty, Signal, SignalDispose, SignalGet,
|
||||
SignalGetUntracked, SignalSet, SignalUpdate, SignalWith,
|
||||
SignalWithUntracked, SuspenseContext, WriteSignal,
|
||||
SignalGetUntracked, SignalSet, SignalUpdate, SignalWith, SuspenseContext,
|
||||
WriteSignal,
|
||||
};
|
||||
use std::{
|
||||
any::Any,
|
||||
@@ -244,7 +244,6 @@ where
|
||||
create_isomorphic_effect({
|
||||
let r = Rc::clone(&r);
|
||||
move |_| {
|
||||
source.track();
|
||||
load_resource(id, r.clone());
|
||||
}
|
||||
});
|
||||
@@ -385,10 +384,7 @@ where
|
||||
// client
|
||||
create_render_effect({
|
||||
let r = Rc::clone(&r);
|
||||
move |_| {
|
||||
source.track();
|
||||
r.load(false, id)
|
||||
}
|
||||
move |_| r.load(false, id)
|
||||
});
|
||||
|
||||
Resource {
|
||||
@@ -1362,7 +1358,7 @@ where
|
||||
self.version.set(version);
|
||||
self.scheduled.set(false);
|
||||
|
||||
_ = self.source.try_with_untracked(|source| {
|
||||
_ = self.source.try_with(|source| {
|
||||
let fut = (self.fetcher)(source.clone());
|
||||
|
||||
// `scheduled` is true for the rest of this code only
|
||||
|
||||
@@ -93,8 +93,8 @@ where
|
||||
any(debug_assertions, feature = "ssr"),
|
||||
tracing::instrument(level = "trace", skip_all,)
|
||||
)]
|
||||
pub fn dispatch(&self, input: I) {
|
||||
self.0.with_value(|a| a.dispatch(input))
|
||||
pub fn dispatch(&self, input: impl Into<I>) {
|
||||
self.0.with_value(|a| a.dispatch(input.into()))
|
||||
}
|
||||
|
||||
/// Create an [Action].
|
||||
|
||||
@@ -321,7 +321,7 @@ where
|
||||
|
||||
/// Creates an [MultiAction] that can be used to call a server function.
|
||||
///
|
||||
/// ```rust,ignore
|
||||
/// ```rust
|
||||
/// # use leptos::*;
|
||||
///
|
||||
/// #[server(MyServerFn)]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "leptos_meta"
|
||||
version = "0.6.3"
|
||||
version = "0.6.0-beta"
|
||||
edition = "2021"
|
||||
authors = ["Greg Johnston"]
|
||||
license = "MIT"
|
||||
@@ -20,10 +20,10 @@ features = ["HtmlLinkElement", "HtmlMetaElement", "HtmlTitleElement"]
|
||||
|
||||
[features]
|
||||
default = []
|
||||
csr = ["leptos/csr"]
|
||||
hydrate = ["leptos/hydrate"]
|
||||
ssr = ["leptos/ssr"]
|
||||
nightly = ["leptos/nightly"]
|
||||
csr = ["leptos/csr", "leptos/tracing"]
|
||||
hydrate = ["leptos/hydrate", "leptos/tracing"]
|
||||
ssr = ["leptos/ssr", "leptos/tracing"]
|
||||
nightly = ["leptos/nightly", "leptos/tracing"]
|
||||
|
||||
[package.metadata.cargo-all-features]
|
||||
denylist = ["nightly"]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "leptos_router"
|
||||
version = "0.6.3"
|
||||
version = "0.6.0-beta"
|
||||
edition = "2021"
|
||||
authors = ["Greg Johnston"]
|
||||
license = "MIT"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#[cfg(feature = "ssr")]
|
||||
use crate::{RouteListing, RouterIntegrationContext, ServerIntegration};
|
||||
#[cfg(feature = "ssr")]
|
||||
use leptos::{create_runtime, provide_context, IntoView, LeptosOptions};
|
||||
use leptos::{provide_context, IntoView, LeptosOptions};
|
||||
#[cfg(feature = "ssr")]
|
||||
use leptos_meta::MetaContext;
|
||||
use linear_map::LinearMap;
|
||||
@@ -204,8 +204,9 @@ impl ResolvedStaticPath {
|
||||
IV: IntoView + 'static,
|
||||
{
|
||||
let html = self.build(options, app_fn, additional_context).await;
|
||||
let file_path = static_file_path(options, &self.0);
|
||||
let path = Path::new(&file_path);
|
||||
let path = Path::new(&options.site_root)
|
||||
.join(format!("{}.static.html", self.0.trim_start_matches('/')));
|
||||
|
||||
if let Some(path) = path.parent() {
|
||||
std::fs::create_dir_all(path)?
|
||||
}
|
||||
@@ -246,15 +247,12 @@ where
|
||||
IV: IntoView + 'static,
|
||||
{
|
||||
let mut static_data: HashMap<&str, StaticParamsMap> = HashMap::new();
|
||||
let runtime = create_runtime();
|
||||
additional_context();
|
||||
for (key, value) in static_data_map {
|
||||
match value {
|
||||
Some(value) => static_data.insert(key, value.as_ref()().await),
|
||||
None => static_data.insert(key, StaticParamsMap::default()),
|
||||
};
|
||||
}
|
||||
runtime.dispose();
|
||||
let static_routes = routes
|
||||
.iter()
|
||||
.filter(|route| route.static_mode().is_some())
|
||||
@@ -279,6 +277,33 @@ where
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
#[cfg(feature = "ssr")]
|
||||
pub fn purge_dir_of_static_files(path: PathBuf) -> Result<(), std::io::Error> {
|
||||
for entry in path.read_dir()? {
|
||||
let entry = entry?;
|
||||
let path = entry.path();
|
||||
if path.is_dir() {
|
||||
purge_dir_of_static_files(path)?;
|
||||
} else if path.is_file() {
|
||||
if let Some(name) = path.file_name().and_then(|i| i.to_str()) {
|
||||
if name.ends_with(".static.html") {
|
||||
std::fs::remove_file(path)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Purge all statically generated route files
|
||||
#[cfg(feature = "ssr")]
|
||||
pub fn purge_all_static_routes<IV>(
|
||||
options: &LeptosOptions,
|
||||
) -> Result<(), std::io::Error> {
|
||||
purge_dir_of_static_files(Path::new(&options.site_root).to_path_buf())
|
||||
}
|
||||
|
||||
pub type StaticData = Arc<StaticDataFn>;
|
||||
|
||||
pub type StaticDataFn = dyn Fn() -> Pin<Box<dyn Future<Output = StaticParamsMap> + Send + Sync>>
|
||||
@@ -325,20 +350,17 @@ pub enum StaticResponse {
|
||||
#[inline(always)]
|
||||
#[cfg(feature = "ssr")]
|
||||
pub fn static_file_path(options: &LeptosOptions, path: &str) -> String {
|
||||
let trimmed_path = path.trim_start_matches('/');
|
||||
let path = if trimmed_path.is_empty() {
|
||||
"index"
|
||||
} else {
|
||||
trimmed_path
|
||||
};
|
||||
format!("{}/{}.html", options.site_root, path)
|
||||
format!("{}{}.static.html", options.site_root, path)
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
#[inline(always)]
|
||||
#[cfg(feature = "ssr")]
|
||||
pub fn not_found_path(options: &LeptosOptions) -> String {
|
||||
format!("{}{}.html", options.site_root, options.not_found_path)
|
||||
format!(
|
||||
"{}{}.static.html",
|
||||
options.site_root, options.not_found_path
|
||||
)
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
@@ -421,6 +443,7 @@ where
|
||||
let body = ResolvedStaticPath(path.into())
|
||||
.build(options, app_fn, additional_context)
|
||||
.await;
|
||||
let path = Path::new(&static_file_path(options, path)).into();
|
||||
let path = Path::new(&options.site_root)
|
||||
.join(format!("{}.static.html", path.trim_start_matches('/')));
|
||||
StaticResponse::WriteFile { body, path }
|
||||
}
|
||||
|
||||
@@ -2,14 +2,14 @@
|
||||
name = "server_fn"
|
||||
version = { workspace = true }
|
||||
edition = "2021"
|
||||
authors = ["Greg Johnston", "Ben Wishovich"]
|
||||
authors = ["Greg Johnston"]
|
||||
license = "MIT"
|
||||
repository = "https://github.com/leptos-rs/leptos"
|
||||
description = "RPC for any web framework."
|
||||
readme = "../README.md"
|
||||
|
||||
[dependencies]
|
||||
server_fn_macro_default = { workspace = true }
|
||||
server_fn_macro_default = { workspace = true}
|
||||
# used for hashing paths in #[server] macro
|
||||
const_format = "0.2"
|
||||
xxhash-rust = { version = "0.8", features = ["const_xxh64"] }
|
||||
@@ -18,7 +18,7 @@ serde = { version = "1", features = ["derive"] }
|
||||
send_wrapper = { version = "0.6", features = ["futures"], optional = true }
|
||||
|
||||
# registration system
|
||||
inventory = { version = "0.3", optional = true }
|
||||
inventory = {version="0.3",optional=true}
|
||||
dashmap = "5"
|
||||
once_cell = "1"
|
||||
|
||||
@@ -72,11 +72,11 @@ reqwest = { version = "0.11", default-features = false, optional = true, feature
|
||||
url = "2"
|
||||
|
||||
[features]
|
||||
default = ["json", "cbor"]
|
||||
default = [ "json", "cbor"]
|
||||
form-redirects = []
|
||||
actix = ["ssr", "dep:actix-web", "dep:send_wrapper"]
|
||||
axum = [
|
||||
"ssr",
|
||||
"ssr",
|
||||
"dep:axum",
|
||||
"dep:hyper",
|
||||
"dep:http-body-util",
|
||||
@@ -94,7 +94,7 @@ browser = [
|
||||
]
|
||||
json = []
|
||||
serde-lite = ["dep:serde-lite"]
|
||||
multipart = ["browser", "dep:multer"]
|
||||
multipart = ["dep:multer"]
|
||||
url = ["dep:serde_qs"]
|
||||
cbor = ["dep:ciborium"]
|
||||
rkyv = ["dep:rkyv"]
|
||||
@@ -105,25 +105,3 @@ ssr = ["inventory"]
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
all-features = true
|
||||
|
||||
# disables some feature combos for testing in CI
|
||||
[package.metadata.cargo-all-features]
|
||||
denylist = ["rustls", "default-tls", "form-redirects"]
|
||||
skip_feature_sets = [
|
||||
[
|
||||
"actix",
|
||||
"axum",
|
||||
],
|
||||
[
|
||||
"browser",
|
||||
"actix",
|
||||
],
|
||||
[
|
||||
"browser",
|
||||
"axum",
|
||||
],
|
||||
[
|
||||
"browser",
|
||||
"reqwest",
|
||||
],
|
||||
]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "server_fn_macro_default"
|
||||
version = { workspace = true }
|
||||
version = "0.6.0"
|
||||
edition = "2021"
|
||||
authors = ["Greg Johnston"]
|
||||
license = "MIT"
|
||||
|
||||
@@ -34,15 +34,12 @@ use syn::__private::ToTokens;
|
||||
/// relative to the prefix (defaults to the function name followed by unique hash)
|
||||
/// - `input`: the encoding for the arguments (defaults to `PostUrl`)
|
||||
/// - `output`: the encoding for the response (defaults to `Json`)
|
||||
/// - `client`: a custom `Client` implementation that will be used for this server fn
|
||||
/// - `encoding`: (legacy, may be deprecated in future) specifies the encoding, which may be one
|
||||
/// of the following (not case sensitive)
|
||||
/// - `"Url"`: `POST` request with URL-encoded arguments and JSON response
|
||||
/// - `"GetUrl"`: `GET` request with URL-encoded arguments and JSON response
|
||||
/// - `"Cbor"`: `POST` request with CBOR-encoded arguments and response
|
||||
/// - `"GetCbor"`: `GET` request with URL-encoded arguments and CBOR response
|
||||
/// - `req` and `res` specify the HTTP request and response types to be used on the server (these
|
||||
/// should usually only be necessary if you are integrating with a server other than Actix/Axum)
|
||||
/// ```rust,ignore
|
||||
/// #[server(
|
||||
/// name = SomeStructName,
|
||||
@@ -74,8 +71,6 @@ pub fn server(args: proc_macro::TokenStream, s: TokenStream) -> TokenStream {
|
||||
s.into(),
|
||||
Some(syn::parse_quote!(server_fns)),
|
||||
"/api",
|
||||
None,
|
||||
None,
|
||||
) {
|
||||
Err(e) => e.to_compile_error().into(),
|
||||
Ok(s) => s.to_token_stream().into(),
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user