mirror of
https://github.com/leptos-rs/leptos.git
synced 2025-12-27 15:44:42 -05:00
Compare commits
84 Commits
docs-clean
...
pavex
| 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 |
1
.github/workflows/ci.yml
vendored
1
.github/workflows/ci.yml
vendored
@@ -22,7 +22,6 @@ jobs:
|
||||
[
|
||||
integrations/actix,
|
||||
integrations/axum,
|
||||
integrations/viz,
|
||||
integrations/utils,
|
||||
leptos,
|
||||
leptos_config,
|
||||
|
||||
29
Cargo.toml
29
Cargo.toml
@@ -16,7 +16,6 @@ members = [
|
||||
# integrations
|
||||
"integrations/actix",
|
||||
"integrations/axum",
|
||||
"integrations/viz",
|
||||
"integrations/utils",
|
||||
|
||||
# libraries
|
||||
@@ -26,22 +25,22 @@ members = [
|
||||
exclude = ["benchmarks", "examples"]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.5.6"
|
||||
version = "0.6.0-beta"
|
||||
|
||||
[workspace.dependencies]
|
||||
leptos = { path = "./leptos", version = "0.5.6" }
|
||||
leptos_dom = { path = "./leptos_dom", version = "0.5.6" }
|
||||
leptos_hot_reload = { path = "./leptos_hot_reload", version = "0.5.6" }
|
||||
leptos_macro = { path = "./leptos_macro", version = "0.5.6" }
|
||||
leptos_reactive = { path = "./leptos_reactive", version = "0.5.6" }
|
||||
leptos_server = { path = "./leptos_server", version = "0.5.6" }
|
||||
server_fn = { path = "./server_fn", version = "0.5.6" }
|
||||
server_fn_macro = { path = "./server_fn_macro", version = "0.5.6" }
|
||||
server_fn_macro_default = { path = "./server_fn/server_fn_macro_default", version = "0.5.6" }
|
||||
leptos_config = { path = "./leptos_config", version = "0.5.6" }
|
||||
leptos_router = { path = "./router", version = "0.5.6" }
|
||||
leptos_meta = { path = "./meta", version = "0.5.6" }
|
||||
leptos_integration_utils = { path = "./integrations/utils", version = "0.5.6" }
|
||||
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.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
|
||||
|
||||
@@ -21,6 +21,7 @@ CARGO_MAKE_CRATE_WORKSPACE_MEMBERS = [
|
||||
"login_with_token_csr_only",
|
||||
"parent_child",
|
||||
"router",
|
||||
"server_fns_axum",
|
||||
"session_auth_axum",
|
||||
"slots",
|
||||
"ssr_modes",
|
||||
@@ -32,7 +33,6 @@ CARGO_MAKE_CRATE_WORKSPACE_MEMBERS = [
|
||||
"timer",
|
||||
"todo_app_sqlite",
|
||||
"todo_app_sqlite_axum",
|
||||
"todo_app_sqlite_viz",
|
||||
"todomvc",
|
||||
]
|
||||
|
||||
|
||||
90
examples/action-form-error-handling/Cargo.toml
Normal file
90
examples/action-form-error-handling/Cargo.toml
Normal file
@@ -0,0 +1,90 @@
|
||||
[package]
|
||||
name = "action-form-error-handling"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib", "rlib"]
|
||||
|
||||
[dependencies]
|
||||
actix-files = { version = "0.6", optional = true }
|
||||
actix-web = { version = "4", optional = true, features = ["macros"] }
|
||||
console_error_panic_hook = "0.1"
|
||||
cfg-if = "1"
|
||||
http = { version = "0.2", optional = true }
|
||||
leptos = { path = "../../leptos" }
|
||||
leptos_meta = { path = "../../meta" }
|
||||
leptos_actix = { path = "../../integrations/actix", optional = true }
|
||||
leptos_router = { path = "../../router" }
|
||||
wasm-bindgen = "0.2"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
|
||||
[features]
|
||||
csr = ["leptos/csr", "leptos_meta/csr", "leptos_router/csr"]
|
||||
hydrate = ["leptos/hydrate", "leptos_meta/hydrate", "leptos_router/hydrate"]
|
||||
ssr = [
|
||||
"dep:actix-files",
|
||||
"dep:actix-web",
|
||||
"dep:leptos_actix",
|
||||
"leptos/ssr",
|
||||
"leptos_meta/ssr",
|
||||
"leptos_router/ssr",
|
||||
]
|
||||
|
||||
# Defines a size-optimized profile for the WASM bundle in release mode
|
||||
[profile.wasm-release]
|
||||
inherits = "release"
|
||||
opt-level = 'z'
|
||||
lto = true
|
||||
codegen-units = 1
|
||||
panic = "abort"
|
||||
|
||||
[package.metadata.leptos]
|
||||
# The name used by wasm-bindgen/cargo-leptos for the JS/WASM bundle. Defaults to the crate name
|
||||
output-name = "leptos_start"
|
||||
# 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"
|
||||
# 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 = ["ssr"]
|
||||
|
||||
# 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 = ["hydrate"]
|
||||
|
||||
# If the --no-default-features flag should be used when compiling the lib target
|
||||
#
|
||||
# Optional. Defaults to false.
|
||||
lib-default-features = false
|
||||
|
||||
# The profile to use for the lib target when compiling for release
|
||||
#
|
||||
# Optional. Defaults to "release".
|
||||
lib-profile-release = "wasm-release"
|
||||
@@ -5,4 +5,4 @@ extend = [
|
||||
|
||||
[env]
|
||||
|
||||
CLIENT_PROCESS_NAME = "todo_app_sqlite_viz"
|
||||
CLIENT_PROCESS_NAME = "action_form_error_handling"
|
||||
68
examples/action-form-error-handling/README.md
Normal file
68
examples/action-form-error-handling/README.md
Normal file
@@ -0,0 +1,68 @@
|
||||
<picture>
|
||||
<source srcset="https://raw.githubusercontent.com/leptos-rs/leptos/main/docs/logos/Leptos_logo_Solid_White.svg" media="(prefers-color-scheme: dark)">
|
||||
<img src="https://raw.githubusercontent.com/leptos-rs/leptos/main/docs/logos/Leptos_logo_RGB.svg" alt="Leptos Logo">
|
||||
</picture>
|
||||
|
||||
# Leptos Starter Template
|
||||
|
||||
This is a template for use with the [Leptos](https://github.com/leptos-rs/leptos) web framework and the [cargo-leptos](https://github.com/akesson/cargo-leptos) tool.
|
||||
|
||||
## Creating your template repo
|
||||
|
||||
If you don't have `cargo-leptos` installed you can install it with
|
||||
|
||||
`cargo install cargo-leptos`
|
||||
|
||||
Then run
|
||||
|
||||
`cargo leptos new --git leptos-rs/start`
|
||||
|
||||
to generate a new project template (you will be prompted to enter a project name).
|
||||
|
||||
`cd {projectname}`
|
||||
|
||||
to go to your newly created project.
|
||||
|
||||
Of course, you should explore around the project structure, but the best place to start with your application code is in `src/app.rs`.
|
||||
|
||||
## Running your project
|
||||
|
||||
`cargo leptos watch`
|
||||
By default, you can access your local project at `http://localhost:3000`
|
||||
|
||||
## Installing Additional Tools
|
||||
|
||||
By default, `cargo-leptos` uses `nightly` Rust, `cargo-generate`, and `sass`. If you run into any trouble, you may need to install one or more of these tools.
|
||||
|
||||
1. `rustup toolchain install nightly --allow-downgrade` - make sure you have Rust nightly
|
||||
2. `rustup target add wasm32-unknown-unknown` - add the ability to compile Rust to WebAssembly
|
||||
3. `cargo install cargo-generate` - install `cargo-generate` binary (should be installed automatically in future)
|
||||
4. `npm install -g sass` - install `dart-sass` (should be optional in future)
|
||||
|
||||
## Executing a Server on a Remote Machine Without the Toolchain
|
||||
After running a `cargo leptos build --release` the minimum files needed are:
|
||||
|
||||
1. The server binary located in `target/server/release`
|
||||
2. The `site` directory and all files within located in `target/site`
|
||||
|
||||
Copy these files to your remote server. The directory structure should be:
|
||||
```text
|
||||
leptos_start
|
||||
site/
|
||||
```
|
||||
Set the following environment variables (updating for your project as needed):
|
||||
```sh
|
||||
export LEPTOS_OUTPUT_NAME="leptos_start"
|
||||
export LEPTOS_SITE_ROOT="site"
|
||||
export LEPTOS_SITE_PKG_DIR="pkg"
|
||||
export LEPTOS_SITE_ADDR="127.0.0.1:3000"
|
||||
export LEPTOS_RELOAD_PORT="3001"
|
||||
```
|
||||
Finally, run the server binary.
|
||||
|
||||
## Notes about CSR and Trunk:
|
||||
Although it is not recommended, you can also run your project without server integration using the feature `csr` and `trunk serve`:
|
||||
|
||||
`trunk serve --open --features csr`
|
||||
|
||||
This may be useful for integrating external tools which require a static site, e.g. `tauri`.
|
||||
90
examples/action-form-error-handling/src/app.rs
Normal file
90
examples/action-form-error-handling/src/app.rs
Normal file
@@ -0,0 +1,90 @@
|
||||
use leptos::*;
|
||||
use leptos_meta::*;
|
||||
use leptos_router::*;
|
||||
|
||||
#[component]
|
||||
pub fn App() -> impl IntoView {
|
||||
// Provides context that manages stylesheets, titles, meta tags, etc.
|
||||
provide_meta_context();
|
||||
|
||||
view! {
|
||||
// injects a stylesheet into the document <head>
|
||||
// id=leptos means cargo-leptos will hot-reload this stylesheet
|
||||
<Stylesheet id="leptos" href="/pkg/leptos_start.css"/>
|
||||
|
||||
// sets the document title
|
||||
<Title text="Welcome to Leptos"/>
|
||||
|
||||
// content for this welcome page
|
||||
<Router>
|
||||
<main id="app">
|
||||
<Routes>
|
||||
<Route path="" view=HomePage/>
|
||||
<Route path="/*any" view=NotFound/>
|
||||
</Routes>
|
||||
</main>
|
||||
</Router>
|
||||
}
|
||||
}
|
||||
|
||||
#[server]
|
||||
async fn do_something(should_error: Option<String>) -> Result<String, ServerFnError> {
|
||||
if should_error.is_none() {
|
||||
Ok(String::from("Successful submit"))
|
||||
} else {
|
||||
Err(ServerFnError::ServerError(String::from(
|
||||
"You got an error!",
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
/// Renders the home page of your application.
|
||||
#[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())));
|
||||
|
||||
Effect::new_isomorphic(move |_| {
|
||||
logging::log!("Got value = {:?}", value.get());
|
||||
});
|
||||
|
||||
view! {
|
||||
<h1>"Test the action form!"</h1>
|
||||
<ErrorBoundary fallback=move |error| format!("{:#?}", error
|
||||
.get()
|
||||
.into_iter()
|
||||
.next()
|
||||
.unwrap()
|
||||
.1.into_inner()
|
||||
.to_string())
|
||||
>
|
||||
{value}
|
||||
<ActionForm action=do_something_action class="form">
|
||||
<label>Should error: <input type="checkbox" name="should_error"/></label>
|
||||
<button type="submit">Submit</button>
|
||||
</ActionForm>
|
||||
</ErrorBoundary>
|
||||
}
|
||||
}
|
||||
|
||||
/// 404 - Not Found
|
||||
#[component]
|
||||
fn NotFound() -> impl IntoView {
|
||||
// set an HTTP status code 404
|
||||
// this is feature gated because it can only be done during
|
||||
// initial server-side rendering
|
||||
// if you navigate to the 404 page subsequently, the status
|
||||
// code will not be set because there is not a new HTTP request
|
||||
// to the server
|
||||
#[cfg(feature = "ssr")]
|
||||
{
|
||||
// this can be done inline because it's synchronous
|
||||
// if it were async, we'd use a server function
|
||||
let resp = expect_context::<leptos_actix::ResponseOptions>();
|
||||
resp.set_status(actix_web::http::StatusCode::NOT_FOUND);
|
||||
}
|
||||
|
||||
view! {
|
||||
<h1>"Not Found"</h1>
|
||||
}
|
||||
}
|
||||
18
examples/action-form-error-handling/src/lib.rs
Normal file
18
examples/action-form-error-handling/src/lib.rs
Normal file
@@ -0,0 +1,18 @@
|
||||
pub mod app;
|
||||
use cfg_if::cfg_if;
|
||||
|
||||
cfg_if! {
|
||||
if #[cfg(feature = "hydrate")] {
|
||||
|
||||
use wasm_bindgen::prelude::wasm_bindgen;
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn hydrate() {
|
||||
use app::*;
|
||||
|
||||
console_error_panic_hook::set_once();
|
||||
|
||||
leptos::mount_to_body(App);
|
||||
}
|
||||
}
|
||||
}
|
||||
53
examples/action-form-error-handling/src/main.rs
Normal file
53
examples/action-form-error-handling/src/main.rs
Normal file
@@ -0,0 +1,53 @@
|
||||
#[cfg(feature = "ssr")]
|
||||
#[actix_web::main]
|
||||
async fn main() -> std::io::Result<()> {
|
||||
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;
|
||||
// Generate the list of routes in your Leptos App
|
||||
let routes = generate_route_list(App);
|
||||
println!("listening on http://{}", &addr);
|
||||
|
||||
HttpServer::new(move || {
|
||||
let leptos_options = &conf.leptos_options;
|
||||
let site_root = &leptos_options.site_root;
|
||||
|
||||
App::new()
|
||||
.route("/api/{tail:.*}", leptos_actix::handle_server_fns())
|
||||
// serve JS/WASM/CSS from `pkg`
|
||||
.service(Files::new("/pkg", format!("{site_root}/pkg")))
|
||||
.leptos_routes(leptos_options.to_owned(), routes.to_owned(), App)
|
||||
.app_data(web::Data::new(leptos_options.to_owned()))
|
||||
//.wrap(middleware::Compress::default())
|
||||
})
|
||||
.bind(&addr)?
|
||||
.run()
|
||||
.await
|
||||
}
|
||||
|
||||
#[cfg(not(any(feature = "ssr", feature = "csr")))]
|
||||
pub fn main() {
|
||||
// no client-side main function
|
||||
// unless we want this to work with e.g., Trunk for pure client-side testing
|
||||
// see lib.rs for hydration function instead
|
||||
// see optional feature `csr` instead
|
||||
}
|
||||
|
||||
#[cfg(all(not(feature = "ssr"), feature = "csr"))]
|
||||
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 leptos::*;
|
||||
use action_form_error_handling::app::*;
|
||||
use wasm_bindgen::prelude::wasm_bindgen;
|
||||
|
||||
console_error_panic_hook::set_once();
|
||||
|
||||
leptos::mount_to_body(App);
|
||||
}
|
||||
15
examples/action-form-error-handling/style/main.scss
Normal file
15
examples/action-form-error-handling/style/main.scss
Normal file
@@ -0,0 +1,15 @@
|
||||
body {
|
||||
font-family: sans-serif;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#app {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
@@ -17,7 +17,6 @@ broadcaster = "1"
|
||||
console_log = "1"
|
||||
console_error_panic_hook = "0.1"
|
||||
futures = "0.3"
|
||||
cfg-if = "1"
|
||||
lazy_static = "1"
|
||||
leptos = { path = "../../leptos" }
|
||||
leptos_actix = { path = "../../integrations/actix", optional = true }
|
||||
|
||||
@@ -1,34 +1,35 @@
|
||||
use cfg_if::cfg_if;
|
||||
use leptos::*;
|
||||
use leptos_meta::*;
|
||||
use leptos_router::*;
|
||||
#[cfg(feature = "ssr")]
|
||||
use tracing::instrument;
|
||||
|
||||
cfg_if! {
|
||||
if #[cfg(feature = "ssr")] {
|
||||
use std::sync::atomic::{AtomicI32, Ordering};
|
||||
use broadcaster::BroadcastChannel;
|
||||
use once_cell::sync::OnceCell;
|
||||
#[cfg(feature = "ssr")]
|
||||
mod ssr_imports {
|
||||
pub use broadcaster::BroadcastChannel;
|
||||
pub use once_cell::sync::OnceCell;
|
||||
pub use std::sync::atomic::{AtomicI32, Ordering};
|
||||
|
||||
static COUNT: AtomicI32 = AtomicI32::new(0);
|
||||
pub static COUNT: AtomicI32 = AtomicI32::new(0);
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
pub static ref COUNT_CHANNEL: BroadcastChannel<i32> = BroadcastChannel::new();
|
||||
}
|
||||
lazy_static::lazy_static! {
|
||||
pub static ref COUNT_CHANNEL: BroadcastChannel<i32> = BroadcastChannel::new();
|
||||
}
|
||||
|
||||
static LOG_INIT: OnceCell<()> = OnceCell::new();
|
||||
fn init_logging() {
|
||||
LOG_INIT.get_or_init(|| {
|
||||
simple_logger::SimpleLogger::new().env().init().unwrap();
|
||||
});
|
||||
}
|
||||
static LOG_INIT: OnceCell<()> = OnceCell::new();
|
||||
|
||||
pub fn init_logging() {
|
||||
LOG_INIT.get_or_init(|| {
|
||||
simple_logger::SimpleLogger::new().env().init().unwrap();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#[server]
|
||||
#[cfg_attr(feature = "ssr", instrument)]
|
||||
pub async fn get_server_count() -> Result<i32, ServerFnError> {
|
||||
use ssr_imports::*;
|
||||
|
||||
Ok(COUNT.load(Ordering::Relaxed))
|
||||
}
|
||||
|
||||
@@ -38,6 +39,8 @@ pub async fn adjust_server_count(
|
||||
delta: i32,
|
||||
msg: String,
|
||||
) -> Result<i32, ServerFnError> {
|
||||
use ssr_imports::*;
|
||||
|
||||
let new = COUNT.load(Ordering::Relaxed) + delta;
|
||||
COUNT.store(new, Ordering::Relaxed);
|
||||
_ = COUNT_CHANNEL.send(&new).await;
|
||||
@@ -48,6 +51,8 @@ pub async fn adjust_server_count(
|
||||
#[server]
|
||||
#[cfg_attr(feature = "ssr", instrument)]
|
||||
pub async fn clear_server_count() -> Result<i32, ServerFnError> {
|
||||
use ssr_imports::*;
|
||||
|
||||
COUNT.store(0, Ordering::Relaxed);
|
||||
_ = COUNT_CHANNEL.send(&0).await;
|
||||
Ok(0)
|
||||
@@ -55,7 +60,7 @@ pub async fn clear_server_count() -> Result<i32, ServerFnError> {
|
||||
#[component]
|
||||
pub fn Counters() -> impl IntoView {
|
||||
#[cfg(feature = "ssr")]
|
||||
init_logging();
|
||||
ssr_imports::init_logging();
|
||||
|
||||
provide_meta_context();
|
||||
view! {
|
||||
|
||||
@@ -1,21 +1,13 @@
|
||||
use cfg_if::cfg_if;
|
||||
pub mod counters;
|
||||
|
||||
// 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 leptos::*;
|
||||
use wasm_bindgen::prelude::wasm_bindgen;
|
||||
use crate::counters::*;
|
||||
#[cfg(feature = "hydrate")]
|
||||
#[wasm_bindgen::prelude::wasm_bindgen]
|
||||
pub fn hydrate() {
|
||||
use crate::counters::*;
|
||||
use leptos::*;
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn hydrate() {
|
||||
_ = console_log::init_with_level(log::Level::Debug);
|
||||
console_error_panic_hook::set_once();
|
||||
_ = console_log::init_with_level(log::Level::Debug);
|
||||
console_error_panic_hook::set_once();
|
||||
|
||||
mount_to_body(|| {
|
||||
view! { <Counters/> }
|
||||
});
|
||||
}
|
||||
}
|
||||
mount_to_body(Counters);
|
||||
}
|
||||
|
||||
@@ -1,10 +1,5 @@
|
||||
use cfg_if::cfg_if;
|
||||
mod counters;
|
||||
|
||||
// boilerplate to run in different modes
|
||||
cfg_if! {
|
||||
// server-only stuff
|
||||
if #[cfg(feature = "ssr")] {
|
||||
use leptos::*;
|
||||
use actix_files::{Files};
|
||||
use actix_web::*;
|
||||
@@ -43,7 +38,7 @@ cfg_if! {
|
||||
let conf = get_configuration(None).await.unwrap();
|
||||
|
||||
let addr = conf.leptos_options.site_addr;
|
||||
let routes = generate_route_list(|| view! { <Counters/> });
|
||||
let routes = generate_route_list(Counters);
|
||||
|
||||
HttpServer::new(move || {
|
||||
let leptos_options = &conf.leptos_options;
|
||||
@@ -51,8 +46,7 @@ cfg_if! {
|
||||
|
||||
App::new()
|
||||
.service(counter_events)
|
||||
.route("/api/{tail:.*}", leptos_actix::handle_server_fns())
|
||||
.leptos_routes(leptos_options.to_owned(), routes.to_owned(), || view! { <Counters/> })
|
||||
.leptos_routes(leptos_options.to_owned(), routes.to_owned(), Counters)
|
||||
.service(Files::new("/", site_root))
|
||||
//.wrap(middleware::Compress::default())
|
||||
})
|
||||
@@ -60,13 +54,4 @@ cfg_if! {
|
||||
.run()
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
// client-only main for Trunk
|
||||
else {
|
||||
pub fn main() {
|
||||
// isomorphic counters cannot work in a Client-Side-Rendered only
|
||||
// app as a server is required to maintain state
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,22 +7,21 @@ edition = "2021"
|
||||
crate-type = ["cdylib", "rlib"]
|
||||
|
||||
[dependencies]
|
||||
console_log = "1.0.0"
|
||||
console_error_panic_hook = "0.1.7"
|
||||
cfg-if = "1.0.0"
|
||||
console_log = "1.0"
|
||||
console_error_panic_hook = "0.1"
|
||||
leptos = { path = "../../leptos", features = ["nightly"] }
|
||||
leptos_axum = { path = "../../integrations/axum", optional = true }
|
||||
leptos_meta = { path = "../../meta" }
|
||||
leptos_router = { path = "../../router" }
|
||||
log = "0.4.17"
|
||||
log = "0.4"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
simple_logger = "4.0.0"
|
||||
axum = { version = "0.6.1", optional = true }
|
||||
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 = "0.2.8" }
|
||||
thiserror = "1.0.38"
|
||||
simple_logger = "4.0"
|
||||
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 = ["full"], optional = true }
|
||||
http = { version = "1.0" }
|
||||
thiserror = "1.0"
|
||||
wasm-bindgen = "0.2"
|
||||
|
||||
[features]
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
use crate::errors::AppError;
|
||||
use cfg_if::cfg_if;
|
||||
use leptos::{logging::log, Errors, *};
|
||||
#[cfg(feature = "ssr")]
|
||||
use leptos_axum::ResponseOptions;
|
||||
@@ -30,12 +29,13 @@ pub fn ErrorTemplate(
|
||||
|
||||
// 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")] {
|
||||
#[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>
|
||||
|
||||
@@ -1,43 +1,48 @@
|
||||
use cfg_if::cfg_if;
|
||||
use crate::landing::App;
|
||||
use axum::{
|
||||
body::Body,
|
||||
extract::State,
|
||||
http::{Request, Response, StatusCode, Uri},
|
||||
response::{IntoResponse, Response as AxumResponse},
|
||||
};
|
||||
use leptos::{view, LeptosOptions};
|
||||
use tower::ServiceExt;
|
||||
use tower_http::services::ServeDir;
|
||||
|
||||
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::{LeptosOptions, view};
|
||||
use crate::landing::App;
|
||||
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();
|
||||
|
||||
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()
|
||||
}
|
||||
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}"),
|
||||
)),
|
||||
}
|
||||
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}"),
|
||||
)),
|
||||
}
|
||||
}}
|
||||
}
|
||||
|
||||
@@ -1,24 +1,21 @@
|
||||
use cfg_if::cfg_if;
|
||||
pub mod error_template;
|
||||
pub mod errors;
|
||||
#[cfg(feature = "ssr")]
|
||||
pub mod fallback;
|
||||
pub mod landing;
|
||||
|
||||
// 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 leptos::*;
|
||||
use wasm_bindgen::prelude::wasm_bindgen;
|
||||
use crate::landing::*;
|
||||
use wasm_bindgen::prelude::wasm_bindgen;
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn hydrate() {
|
||||
_ = console_log::init_with_level(log::Level::Debug);
|
||||
console_error_panic_hook::set_once();
|
||||
#[cfg(feature = "hydrate")]
|
||||
#[wasm_bindgen]
|
||||
pub fn hydrate() {
|
||||
use crate::landing::*;
|
||||
use leptos::*;
|
||||
|
||||
leptos::mount_to_body(|| {
|
||||
view! { <App/> }
|
||||
});
|
||||
}
|
||||
}
|
||||
_ = console_log::init_with_level(log::Level::Debug);
|
||||
console_error_panic_hook::set_once();
|
||||
|
||||
leptos::mount_to_body(|| {
|
||||
view! { <App/> }
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,41 +1,39 @@
|
||||
use cfg_if::cfg_if;
|
||||
|
||||
cfg_if! { if #[cfg(feature = "ssr")] {
|
||||
use crate::fallback::file_and_error_handler;
|
||||
use crate::landing::*;
|
||||
use axum::body::Body as AxumBody;
|
||||
use axum::{
|
||||
extract::{State, Path},
|
||||
#[cfg(feature = "ssr")]
|
||||
mod ssr_imports {
|
||||
pub use axum::{
|
||||
body::Body as AxumBody,
|
||||
extract::{Path, State},
|
||||
http::Request,
|
||||
response::{IntoResponse, Response},
|
||||
routing::{get, post},
|
||||
routing::get,
|
||||
Router,
|
||||
};
|
||||
use errors_axum::*;
|
||||
use leptos::{logging::log, *};
|
||||
use leptos_axum::{generate_route_list, LeptosRoutes};
|
||||
}}
|
||||
pub use errors_axum::{fallback::*, landing::App};
|
||||
pub use leptos::{logging::log, *};
|
||||
pub use leptos_axum::{generate_route_list, LeptosRoutes};
|
||||
|
||||
//Define a handler to test extractor with state
|
||||
#[cfg(feature = "ssr")]
|
||||
async fn custom_handler(
|
||||
Path(id): Path<String>,
|
||||
State(options): State<LeptosOptions>,
|
||||
req: Request<AxumBody>,
|
||||
) -> Response {
|
||||
let handler = leptos_axum::render_app_to_stream_with_context(
|
||||
options.clone(),
|
||||
move || {
|
||||
provide_context(id.clone());
|
||||
},
|
||||
App,
|
||||
);
|
||||
handler(req).await.into_response()
|
||||
// This custom handler lets us provide Axum State via context
|
||||
pub async fn custom_handler(
|
||||
Path(id): Path<String>,
|
||||
State(options): State<LeptosOptions>,
|
||||
req: Request<AxumBody>,
|
||||
) -> Response {
|
||||
let handler = leptos_axum::render_app_to_stream_with_context(
|
||||
options.clone(),
|
||||
move || {
|
||||
provide_context(id.clone());
|
||||
},
|
||||
App,
|
||||
);
|
||||
handler(req).await.into_response()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
use ssr_imports::*;
|
||||
|
||||
simple_logger::init_with_level(log::Level::Debug)
|
||||
.expect("couldn't initialize logging");
|
||||
|
||||
@@ -52,7 +50,6 @@ async fn main() {
|
||||
|
||||
// build our application with a route
|
||||
let app = Router::new()
|
||||
.route("/api/*fn_name", post(leptos_axum::handle_server_fns))
|
||||
.route("/special/:id", get(custom_handler))
|
||||
.leptos_routes(&leptos_options, routes, App)
|
||||
.fallback(file_and_error_handler)
|
||||
@@ -61,8 +58,8 @@ async fn main() {
|
||||
// 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())
|
||||
let listener = tokio::net::TcpListener::bind(&addr).await.unwrap();
|
||||
axum::serve(listener, app.into_make_service())
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
@@ -71,5 +68,5 @@ async fn main() {
|
||||
#[cfg(not(feature = "ssr"))]
|
||||
pub fn main() {
|
||||
// This example cannot be built as a trunk standalone CSR-only app.
|
||||
// The server is needed to demonstrate the error statuses.
|
||||
// The server is needed to demonstrate the error statuses.
|
||||
}
|
||||
|
||||
@@ -15,7 +15,6 @@ actix-files = { version = "0.6", optional = true }
|
||||
actix-web = { version = "4", optional = true, features = ["macros"] }
|
||||
console_log = "1"
|
||||
console_error_panic_hook = "0.1"
|
||||
cfg-if = "1"
|
||||
leptos = { path = "../../leptos", features = ["nightly"] }
|
||||
leptos_meta = { path = "../../meta", features = ["nightly"] }
|
||||
leptos_actix = { path = "../../integrations/actix", optional = true }
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
use cfg_if::cfg_if;
|
||||
use leptos::*;
|
||||
use leptos_meta::*;
|
||||
use leptos_router::*;
|
||||
@@ -33,16 +32,10 @@ pub fn App() -> impl IntoView {
|
||||
}
|
||||
}
|
||||
|
||||
// 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;
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn hydrate() {
|
||||
_ = console_log::init_with_level(log::Level::Debug);
|
||||
console_error_panic_hook::set_once();
|
||||
leptos::mount_to_body(App);
|
||||
}
|
||||
}
|
||||
#[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();
|
||||
leptos::mount_to_body(App);
|
||||
}
|
||||
|
||||
@@ -1,56 +1,56 @@
|
||||
use cfg_if::cfg_if;
|
||||
use leptos::*;
|
||||
// server-only stuff
|
||||
#[cfg(feature = "ssr")]
|
||||
mod ssr_imports {
|
||||
pub use actix_files::Files;
|
||||
pub use actix_web::*;
|
||||
pub use hackernews::App;
|
||||
pub use leptos_actix::{generate_route_list, LeptosRoutes};
|
||||
|
||||
// boilerplate to run in different modes
|
||||
cfg_if! {
|
||||
// server-only stuff
|
||||
if #[cfg(feature = "ssr")] {
|
||||
use actix_files::{Files};
|
||||
use actix_web::*;
|
||||
use hackernews::{App};
|
||||
use leptos_actix::{LeptosRoutes, generate_route_list};
|
||||
|
||||
#[get("/style.css")]
|
||||
async fn css() -> impl Responder {
|
||||
actix_files::NamedFile::open_async("./style.css").await
|
||||
}
|
||||
#[get("/favicon.ico")]
|
||||
async fn favicon() -> impl Responder {
|
||||
actix_files::NamedFile::open_async("./target/site//favicon.ico").await
|
||||
}
|
||||
|
||||
#[actix_web::main]
|
||||
async fn main() -> std::io::Result<()> {
|
||||
// Setting this to None means we'll be using cargo-leptos and its env vars.
|
||||
let conf = get_configuration(None).await.unwrap();
|
||||
|
||||
let addr = conf.leptos_options.site_addr;
|
||||
// Generate the list of routes in your Leptos App
|
||||
let routes = generate_route_list(App);
|
||||
|
||||
HttpServer::new(move || {
|
||||
let leptos_options = &conf.leptos_options;
|
||||
let site_root = &leptos_options.site_root;
|
||||
|
||||
App::new()
|
||||
.service(css)
|
||||
.service(favicon)
|
||||
.route("/api/{tail:.*}", leptos_actix::handle_server_fns())
|
||||
.leptos_routes(leptos_options.to_owned(), routes.to_owned(), App)
|
||||
.service(Files::new("/", site_root))
|
||||
//.wrap(middleware::Compress::default())
|
||||
})
|
||||
.bind(&addr)?
|
||||
.run()
|
||||
.await
|
||||
}
|
||||
} else {
|
||||
fn main() {
|
||||
use hackernews::{App};
|
||||
|
||||
_ = console_log::init_with_level(log::Level::Debug);
|
||||
console_error_panic_hook::set_once();
|
||||
mount_to_body(App)
|
||||
}
|
||||
#[get("/style.css")]
|
||||
pub async fn css() -> impl Responder {
|
||||
actix_files::NamedFile::open_async("./style.css").await
|
||||
}
|
||||
#[get("/favicon.ico")]
|
||||
pub async fn favicon() -> impl Responder {
|
||||
actix_files::NamedFile::open_async("./target/site//favicon.ico").await
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
#[actix_web::main]
|
||||
async fn main() -> std::io::Result<()> {
|
||||
use leptos::get_configuration;
|
||||
use ssr_imports::*;
|
||||
|
||||
// Setting this to None means we'll be using cargo-leptos and its env vars.
|
||||
let conf = get_configuration(None).await.unwrap();
|
||||
|
||||
let addr = conf.leptos_options.site_addr;
|
||||
// Generate the list of routes in your Leptos App
|
||||
let routes = generate_route_list(App);
|
||||
|
||||
HttpServer::new(move || {
|
||||
let leptos_options = &conf.leptos_options;
|
||||
let site_root = &leptos_options.site_root;
|
||||
|
||||
App::new()
|
||||
.service(css)
|
||||
.service(favicon)
|
||||
.leptos_routes(leptos_options.to_owned(), routes.to_owned(), App)
|
||||
.service(Files::new("/", site_root))
|
||||
//.wrap(middleware::Compress::default())
|
||||
})
|
||||
.bind(&addr)?
|
||||
.run()
|
||||
.await
|
||||
}
|
||||
|
||||
// CSR-only setup
|
||||
#[cfg(not(feature = "ssr"))]
|
||||
fn main() {
|
||||
use hackernews::App;
|
||||
|
||||
_ = console_log::init_with_level(log::Level::Debug);
|
||||
console_error_panic_hook::set_once();
|
||||
mount_to_body(App)
|
||||
}
|
||||
|
||||
@@ -11,24 +11,23 @@ codegen-units = 1
|
||||
lto = true
|
||||
|
||||
[dependencies]
|
||||
console_log = "1.0.0"
|
||||
console_error_panic_hook = "0.1.7"
|
||||
cfg-if = "1.0.0"
|
||||
console_log = "1.0"
|
||||
console_error_panic_hook = "0.1"
|
||||
leptos = { path = "../../leptos", features = ["nightly"] }
|
||||
leptos_axum = { path = "../../integrations/axum", optional = true }
|
||||
leptos_meta = { path = "../../meta", features = ["nightly"] }
|
||||
leptos_router = { path = "../../router", features = ["nightly"] }
|
||||
log = "0.4.17"
|
||||
simple_logger = "4.0.0"
|
||||
serde = { version = "1.0.148", features = ["derive"] }
|
||||
log = "0.4"
|
||||
simple_logger = "4.0"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
tracing = "0.1"
|
||||
gloo-net = { version = "0.2.5", features = ["http"] }
|
||||
reqwest = { version = "0.11.13", features = ["json"] }
|
||||
axum = { version = "0.6.1", optional = true }
|
||||
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 = "0.2.11", optional = true }
|
||||
gloo-net = { version = "0.4", features = ["http"] }
|
||||
reqwest = { version = "0.11", features = ["json"] }
|
||||
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 = ["full"], optional = true }
|
||||
http = { version = "1.0", optional = true }
|
||||
web-sys = { version = "0.3", features = ["AbortController", "AbortSignal"] }
|
||||
wasm-bindgen = "0.2"
|
||||
|
||||
|
||||
@@ -1,44 +1,48 @@
|
||||
use cfg_if::cfg_if;
|
||||
use crate::error_template::error_template;
|
||||
use axum::{
|
||||
body::Body,
|
||||
extract::State,
|
||||
http::{Request, Response, StatusCode, Uri},
|
||||
response::{IntoResponse, Response as AxumResponse},
|
||||
};
|
||||
use leptos::LeptosOptions;
|
||||
use tower::ServiceExt;
|
||||
use tower_http::services::ServeDir;
|
||||
|
||||
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::{LeptosOptions};
|
||||
use crate::error_template::error_template;
|
||||
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();
|
||||
|
||||
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(), || error_template( None));
|
||||
handler(req).await.into_response()
|
||||
}
|
||||
if res.status() == StatusCode::OK {
|
||||
res.into_response()
|
||||
} else {
|
||||
let handler =
|
||||
leptos_axum::render_app_to_stream(options.to_owned(), || {
|
||||
error_template(None)
|
||||
});
|
||||
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),
|
||||
)),
|
||||
}
|
||||
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),
|
||||
)),
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,63 +1,68 @@
|
||||
use cfg_if::cfg_if;
|
||||
use axum::{
|
||||
body::Body,
|
||||
http::{Request, Response, StatusCode, Uri},
|
||||
response::IntoResponse,
|
||||
};
|
||||
use tower::ServiceExt;
|
||||
use tower_http::services::ServeDir;
|
||||
|
||||
cfg_if! {
|
||||
if #[cfg(feature = "ssr")] {
|
||||
use axum::{
|
||||
body::{boxed, Body, BoxBody},
|
||||
http::{Request, Response, StatusCode, Uri},
|
||||
};
|
||||
use tower::ServiceExt;
|
||||
use tower_http::services::ServeDir;
|
||||
pub async fn file_handler(
|
||||
uri: Uri,
|
||||
) -> Result<Response<Body>, (StatusCode, String)> {
|
||||
let res = get_static_file(uri.clone(), "/pkg").await?;
|
||||
|
||||
pub async fn file_handler(uri: Uri) -> Result<Response<BoxBody>, (StatusCode, String)> {
|
||||
let res = get_static_file(uri.clone(), "/pkg").await?;
|
||||
|
||||
if res.status() == StatusCode::NOT_FOUND {
|
||||
// try with `.html`
|
||||
// TODO: handle if the Uri has query parameters
|
||||
match format!("{}.html", uri).parse() {
|
||||
Ok(uri_html) => get_static_file(uri_html, "/pkg").await,
|
||||
Err(_) => Err((StatusCode::INTERNAL_SERVER_ERROR, "Invalid URI".to_string())),
|
||||
}
|
||||
} else {
|
||||
Ok(res)
|
||||
if res.status() == StatusCode::NOT_FOUND {
|
||||
// try with `.html`
|
||||
// TODO: handle if the Uri has query parameters
|
||||
match format!("{}.html", uri).parse() {
|
||||
Ok(uri_html) => get_static_file(uri_html, "/pkg").await,
|
||||
Err(_) => Err((
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
"Invalid URI".to_string(),
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_static_file_handler(uri: Uri) -> Result<Response<BoxBody>, (StatusCode, String)> {
|
||||
let res = get_static_file(uri.clone(), "/static").await?;
|
||||
|
||||
if res.status() == StatusCode::NOT_FOUND {
|
||||
Err((StatusCode::INTERNAL_SERVER_ERROR, "Invalid URI".to_string()))
|
||||
} else {
|
||||
Ok(res)
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_static_file(uri: Uri, base: &str) -> Result<Response<BoxBody>, (StatusCode, String)> {
|
||||
let req = Request::builder().uri(&uri).body(Body::empty()).unwrap();
|
||||
|
||||
// `ServeDir` implements `tower::Service` so we can call it with `tower::ServiceExt::oneshot`
|
||||
// When run normally, the root should be the crate root
|
||||
if base == "/static" {
|
||||
match ServeDir::new("./static").oneshot(req).await {
|
||||
Ok(res) => Ok(res.map(boxed)),
|
||||
Err(err) => Err((
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("Something went wrong: {}", err),
|
||||
))
|
||||
}
|
||||
} else if base == "/pkg" {
|
||||
match ServeDir::new("./pkg").oneshot(req).await {
|
||||
Ok(res) => Ok(res.map(boxed)),
|
||||
Err(err) => Err((
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("Something went wrong: {}", err),
|
||||
)),
|
||||
}
|
||||
} else{
|
||||
Err((StatusCode::NOT_FOUND, "Not Found".to_string()))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Ok(res)
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_static_file_handler(
|
||||
uri: Uri,
|
||||
) -> Result<Response<Body>, (StatusCode, String)> {
|
||||
let res = get_static_file(uri.clone(), "/static").await?;
|
||||
|
||||
if res.status() == StatusCode::NOT_FOUND {
|
||||
Err((StatusCode::INTERNAL_SERVER_ERROR, "Invalid URI".to_string()))
|
||||
} else {
|
||||
Ok(res)
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_static_file(
|
||||
uri: Uri,
|
||||
base: &str,
|
||||
) -> Result<Response<Body>, (StatusCode, String)> {
|
||||
let req = Request::builder().uri(&uri).body(Body::empty()).unwrap();
|
||||
|
||||
// `ServeDir` implements `tower::Service` so we can call it with `tower::ServiceExt::oneshot`
|
||||
// When run normally, the root should be the crate root
|
||||
if base == "/static" {
|
||||
match ServeDir::new("./static").oneshot(req).await {
|
||||
Ok(res) => Ok(res.into_response()),
|
||||
Err(err) => Err((
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("Something went wrong: {}", err),
|
||||
)),
|
||||
}
|
||||
} else if base == "/pkg" {
|
||||
match ServeDir::new("./pkg").oneshot(req).await {
|
||||
Ok(res) => Ok(res.into_response()),
|
||||
Err(err) => Err((
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("Something went wrong: {}", err),
|
||||
)),
|
||||
}
|
||||
} else {
|
||||
Err((StatusCode::NOT_FOUND, "Not Found".to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
use cfg_if::cfg_if;
|
||||
use leptos::{component, view, IntoView};
|
||||
use leptos_meta::*;
|
||||
use leptos_router::*;
|
||||
mod api;
|
||||
pub mod error_template;
|
||||
#[cfg(feature = "ssr")]
|
||||
pub mod fallback;
|
||||
#[cfg(feature = "ssr")]
|
||||
pub mod handlers;
|
||||
mod routes;
|
||||
use routes::{nav::*, stories::*, story::*, users::*};
|
||||
@@ -12,38 +13,28 @@ use routes::{nav::*, stories::*, story::*, users::*};
|
||||
#[component]
|
||||
pub fn App() -> impl IntoView {
|
||||
provide_meta_context();
|
||||
|
||||
view! {
|
||||
|
||||
<>
|
||||
<Link rel="shortcut icon" type_="image/ico" href="/favicon.ico"/>
|
||||
<Stylesheet id="leptos" href="/pkg/hackernews_axum.css"/>
|
||||
<Meta name="description" content="Leptos implementation of a HackerNews demo."/>
|
||||
<Router>
|
||||
<Nav />
|
||||
<main>
|
||||
<Routes>
|
||||
<Route path="users/:id" view=User/>
|
||||
<Route path="stories/:id" view=Story/>
|
||||
<Route path=":stories?" view=Stories/>
|
||||
</Routes>
|
||||
</main>
|
||||
</Router>
|
||||
</>
|
||||
<Link rel="shortcut icon" type_="image/ico" href="/favicon.ico"/>
|
||||
<Stylesheet id="leptos" href="/pkg/hackernews_axum.css"/>
|
||||
<Meta name="description" content="Leptos implementation of a HackerNews demo."/>
|
||||
<Router>
|
||||
<Nav />
|
||||
<main>
|
||||
<Routes>
|
||||
<Route path="users/:id" view=User/>
|
||||
<Route path="stories/:id" view=Story/>
|
||||
<Route path=":stories?" view=Stories/>
|
||||
</Routes>
|
||||
</main>
|
||||
</Router>
|
||||
}
|
||||
}
|
||||
|
||||
// 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;
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn hydrate() {
|
||||
_ = console_log::init_with_level(log::Level::Debug);
|
||||
console_error_panic_hook::set_once();
|
||||
leptos::mount_to_body(move || {
|
||||
view! { <App/> }
|
||||
});
|
||||
}
|
||||
}
|
||||
#[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();
|
||||
leptos::mount_to_body(App);
|
||||
}
|
||||
|
||||
@@ -1,54 +1,41 @@
|
||||
use cfg_if::cfg_if;
|
||||
use leptos::{logging::log, *};
|
||||
|
||||
// boilerplate to run in different modes
|
||||
cfg_if! {
|
||||
if #[cfg(feature = "ssr")] {
|
||||
use axum::{
|
||||
Router,
|
||||
routing::get,
|
||||
};
|
||||
#[cfg(feature = "ssr")]
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
use axum::{routing::get, Router};
|
||||
use hackernews_axum::{fallback::file_and_error_handler, *};
|
||||
use leptos::get_configuration;
|
||||
use leptos_axum::{generate_route_list, LeptosRoutes};
|
||||
use hackernews_axum::fallback::file_and_error_handler;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
use hackernews_axum::*;
|
||||
let conf = get_configuration(Some("Cargo.toml")).await.unwrap();
|
||||
let leptos_options = conf.leptos_options;
|
||||
let addr = leptos_options.site_addr;
|
||||
let routes = generate_route_list(App);
|
||||
|
||||
let conf = get_configuration(Some("Cargo.toml")).await.unwrap();
|
||||
let leptos_options = conf.leptos_options;
|
||||
let addr = leptos_options.site_addr;
|
||||
let routes = generate_route_list(App);
|
||||
simple_logger::init_with_level(log::Level::Debug)
|
||||
.expect("couldn't initialize logging");
|
||||
|
||||
simple_logger::init_with_level(log::Level::Debug).expect("couldn't initialize logging");
|
||||
|
||||
// build our application with a route
|
||||
let app = Router::new()
|
||||
// build our application with a route
|
||||
let app = Router::new()
|
||||
.route("/favicon.ico", get(file_and_error_handler))
|
||||
.leptos_routes(&leptos_options, routes, || view! { <App/> } )
|
||||
.leptos_routes(&leptos_options, routes, App)
|
||||
.fallback(file_and_error_handler)
|
||||
.with_state(leptos_options);
|
||||
|
||||
// run our app with hyper
|
||||
// `axum::Server` is a re-export of `hyper::Server`
|
||||
log!("listening on {}", addr);
|
||||
axum::Server::bind(&addr)
|
||||
.serve(app.into_make_service())
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
// run our app with hyper
|
||||
// `axum::Server` is a re-export of `hyper::Server`
|
||||
println!("listening on {}", addr);
|
||||
let listener = tokio::net::TcpListener::bind(&addr).await.unwrap();
|
||||
axum::serve(listener, app.into_make_service())
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
// client-only stuff for Trunk
|
||||
else {
|
||||
use hackernews_axum::*;
|
||||
// client-only stuff for Trunk
|
||||
#[cfg(not(feature = "ssr"))]
|
||||
pub fn main() {
|
||||
use hackernews_axum::*;
|
||||
|
||||
pub fn main() {
|
||||
_ = console_log::init_with_level(log::Level::Debug);
|
||||
console_error_panic_hook::set_once();
|
||||
mount_to_body(|| {
|
||||
view! { <App/> }
|
||||
});
|
||||
}
|
||||
}
|
||||
_ = console_log::init_with_level(log::Level::Debug);
|
||||
console_error_panic_hook::set_once();
|
||||
mount_to_body(App);
|
||||
}
|
||||
|
||||
@@ -11,9 +11,8 @@ codegen-units = 1
|
||||
lto = true
|
||||
|
||||
[dependencies]
|
||||
console_log = "1.0.0"
|
||||
console_error_panic_hook = "0.1.7"
|
||||
cfg-if = "1.0.0"
|
||||
console_log = "1.0"
|
||||
console_error_panic_hook = "0.1"
|
||||
leptos = { path = "../../leptos", features = [
|
||||
"nightly",
|
||||
"experimental-islands",
|
||||
@@ -23,20 +22,20 @@ leptos_axum = { path = "../../integrations/axum", optional = true, features = [
|
||||
] }
|
||||
leptos_meta = { path = "../../meta", features = ["nightly"] }
|
||||
leptos_router = { path = "../../router", features = ["nightly"] }
|
||||
log = "0.4.17"
|
||||
simple_logger = "4.0.0"
|
||||
serde = { version = "1.0.148", features = ["derive"] }
|
||||
log = "0.4"
|
||||
simple_logger = "4.0"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
tracing = "0.1"
|
||||
gloo-net = { version = "0.2.5", features = ["http"] }
|
||||
reqwest = { version = "0.11.13", features = ["json"] }
|
||||
axum = { version = "0.6.1", optional = true, features = ["http2"] }
|
||||
tower = { version = "0.4.13", optional = true }
|
||||
tower-http = { version = "0.4", features = [
|
||||
gloo-net = { version = "0.4", features = ["http"] }
|
||||
reqwest = { version = "0.11", features = ["json"] }
|
||||
axum = { version = "0.7", optional = true, features = ["http2"] }
|
||||
tower = { version = "0.4", optional = true }
|
||||
tower-http = { version = "0.5", features = [
|
||||
"fs",
|
||||
"compression-br",
|
||||
], optional = true }
|
||||
tokio = { version = "1.22.0", features = ["full"], optional = true }
|
||||
http = { version = "0.2.11", optional = true }
|
||||
tokio = { version = "1", features = ["full"], optional = true }
|
||||
http = { version = "1.0", optional = true }
|
||||
web-sys = { version = "0.3", features = ["AbortController", "AbortSignal"] }
|
||||
wasm-bindgen = "0.2"
|
||||
lazy_static = "1.4.0"
|
||||
|
||||
@@ -1,44 +1,48 @@
|
||||
use cfg_if::cfg_if;
|
||||
use crate::error_template::error_template;
|
||||
use axum::{
|
||||
body::Body,
|
||||
extract::State,
|
||||
http::{Request, Response, StatusCode, Uri},
|
||||
response::{IntoResponse, Response as AxumResponse},
|
||||
};
|
||||
use leptos::LeptosOptions;
|
||||
use tower::ServiceExt;
|
||||
use tower_http::services::ServeDir;
|
||||
|
||||
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::{LeptosOptions};
|
||||
use crate::error_template::error_template;
|
||||
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();
|
||||
|
||||
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(), || error_template( None));
|
||||
handler(req).await.into_response()
|
||||
}
|
||||
if res.status() == StatusCode::OK {
|
||||
res.into_response()
|
||||
} else {
|
||||
let handler =
|
||||
leptos_axum::render_app_to_stream(options.to_owned(), || {
|
||||
error_template(None)
|
||||
});
|
||||
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),
|
||||
)),
|
||||
}
|
||||
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),
|
||||
)),
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,63 +0,0 @@
|
||||
use cfg_if::cfg_if;
|
||||
|
||||
cfg_if! {
|
||||
if #[cfg(feature = "ssr")] {
|
||||
use axum::{
|
||||
body::{boxed, Body, BoxBody},
|
||||
http::{Request, Response, StatusCode, Uri},
|
||||
};
|
||||
use tower::ServiceExt;
|
||||
use tower_http::services::ServeDir;
|
||||
|
||||
pub async fn file_handler(uri: Uri) -> Result<Response<BoxBody>, (StatusCode, String)> {
|
||||
let res = get_static_file(uri.clone(), "/pkg").await?;
|
||||
|
||||
if res.status() == StatusCode::NOT_FOUND {
|
||||
// try with `.html`
|
||||
// TODO: handle if the Uri has query parameters
|
||||
match format!("{}.html", uri).parse() {
|
||||
Ok(uri_html) => get_static_file(uri_html, "/pkg").await,
|
||||
Err(_) => Err((StatusCode::INTERNAL_SERVER_ERROR, "Invalid URI".to_string())),
|
||||
}
|
||||
} else {
|
||||
Ok(res)
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_static_file_handler(uri: Uri) -> Result<Response<BoxBody>, (StatusCode, String)> {
|
||||
let res = get_static_file(uri.clone(), "/static").await?;
|
||||
|
||||
if res.status() == StatusCode::NOT_FOUND {
|
||||
Err((StatusCode::INTERNAL_SERVER_ERROR, "Invalid URI".to_string()))
|
||||
} else {
|
||||
Ok(res)
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_static_file(uri: Uri, base: &str) -> Result<Response<BoxBody>, (StatusCode, String)> {
|
||||
let req = Request::builder().uri(&uri).body(Body::empty()).unwrap();
|
||||
|
||||
// `ServeDir` implements `tower::Service` so we can call it with `tower::ServiceExt::oneshot`
|
||||
// When run normally, the root should be the crate root
|
||||
if base == "/static" {
|
||||
match ServeDir::new("./static").oneshot(req).await {
|
||||
Ok(res) => Ok(res.map(boxed)),
|
||||
Err(err) => Err((
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("Something went wrong: {}", err),
|
||||
))
|
||||
}
|
||||
} else if base == "/pkg" {
|
||||
match ServeDir::new("./pkg").oneshot(req).await {
|
||||
Ok(res) => Ok(res.map(boxed)),
|
||||
Err(err) => Err((
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("Something went wrong: {}", err),
|
||||
)),
|
||||
}
|
||||
} else{
|
||||
Err((StatusCode::NOT_FOUND, "Not Found".to_string()))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,11 @@
|
||||
#![feature(lazy_cell)]
|
||||
|
||||
use cfg_if::cfg_if;
|
||||
use leptos::*;
|
||||
use leptos_meta::*;
|
||||
use leptos_router::*;
|
||||
mod api;
|
||||
pub mod error_template;
|
||||
#[cfg(feature = "ssr")]
|
||||
pub mod fallback;
|
||||
mod routes;
|
||||
use routes::{nav::*, stories::*, story::*, users::*};
|
||||
@@ -31,16 +31,10 @@ pub fn App() -> impl IntoView {
|
||||
}
|
||||
}
|
||||
|
||||
// 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;
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn hydrate() {
|
||||
#[cfg(debug_assertions)]
|
||||
console_error_panic_hook::set_once();
|
||||
leptos::leptos_dom::HydrationCtx::stop_hydrating();
|
||||
}
|
||||
}
|
||||
#[cfg(feature = "hydrate")]
|
||||
#[wasm_bindgen::prelude::wasm_bindgen]
|
||||
pub fn hydrate() {
|
||||
#[cfg(debug_assertions)]
|
||||
console_error_panic_hook::set_once();
|
||||
leptos::leptos_dom::HydrationCtx::stop_hydrating();
|
||||
}
|
||||
|
||||
@@ -1,16 +1,11 @@
|
||||
#[cfg(feature = "ssr")]
|
||||
mod ssr_imports {
|
||||
pub use axum::{routing::get, Router};
|
||||
pub use hackernews_islands::fallback::file_and_error_handler;
|
||||
pub use leptos::*;
|
||||
pub use leptos_axum::{generate_route_list, LeptosRoutes};
|
||||
}
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
pub use axum::{routing::get, Router};
|
||||
pub use hackernews_islands::fallback::file_and_error_handler;
|
||||
use hackernews_islands::*;
|
||||
use ssr_imports::*;
|
||||
pub use leptos::get_configuration;
|
||||
pub use leptos_axum::{generate_route_list, LeptosRoutes};
|
||||
|
||||
let conf = get_configuration(Some("Cargo.toml")).await.unwrap();
|
||||
let leptos_options = conf.leptos_options;
|
||||
@@ -27,8 +22,8 @@ async fn main() {
|
||||
// run our app with hyper
|
||||
// `axum::Server` is a re-export of `hyper::Server`
|
||||
logging::log!("listening on {}", addr);
|
||||
axum::Server::bind(&addr)
|
||||
.serve(app.into_make_service())
|
||||
let listener = tokio::net::TcpListener::bind(&addr).await.unwrap();
|
||||
axum::serve(listener, app.into_make_service())
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
@@ -40,7 +35,5 @@ pub fn main() {
|
||||
use leptos::*;
|
||||
_ = console_log::init_with_level(log::Level::Debug);
|
||||
console_error_panic_hook::set_once();
|
||||
mount_to_body(|| {
|
||||
view! { <App/> }
|
||||
});
|
||||
mount_to_body(App);
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
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.17"
|
||||
simple_logger = "4.0.0"
|
||||
serde = { version = "1.0.148", features = ["derive"] }
|
||||
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]
|
||||
|
||||
@@ -1,39 +1,43 @@
|
||||
use cfg_if::cfg_if;
|
||||
use crate::error_template::error_template;
|
||||
use axum::{
|
||||
body::Body,
|
||||
extract::State,
|
||||
http::{Request, Response, StatusCode, Uri},
|
||||
response::{IntoResponse, Response as AxumResponse},
|
||||
};
|
||||
//use tower::ServiceExt;
|
||||
use leptos::LeptosOptions;
|
||||
|
||||
cfg_if! {
|
||||
if #[cfg(feature = "ssr")] {
|
||||
use axum::{
|
||||
body::{Body, BoxBody},
|
||||
extract::State,
|
||||
response::IntoResponse,
|
||||
http::{Request, Response, StatusCode, Uri},
|
||||
};
|
||||
use axum::response::Response as AxumResponse;
|
||||
//use tower::ServiceExt;
|
||||
use leptos::{LeptosOptions};
|
||||
use crate::error_template::error_template;
|
||||
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();
|
||||
|
||||
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(), || error_template(None));
|
||||
handler(req).await.into_response()
|
||||
}
|
||||
if res.status() == StatusCode::OK {
|
||||
res.into_response()
|
||||
} else {
|
||||
let handler =
|
||||
leptos_axum::render_app_to_stream(options.to_owned(), || {
|
||||
error_template(None)
|
||||
});
|
||||
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
|
||||
_ = req;
|
||||
_ = root;
|
||||
todo!()
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
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
|
||||
_ = req;
|
||||
_ = root;
|
||||
todo!()
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
use cfg_if::cfg_if;
|
||||
use leptos::{component, view, IntoView};
|
||||
use leptos_meta::*;
|
||||
use leptos_router::*;
|
||||
mod api;
|
||||
pub mod error_template;
|
||||
#[cfg(feature = "ssr")]
|
||||
pub mod fallback;
|
||||
mod routes;
|
||||
use routes::{nav::*, stories::*, story::*, users::*};
|
||||
@@ -29,25 +29,22 @@ pub fn App() -> impl IntoView {
|
||||
}
|
||||
}
|
||||
|
||||
cfg_if! {
|
||||
if #[cfg(feature = "hydrate")] {
|
||||
#[wasm_bindgen]
|
||||
pub fn hydrate() {
|
||||
_ = console_log::init_with_level(log::Level::Debug);
|
||||
console_error_panic_hook::set_once();
|
||||
leptos::mount_to_body(move || {
|
||||
view! { <App/> }
|
||||
});
|
||||
}
|
||||
} else if #[cfg(feature = "ssr")] {
|
||||
#[cfg(feature = "hydrate")]
|
||||
#[wasm_bindgen]
|
||||
pub fn hydrate() {
|
||||
_ = console_log::init_with_level(log::Level::Debug);
|
||||
console_error_panic_hook::set_once();
|
||||
leptos::mount_to_body(App);
|
||||
}
|
||||
|
||||
use axum::{
|
||||
Router,
|
||||
routing::post
|
||||
};
|
||||
use leptos_axum::{generate_route_list, LeptosRoutes};
|
||||
#[cfg(feature = "ssr")]
|
||||
mod ssr_imports {
|
||||
use crate::App;
|
||||
use axum::Router;
|
||||
use leptos::*;
|
||||
use leptos_axum::{generate_route_list, LeptosRoutes};
|
||||
use log::{info, Level};
|
||||
use wasm_bindgen::prelude::wasm_bindgen;
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub struct Handler(axum_js_fetch::App);
|
||||
@@ -58,14 +55,16 @@ cfg_if! {
|
||||
console_log::init_with_level(Level::Debug);
|
||||
console_error_panic_hook::set_once();
|
||||
|
||||
let leptos_options = LeptosOptions::builder().output_name("client").site_pkg_dir("pkg").build();
|
||||
let leptos_options = LeptosOptions::builder()
|
||||
.output_name("client")
|
||||
.site_pkg_dir("pkg")
|
||||
.build();
|
||||
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))
|
||||
.with_state(leptos_options);
|
||||
let app: axum::Router = Router::new()
|
||||
.leptos_routes(&leptos_options, routes, App)
|
||||
.with_state(leptos_options);
|
||||
|
||||
info!("creating handler instance");
|
||||
|
||||
@@ -77,4 +76,3 @@ cfg_if! {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ leptos_router = { path = "../../../router", features = ["csr"] }
|
||||
log = "0.4"
|
||||
console_error_panic_hook = "0.1"
|
||||
console_log = "1"
|
||||
gloo-net = "0.2"
|
||||
gloo-storage = "0.2"
|
||||
gloo-net = "0.5"
|
||||
gloo-storage = "0.3"
|
||||
serde = "1.0"
|
||||
thiserror = "1.0"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use api_boundary::*;
|
||||
use gloo_net::http::{Request, Response};
|
||||
use gloo_net::http::{Request, RequestBuilder, Response};
|
||||
use serde::de::DeserializeOwned;
|
||||
use thiserror::Error;
|
||||
|
||||
@@ -41,7 +41,7 @@ impl AuthorizedApi {
|
||||
fn auth_header_value(&self) -> String {
|
||||
format!("Bearer {}", self.token.token)
|
||||
}
|
||||
async fn send<T>(&self, req: Request) -> Result<T>
|
||||
async fn send<T>(&self, req: RequestBuilder) -> Result<T>
|
||||
where
|
||||
T: DeserializeOwned,
|
||||
{
|
||||
|
||||
@@ -5,14 +5,18 @@ edition = "2021"
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
api-boundary = "=0.0.0"
|
||||
|
||||
anyhow = "1.0"
|
||||
api-boundary = "*"
|
||||
axum = { version = "0.6", features = ["headers"] }
|
||||
axum = "0.7"
|
||||
axum-extra = { version = "0.9.2", features = ["typed-header"] }
|
||||
env_logger = "0.10"
|
||||
log = "0.4"
|
||||
mailparse = "0.14"
|
||||
pwhash = "1.0"
|
||||
thiserror = "1.0"
|
||||
tokio = { version = "1.25", features = ["macros", "rt-multi-thread"] }
|
||||
tower-http = { version = "0.4", features = ["cors"] }
|
||||
uuid = { version = "1.3", features = ["v4"] }
|
||||
tokio = { version = "1.35", features = ["macros", "rt-multi-thread"] }
|
||||
tower-http = { version = "0.5", features = ["cors"] }
|
||||
uuid = { version = "1.6", features = ["v4"] }
|
||||
parking_lot = "0.12.1"
|
||||
headers = "0.4.0"
|
||||
|
||||
@@ -1,38 +1,36 @@
|
||||
use mailparse::addrparse;
|
||||
use pwhash::bcrypt;
|
||||
use std::{collections::HashMap, str::FromStr, sync::RwLock};
|
||||
use std::{collections::HashMap, str::FromStr};
|
||||
use thiserror::Error;
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct AppState {
|
||||
users: RwLock<HashMap<EmailAddress, Password>>,
|
||||
tokens: RwLock<HashMap<Uuid, EmailAddress>>,
|
||||
users: HashMap<EmailAddress, Password>,
|
||||
tokens: HashMap<Uuid, EmailAddress>,
|
||||
}
|
||||
|
||||
impl AppState {
|
||||
pub fn create_user(
|
||||
&self,
|
||||
&mut self,
|
||||
credentials: Credentials,
|
||||
) -> Result<(), CreateUserError> {
|
||||
let Credentials { email, password } = credentials;
|
||||
let user_exists = self.users.read().unwrap().get(&email).is_some();
|
||||
let user_exists = self.users.get(&email).is_some();
|
||||
if user_exists {
|
||||
return Err(CreateUserError::UserExists);
|
||||
}
|
||||
self.users.write().unwrap().insert(email, password);
|
||||
self.users.insert(email, password);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn login(
|
||||
&self,
|
||||
&mut self,
|
||||
email: EmailAddress,
|
||||
password: &str,
|
||||
) -> Result<Uuid, LoginError> {
|
||||
let valid_credentials = self
|
||||
.users
|
||||
.read()
|
||||
.unwrap()
|
||||
.get(&email)
|
||||
.map(|hashed_password| hashed_password.verify(password))
|
||||
.unwrap_or(false);
|
||||
@@ -40,16 +38,16 @@ impl AppState {
|
||||
Err(LoginError::InvalidEmailOrPassword)
|
||||
} else {
|
||||
let token = Uuid::new_v4();
|
||||
self.tokens.write().unwrap().insert(token, email);
|
||||
self.tokens.insert(token, email);
|
||||
Ok(token)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn logout(&self, token: &str) -> Result<(), LogoutError> {
|
||||
pub fn logout(&mut self, token: &str) -> Result<(), LogoutError> {
|
||||
let token = token
|
||||
.parse::<Uuid>()
|
||||
.map_err(|_| LogoutError::NotLoggedIn)?;
|
||||
self.tokens.write().unwrap().remove(&token);
|
||||
self.tokens.remove(&token);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -62,8 +60,6 @@ impl AppState {
|
||||
.map_err(|_| AuthError::NotAuthorized)
|
||||
.and_then(|token| {
|
||||
self.tokens
|
||||
.read()
|
||||
.unwrap()
|
||||
.get(&token)
|
||||
.cloned()
|
||||
.map(|email| CurrentUser { email, token })
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
use api_boundary as json;
|
||||
use axum::{
|
||||
extract::{State, TypedHeader},
|
||||
headers::{authorization::Bearer, Authorization},
|
||||
extract::State,
|
||||
http::Method,
|
||||
response::Json,
|
||||
routing::{get, post},
|
||||
Router,
|
||||
};
|
||||
use std::{env, sync::Arc};
|
||||
use axum_extra::TypedHeader;
|
||||
use headers::{authorization::Bearer, Authorization};
|
||||
use parking_lot::RwLock;
|
||||
use std::{env, net::SocketAddr, sync::Arc};
|
||||
use tokio::net::TcpListener;
|
||||
use tower_http::cors::{Any, CorsLayer};
|
||||
|
||||
mod adapters;
|
||||
@@ -32,7 +35,7 @@ async fn main() -> anyhow::Result<()> {
|
||||
}
|
||||
env_logger::init();
|
||||
|
||||
let shared_state = Arc::new(AppState::default());
|
||||
let shared_state = Arc::new(RwLock::new(AppState::default()));
|
||||
|
||||
let cors_layer = CorsLayer::new()
|
||||
.allow_methods([Method::GET, Method::POST])
|
||||
@@ -46,11 +49,10 @@ async fn main() -> anyhow::Result<()> {
|
||||
.route_layer(cors_layer)
|
||||
.with_state(shared_state);
|
||||
|
||||
let addr = "0.0.0.0:3000".parse().unwrap();
|
||||
log::info!("Listen on {addr}");
|
||||
axum::Server::bind(&addr)
|
||||
.serve(app.into_make_service())
|
||||
.await?;
|
||||
let addr = "0.0.0.0:3000".parse::<SocketAddr>()?;
|
||||
log::info!("Start listening on http://{addr}");
|
||||
let listener = TcpListener::bind(addr).await?;
|
||||
axum::serve(listener, app.into_make_service()).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -73,40 +75,43 @@ enum Error {
|
||||
}
|
||||
|
||||
async fn create_user(
|
||||
State(state): State<Arc<AppState>>,
|
||||
State(state): State<Arc<RwLock<AppState>>>,
|
||||
Json(credentials): Json<json::Credentials>,
|
||||
) -> Result<()> {
|
||||
let credentials = Credentials::try_from(credentials)?;
|
||||
state.create_user(credentials)?;
|
||||
state.write().create_user(credentials)?;
|
||||
Ok(Json(()))
|
||||
}
|
||||
|
||||
async fn login(
|
||||
State(state): State<Arc<AppState>>,
|
||||
State(state): State<Arc<RwLock<AppState>>>,
|
||||
Json(credentials): Json<json::Credentials>,
|
||||
) -> Result<json::ApiToken> {
|
||||
let json::Credentials { email, password } = credentials;
|
||||
log::debug!("{email} tries to login");
|
||||
let email = email.parse().map_err(|_|
|
||||
// Here we don't want to leak detailed info.
|
||||
LoginError::InvalidEmailOrPassword)?;
|
||||
let token = state.login(email, &password).map(|s| s.to_string())?;
|
||||
// Here we don't want to leak detailed info.
|
||||
LoginError::InvalidEmailOrPassword)?;
|
||||
let token = state
|
||||
.write()
|
||||
.login(email, &password)
|
||||
.map(|s| s.to_string())?;
|
||||
Ok(Json(json::ApiToken { token }))
|
||||
}
|
||||
|
||||
async fn logout(
|
||||
State(state): State<Arc<AppState>>,
|
||||
State(state): State<Arc<RwLock<AppState>>>,
|
||||
TypedHeader(auth): TypedHeader<Authorization<Bearer>>,
|
||||
) -> Result<()> {
|
||||
state.logout(auth.token())?;
|
||||
state.write().logout(auth.token())?;
|
||||
Ok(Json(()))
|
||||
}
|
||||
|
||||
async fn get_user_info(
|
||||
State(state): State<Arc<AppState>>,
|
||||
State(state): State<Arc<RwLock<AppState>>>,
|
||||
TypedHeader(auth): TypedHeader<Authorization<Bearer>>,
|
||||
) -> Result<json::UserInfo> {
|
||||
let user = state.authorize_user(auth.token())?;
|
||||
let user = state.read().authorize_user(auth.token())?;
|
||||
let CurrentUser { email, .. } = user;
|
||||
Ok(Json(json::UserInfo {
|
||||
email: email.into_string(),
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
[package]
|
||||
name = "todo_app_sqlite_viz"
|
||||
name = "server_fns_axum"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
@@ -7,48 +7,53 @@ edition = "2021"
|
||||
crate-type = ["cdylib", "rlib"]
|
||||
|
||||
[dependencies]
|
||||
console_log = "1.0.0"
|
||||
console_error_panic_hook = "0.1.7"
|
||||
futures = "0.3.25"
|
||||
cfg-if = "1.0.0"
|
||||
leptos = { path = "../../leptos" }
|
||||
leptos_viz = { path = "../../integrations/viz", optional = true }
|
||||
console_log = "1.0"
|
||||
console_error_panic_hook = "0.1"
|
||||
futures = "0.3"
|
||||
http = "1.0"
|
||||
leptos = { path = "../../leptos", features = ["nightly"] }
|
||||
leptos_axum = { path = "../../integrations/axum", optional = true }
|
||||
leptos_meta = { path = "../../meta", features = ["nightly"] }
|
||||
leptos_router = { path = "../../router", features = ["nightly"] }
|
||||
leptos_reactive = { path = "../../leptos_reactive", features = ["nightly"] }
|
||||
log = "0.4.17"
|
||||
simple_logger = "4.0.0"
|
||||
server_fn = { path = "../../server_fn", features = ["serde-lite", "rkyv", "multipart" ]}
|
||||
log = "0.4"
|
||||
simple_logger = "4.0"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
viz = { version = "0.4.8", features = ["serve"], optional = true }
|
||||
tokio = { version = "1.25.0", features = ["full"], optional = true }
|
||||
http = { version = "0.2.11" }
|
||||
sqlx = { version = "0.6.2", features = [
|
||||
"runtime-tokio-rustls",
|
||||
"sqlite",
|
||||
], optional = true }
|
||||
thiserror = "1.0.38"
|
||||
axum = { version = "0.7", optional = true }
|
||||
tower = { version = "0.4", optional = true }
|
||||
tower-http = { version = "0.5", features = ["fs", "tracing", "trace"], optional = true }
|
||||
tokio = { version = "1", features = ["full"], optional = true }
|
||||
thiserror = "1.0"
|
||||
wasm-bindgen = "0.2"
|
||||
serde_toml = "0.0.1"
|
||||
toml = "0.8.8"
|
||||
web-sys = { version = "0.3.67", features = ["FileList", "File"] }
|
||||
strum = { version = "0.25.0", features = ["strum_macros", "derive"] }
|
||||
notify = { version = "6.1.1", optional = true }
|
||||
pin-project-lite = "0.2.13"
|
||||
|
||||
[features]
|
||||
csr = ["leptos/csr", "leptos_meta/csr", "leptos_router/csr"]
|
||||
hydrate = ["leptos/hydrate", "leptos_meta/hydrate", "leptos_router/hydrate"]
|
||||
ssr = [
|
||||
"dep:viz",
|
||||
"dep:tokio",
|
||||
"dep:sqlx",
|
||||
"leptos/ssr",
|
||||
"leptos_meta/ssr",
|
||||
"leptos_router/ssr",
|
||||
"dep:leptos_viz",
|
||||
"dep:axum",
|
||||
"dep:tower",
|
||||
"dep:tower-http",
|
||||
"dep:tokio",
|
||||
"leptos/ssr",
|
||||
"leptos_meta/ssr",
|
||||
"leptos_router/ssr",
|
||||
"dep:leptos_axum",
|
||||
"dep:notify"
|
||||
]
|
||||
notify = ["dep:notify"]
|
||||
|
||||
[package.metadata.cargo-all-features]
|
||||
denylist = ["viz", "tokio", "sqlx", "leptos_viz"]
|
||||
denylist = ["axum", "tower", "tower-http", "tokio", "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
|
||||
output-name = "todo_app_sqlite_viz"
|
||||
output-name = "server_fns_axum"
|
||||
# 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
|
||||
@@ -63,7 +68,8 @@ 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.
|
||||
end2end-cmd = "npx playwright test"
|
||||
end2end-cmd = "cargo make test-ui"
|
||||
end2end-dir = "e2e"
|
||||
# The browserlist query used for optimizing the CSS.
|
||||
browserquery = "defaults"
|
||||
# Set by cargo-leptos watch when building with tha tool. Controls whether autoreload JS will be included in the head
|
||||
12
examples/server_fns_axum/Makefile.toml
Normal file
12
examples/server_fns_axum/Makefile.toml
Normal file
@@ -0,0 +1,12 @@
|
||||
extend = [
|
||||
{ path = "../cargo-make/main.toml" },
|
||||
{ path = "../cargo-make/cargo-leptos-webdriver-test.toml" },
|
||||
]
|
||||
|
||||
[env]
|
||||
CLIENT_PROCESS_NAME = "todo_app_sqlite_axum"
|
||||
|
||||
[tasks.test-ui]
|
||||
cwd = "./e2e"
|
||||
command = "cargo"
|
||||
args = ["make", "test-ui", "${@}"]
|
||||
19
examples/server_fns_axum/README.md
Normal file
19
examples/server_fns_axum/README.md
Normal file
@@ -0,0 +1,19 @@
|
||||
# Leptos Todo App Sqlite with Axum
|
||||
|
||||
This example creates a basic todo app with an Axum backend that uses Leptos' server functions to call sqlx from the client and seamlessly run it on the server.
|
||||
|
||||
## Getting Started
|
||||
|
||||
See the [Examples README](../README.md) for setup and run instructions.
|
||||
|
||||
## E2E Testing
|
||||
|
||||
See the [E2E README](./e2e/README.md) for more information about the testing strategy.
|
||||
|
||||
## Rendering
|
||||
|
||||
See the [SSR Notes](../SSR_NOTES.md) for more information about Server Side Rendering.
|
||||
|
||||
## Quick Start
|
||||
|
||||
Run `cargo leptos watch` to run this example.
|
||||
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)
|
||||
}
|
||||
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user