mirror of
https://github.com/leptos-rs/leptos.git
synced 2025-12-28 07:52:34 -05:00
Compare commits
98 Commits
v0.6.0-bet
...
actix_midd
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
55ef106c50 | ||
|
|
26d9d75cf2 | ||
|
|
46e7abf9ba | ||
|
|
1b1e02729e | ||
|
|
fdd576535a | ||
|
|
2a9e502893 | ||
|
|
a519859a66 | ||
|
|
25120c0e9f | ||
|
|
94cb4c0ec3 | ||
|
|
f9cd8539e4 | ||
|
|
14072457d0 | ||
|
|
e179db1d42 | ||
|
|
2fa60103b4 | ||
|
|
a3a15f244d | ||
|
|
0df5dfeaf8 | ||
|
|
3f22906053 | ||
|
|
33ad30515d | ||
|
|
c5bab09423 | ||
|
|
320179bc04 | ||
|
|
5065bed594 | ||
|
|
22b4537f27 | ||
|
|
8d23d5136a | ||
|
|
c7fac64054 | ||
|
|
047235e7c1 | ||
|
|
7a086ad159 | ||
|
|
bb923b3f9b | ||
|
|
6a8c26a820 | ||
|
|
21f8085851 | ||
|
|
9a5a102ce3 | ||
|
|
4d602c21f8 | ||
|
|
7d114c7414 | ||
|
|
1f017a2ade | ||
|
|
35e8e74dcf | ||
|
|
4366d786ac | ||
|
|
1777a4057a | ||
|
|
0571ebbc36 | ||
|
|
06c478b7cb | ||
|
|
90ba3529e9 | ||
|
|
13a2691806 | ||
|
|
1ad7ee8a03 | ||
|
|
88fee243a8 | ||
|
|
5e08253521 | ||
|
|
cc6f65cd83 | ||
|
|
9488114801 | ||
|
|
b0cdeab906 | ||
|
|
def4be80b2 | ||
|
|
15b04a8a85 | ||
|
|
0a9cdba22e | ||
|
|
1d1de4ac38 | ||
|
|
31b2b9e94c | ||
|
|
8f07818687 | ||
|
|
a5cbfa0aad | ||
|
|
6c8e704fb3 | ||
|
|
81fb5160e5 | ||
|
|
2af0d3d781 | ||
|
|
7f532cda70 | ||
|
|
c7941f7639 | ||
|
|
61148026d1 | ||
|
|
738eeefe73 | ||
|
|
be084a5d1d | ||
|
|
f5c007df7b | ||
|
|
a1bd84f3dc | ||
|
|
f6ce82c9d1 | ||
|
|
853c080707 | ||
|
|
f6b95e40f4 | ||
|
|
db1497b9c2 | ||
|
|
f53ac1a4ae | ||
|
|
5e6f4403ca | ||
|
|
4e3f1c834c | ||
|
|
566df034ff | ||
|
|
fd97e2e027 | ||
|
|
c8fbee18c8 | ||
|
|
e1a9856ca9 | ||
|
|
60efaefff4 | ||
|
|
db4158f5c3 | ||
|
|
af62d2e900 | ||
|
|
c3e3ce7878 | ||
|
|
dec17fc65b | ||
|
|
2dbc5899f3 | ||
|
|
dd368a845c | ||
|
|
9c258219dd | ||
|
|
6a1685936b | ||
|
|
7d45e6bb13 | ||
|
|
8fae76828e | ||
|
|
d5b9e84f36 | ||
|
|
197edebd51 | ||
|
|
2a5c855595 | ||
|
|
c9627bfeb4 | ||
|
|
c7422cd96e | ||
|
|
cadd217078 | ||
|
|
0c4cf5471d | ||
|
|
dd0c349554 | ||
|
|
dd5a0ae094 | ||
|
|
5cacb57283 | ||
|
|
b356d3cd28 | ||
|
|
ae1de88916 | ||
|
|
67dd188358 | ||
|
|
1d4772251a |
2
.github/workflows/get-example-changed.yml
vendored
2
.github/workflows/get-example-changed.yml
vendored
@@ -24,7 +24,7 @@ jobs:
|
||||
uses: tj-actions/changed-files@v41
|
||||
with:
|
||||
files: |
|
||||
examples
|
||||
examples/**
|
||||
!examples/cargo-make
|
||||
!examples/gtk
|
||||
!examples/Makefile.toml
|
||||
|
||||
24
.github/workflows/get-leptos-changed.yml
vendored
24
.github/workflows/get-leptos-changed.yml
vendored
@@ -22,18 +22,18 @@ jobs:
|
||||
uses: tj-actions/changed-files@v41
|
||||
with:
|
||||
files: |
|
||||
integrations
|
||||
leptos
|
||||
leptos_config
|
||||
leptos_dom
|
||||
leptos_hot_reload
|
||||
leptos_macro
|
||||
leptos_reactive
|
||||
leptos_server
|
||||
meta
|
||||
router
|
||||
server_fn
|
||||
server_fn_macro
|
||||
integrations/**
|
||||
leptos/**
|
||||
leptos_config/**
|
||||
leptos_dom/**
|
||||
leptos_hot_reload/**
|
||||
leptos_macro/**
|
||||
leptos_reactive/**
|
||||
leptos_server/**
|
||||
meta/**
|
||||
router/**
|
||||
server_fn/**
|
||||
server_fn_macro/**
|
||||
|
||||
- name: List source files that changed
|
||||
run: echo '${{ steps.changed-source.outputs.all_changed_files }}'
|
||||
|
||||
@@ -34,8 +34,8 @@ 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" }
|
||||
server_fn_macro = { path = "./server_fn_macro", version = "0.6" }
|
||||
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" }
|
||||
|
||||
@@ -28,7 +28,9 @@ pub fn App() -> impl IntoView {
|
||||
}
|
||||
|
||||
#[server]
|
||||
async fn do_something(should_error: Option<String>) -> Result<String, ServerFnError> {
|
||||
async fn do_something(
|
||||
should_error: Option<String>,
|
||||
) -> Result<String, ServerFnError> {
|
||||
if should_error.is_none() {
|
||||
Ok(String::from("Successful submit"))
|
||||
} else {
|
||||
@@ -42,7 +44,12 @@ async fn do_something(should_error: Option<String>) -> Result<String, ServerFnEr
|
||||
#[component]
|
||||
fn HomePage() -> impl IntoView {
|
||||
let do_something_action = Action::<DoSomething, _>::server();
|
||||
let value = Signal::derive(move || do_something_action.value().get().unwrap_or_else(|| Ok(String::new())));
|
||||
let value = Signal::derive(move || {
|
||||
do_something_action
|
||||
.value()
|
||||
.get()
|
||||
.unwrap_or_else(|| Ok(String::new()))
|
||||
});
|
||||
|
||||
Effect::new_isomorphic(move |_| {
|
||||
logging::log!("Got value = {:?}", value.get());
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
#[cfg(feature = "ssr")]
|
||||
#[actix_web::main]
|
||||
async fn main() -> std::io::Result<()> {
|
||||
use action_form_error_handling::app::*;
|
||||
use actix_files::Files;
|
||||
use actix_web::*;
|
||||
use leptos::*;
|
||||
use leptos_actix::{generate_route_list, LeptosRoutes};
|
||||
use action_form_error_handling::app::*;
|
||||
|
||||
let conf = get_configuration(None).await.unwrap();
|
||||
let addr = conf.leptos_options.site_addr;
|
||||
@@ -43,8 +43,8 @@ pub fn main() {
|
||||
// a client-side main function is required for using `trunk serve`
|
||||
// prefer using `cargo leptos serve` instead
|
||||
// to run: `trunk serve --open --features csr`
|
||||
use leptos::*;
|
||||
use action_form_error_handling::app::*;
|
||||
use leptos::*;
|
||||
use wasm_bindgen::prelude::wasm_bindgen;
|
||||
|
||||
console_error_panic_hook::set_once();
|
||||
|
||||
@@ -5,7 +5,7 @@ use leptos_router::*;
|
||||
use tracing::instrument;
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
mod ssr_imports {
|
||||
pub mod ssr_imports {
|
||||
pub use broadcaster::BroadcastChannel;
|
||||
pub use once_cell::sync::OnceCell;
|
||||
pub use std::sync::atomic::{AtomicI32, Ordering};
|
||||
|
||||
@@ -1,57 +1,54 @@
|
||||
mod counters;
|
||||
|
||||
use leptos::*;
|
||||
use actix_files::{Files};
|
||||
use actix_web::*;
|
||||
use crate::counters::*;
|
||||
use leptos_actix::{generate_route_list, LeptosRoutes};
|
||||
use crate::counters::*;
|
||||
use actix_files::Files;
|
||||
use actix_web::*;
|
||||
use leptos::*;
|
||||
use leptos_actix::{generate_route_list, LeptosRoutes};
|
||||
|
||||
#[get("/api/events")]
|
||||
async fn counter_events() -> impl Responder {
|
||||
use futures::StreamExt;
|
||||
#[get("/api/events")]
|
||||
async fn counter_events() -> impl Responder {
|
||||
use crate::counters::ssr_imports::*;
|
||||
use futures::StreamExt;
|
||||
|
||||
let stream =
|
||||
futures::stream::once(async { crate::counters::get_server_count().await.unwrap_or(0) })
|
||||
.chain(COUNT_CHANNEL.clone())
|
||||
.map(|value| {
|
||||
Ok(web::Bytes::from(format!(
|
||||
"event: message\ndata: {value}\n\n"
|
||||
))) as Result<web::Bytes>
|
||||
});
|
||||
HttpResponse::Ok()
|
||||
.insert_header(("Content-Type", "text/event-stream"))
|
||||
.streaming(stream)
|
||||
}
|
||||
|
||||
#[actix_web::main]
|
||||
async fn main() -> std::io::Result<()> {
|
||||
|
||||
// Explicit server function registration is no longer required
|
||||
// on the main branch. On 0.3.0 and earlier, uncomment the lines
|
||||
// below to register the server functions.
|
||||
// _ = GetServerCount::register();
|
||||
// _ = AdjustServerCount::register();
|
||||
// _ = ClearServerCount::register();
|
||||
|
||||
// Setting this to None means we'll be using cargo-leptos and its env vars.
|
||||
// when not using cargo-leptos None must be replaced with Some("Cargo.toml")
|
||||
let conf = get_configuration(None).await.unwrap();
|
||||
|
||||
let addr = conf.leptos_options.site_addr;
|
||||
let routes = generate_route_list(Counters);
|
||||
|
||||
HttpServer::new(move || {
|
||||
let leptos_options = &conf.leptos_options;
|
||||
let site_root = &leptos_options.site_root;
|
||||
|
||||
App::new()
|
||||
.service(counter_events)
|
||||
.leptos_routes(leptos_options.to_owned(), routes.to_owned(), Counters)
|
||||
.service(Files::new("/", site_root))
|
||||
//.wrap(middleware::Compress::default())
|
||||
})
|
||||
.bind(&addr)?
|
||||
.run()
|
||||
.await
|
||||
}
|
||||
let stream = futures::stream::once(async {
|
||||
crate::counters::get_server_count().await.unwrap_or(0)
|
||||
})
|
||||
.chain(COUNT_CHANNEL.clone())
|
||||
.map(|value| {
|
||||
Ok(web::Bytes::from(format!(
|
||||
"event: message\ndata: {value}\n\n"
|
||||
))) as Result<web::Bytes>
|
||||
});
|
||||
HttpResponse::Ok()
|
||||
.insert_header(("Content-Type", "text/event-stream"))
|
||||
.streaming(stream)
|
||||
}
|
||||
|
||||
#[actix_web::main]
|
||||
async fn main() -> std::io::Result<()> {
|
||||
// Setting this to None means we'll be using cargo-leptos and its env vars.
|
||||
// when not using cargo-leptos None must be replaced with Some("Cargo.toml")
|
||||
let conf = get_configuration(None).await.unwrap();
|
||||
|
||||
let addr = conf.leptos_options.site_addr;
|
||||
let routes = generate_route_list(Counters);
|
||||
|
||||
HttpServer::new(move || {
|
||||
let leptos_options = &conf.leptos_options;
|
||||
let site_root = &leptos_options.site_root;
|
||||
|
||||
App::new()
|
||||
.service(counter_events)
|
||||
.leptos_routes(
|
||||
leptos_options.to_owned(),
|
||||
routes.to_owned(),
|
||||
Counters,
|
||||
)
|
||||
.service(Files::new("/", site_root))
|
||||
//.wrap(middleware::Compress::default())
|
||||
})
|
||||
.bind(&addr)?
|
||||
.run()
|
||||
.await
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ async fn main() {
|
||||
|
||||
// run our app with hyper
|
||||
// `axum::Server` is a re-export of `hyper::Server`
|
||||
logging::log!("listening on {}", addr);
|
||||
leptos::logging::log!("listening on {}", addr);
|
||||
let listener = tokio::net::TcpListener::bind(&addr).await.unwrap();
|
||||
axum::serve(listener, app.into_make_service())
|
||||
.await
|
||||
|
||||
3
examples/pavex_demo/.gitignore
vendored
3
examples/pavex_demo/.gitignore
vendored
@@ -1,3 +0,0 @@
|
||||
/target
|
||||
.env
|
||||
.direnv
|
||||
@@ -1,92 +0,0 @@
|
||||
[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
|
||||
@@ -1,71 +0,0 @@
|
||||
# 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
119
examples/pavex_demo/flake.lock
generated
@@ -1,119 +0,0 @@
|
||||
{
|
||||
"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
|
||||
}
|
||||
@@ -1,129 +0,0 @@
|
||||
{
|
||||
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";
|
||||
};
|
||||
});
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
[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"]
|
||||
@@ -1,73 +0,0 @@
|
||||
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>
|
||||
}
|
||||
}
|
||||
/>
|
||||
}
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
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>
|
||||
}
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
[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]
|
||||
@@ -1,13 +0,0 @@
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
body {
|
||||
font-family: sans-serif;
|
||||
text-align: center;
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
[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
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
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(())
|
||||
}
|
||||
@@ -1,98 +0,0 @@
|
||||
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));
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
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}"),
|
||||
)),
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
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)
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
mod blueprint;
|
||||
pub mod configuration;
|
||||
pub mod leptos;
|
||||
pub mod routes;
|
||||
pub mod telemetry;
|
||||
pub mod user_agent;
|
||||
pub use blueprint::blueprint;
|
||||
@@ -1,21 +0,0 @@
|
||||
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()
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
pub mod greet;
|
||||
pub mod status;
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
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
|
||||
}
|
||||
@@ -1,84 +0,0 @@
|
||||
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(),
|
||||
}
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
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()
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
[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"
|
||||
@@ -1,3 +0,0 @@
|
||||
server:
|
||||
ip: "0.0.0.0"
|
||||
port: 8000
|
||||
@@ -1,6 +0,0 @@
|
||||
# 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
|
||||
@@ -1,3 +0,0 @@
|
||||
server:
|
||||
ip: "0.0.0.0"
|
||||
port: 8000
|
||||
@@ -1,8 +0,0 @@
|
||||
# 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
|
||||
@@ -1,49 +0,0 @@
|
||||
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(())
|
||||
}
|
||||
@@ -1,140 +0,0 @@
|
||||
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
|
||||
)),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
pub mod configuration;
|
||||
pub mod telemetry;
|
||||
@@ -1,40 +0,0 @@
|
||||
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)
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
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"
|
||||
);
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
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.")
|
||||
}
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
mod greet;
|
||||
mod helpers;
|
||||
mod ping;
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
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());
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
[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" }
|
||||
@@ -1,233 +0,0 @@
|
||||
(
|
||||
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: [],
|
||||
)
|
||||
@@ -1,254 +0,0 @@
|
||||
//! 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,10 @@ test.describe("Test Router example", () => {
|
||||
await page.goto("/");
|
||||
});
|
||||
|
||||
test("Starts on correct home page", async({ page }) => {
|
||||
await expect(page.getByText("Select a contact.")).toBeVisible();
|
||||
});
|
||||
|
||||
const links = [
|
||||
{ label: "Bill Smith", url: "/0" },
|
||||
{ label: "Tim Jones", url: "/1" },
|
||||
|
||||
@@ -15,7 +15,7 @@ leptos = { path = "../../leptos", features = ["nightly"] }
|
||||
leptos_axum = { path = "../../integrations/axum", optional = true }
|
||||
leptos_meta = { path = "../../meta", features = ["nightly"] }
|
||||
leptos_router = { path = "../../router", features = ["nightly"] }
|
||||
server_fn = { path = "../../server_fn", features = ["serde-lite", "rkyv", "multipart"] }
|
||||
server_fn = { path = "../../server_fn", features = ["serde-lite", "rkyv", "multipart" ]}
|
||||
log = "0.4"
|
||||
simple_logger = "4.0"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
@@ -30,6 +30,7 @@ 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]
|
||||
hydrate = ["leptos/hydrate", "leptos_meta/hydrate", "leptos_router/hydrate"]
|
||||
|
||||
@@ -242,6 +242,7 @@ pub fn WithActionForm() -> impl IntoView {
|
||||
// In this case, any `tower::Layer` that takes services of `Request<Body>` will work
|
||||
#[middleware(crate::middleware::LoggingLayer)]
|
||||
pub async fn length_of_input(input: String) -> Result<usize, ServerFnError> {
|
||||
println!("2. Running server function.");
|
||||
// insert a simulated wait
|
||||
tokio::time::sleep(std::time::Duration::from_millis(250)).await;
|
||||
Ok(input.len())
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
use axum::body::Body;
|
||||
use http::Request;
|
||||
use std::task::{Context, Poll};
|
||||
use pin_project_lite::pin_project;
|
||||
use std::{
|
||||
future::Future,
|
||||
pin::Pin,
|
||||
task::{Context, Poll},
|
||||
};
|
||||
use tower::{Layer, Service};
|
||||
|
||||
pub struct LoggingLayer;
|
||||
@@ -23,7 +28,7 @@ where
|
||||
{
|
||||
type Response = T::Response;
|
||||
type Error = T::Error;
|
||||
type Future = T::Future;
|
||||
type Future = LoggingServiceFuture<T::Future>;
|
||||
|
||||
fn poll_ready(
|
||||
&mut self,
|
||||
@@ -33,8 +38,35 @@ where
|
||||
}
|
||||
|
||||
fn call(&mut self, req: Request<Body>) -> Self::Future {
|
||||
println!("Running my middleware!");
|
||||
println!("1. Running my middleware!");
|
||||
|
||||
self.inner.call(req)
|
||||
LoggingServiceFuture {
|
||||
inner: self.inner.call(req),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pin_project! {
|
||||
pub struct LoggingServiceFuture<T> {
|
||||
#[pin]
|
||||
inner: T,
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Future for LoggingServiceFuture<T>
|
||||
where
|
||||
T: Future,
|
||||
{
|
||||
type Output = T::Output;
|
||||
|
||||
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
|
||||
let this = self.project();
|
||||
match this.inner.poll(cx) {
|
||||
Poll::Pending => Poll::Pending,
|
||||
Poll::Ready(output) => {
|
||||
println!("3. Running my middleware!");
|
||||
Poll::Ready(output)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,16 +27,16 @@ 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" }
|
||||
sqlx = { version = "0.6.2", features = [
|
||||
sqlx = { version = "0.7", features = [
|
||||
"runtime-tokio-rustls",
|
||||
"sqlite",
|
||||
], optional = true }
|
||||
thiserror = "1.0.38"
|
||||
wasm-bindgen = "0.2"
|
||||
axum_session_auth = { version = "0.2.1", features = [
|
||||
axum_session_auth = { version = "0.9", features = [
|
||||
"sqlite-rustls",
|
||||
], optional = true }
|
||||
axum_session = { version = "0.2.3", features = [
|
||||
axum_session = { version = "0.9", features = [
|
||||
"sqlite-rustls",
|
||||
], optional = true }
|
||||
async-trait = { version = "0.1.64", optional = true }
|
||||
|
||||
@@ -69,8 +69,7 @@ if #[cfg(feature = "ssr")] {
|
||||
.with_security_mode(SecurityMode::PerSession);
|
||||
|
||||
let auth_config = AuthConfig::<i64>::default();
|
||||
let session_store = SessionStore::<SessionSqlitePool>::new(Some(pool.clone().into()), session_config);
|
||||
session_store.initiate().await.unwrap();
|
||||
let session_store = SessionStore::<SessionSqlitePool>::new(Some(pool.clone().into()), session_config).await.unwrap();
|
||||
|
||||
sqlx::migrate!()
|
||||
.run(&pool)
|
||||
|
||||
@@ -9,13 +9,12 @@ use thiserror::Error;
|
||||
pub fn App() -> impl IntoView {
|
||||
// Provides context that manages stylesheets, titles, meta tags, etc.
|
||||
provide_meta_context();
|
||||
let fallback = || view! { "Page not found." }.into_view();
|
||||
|
||||
view! {
|
||||
<Stylesheet id="leptos" href="/pkg/ssr_modes.css"/>
|
||||
<Title text="Welcome to Leptos"/>
|
||||
|
||||
<Router fallback>
|
||||
<Router>
|
||||
<main>
|
||||
<Routes>
|
||||
// We’ll load the home page with out-of-order streaming and <Suspense/>
|
||||
|
||||
@@ -9,13 +9,12 @@ use thiserror::Error;
|
||||
pub fn App() -> impl IntoView {
|
||||
// Provides context that manages stylesheets, titles, meta tags, etc.
|
||||
provide_meta_context();
|
||||
let fallback = || view! { "Page not found." }.into_view();
|
||||
|
||||
view! {
|
||||
<Stylesheet id="leptos" href="/pkg/ssr_modes.css"/>
|
||||
<Title text="Welcome to Leptos"/>
|
||||
|
||||
<Router fallback>
|
||||
<Router>
|
||||
<main>
|
||||
<Routes>
|
||||
// We’ll load the home page with out-of-order streaming and <Suspense/>
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
#[cfg(feature = "ssr")]
|
||||
pub mod middleware;
|
||||
pub mod todo;
|
||||
|
||||
#[cfg(feature = "hydrate")]
|
||||
|
||||
58
examples/todo_app_sqlite/src/middleware.rs
Normal file
58
examples/todo_app_sqlite/src/middleware.rs
Normal file
@@ -0,0 +1,58 @@
|
||||
use actix_web::{
|
||||
dev::{forward_ready, Service, ServiceRequest, ServiceResponse, Transform},
|
||||
Error,
|
||||
};
|
||||
use std::{
|
||||
future::{ready, Future, Ready},
|
||||
pin::Pin,
|
||||
};
|
||||
|
||||
pub struct LoggingLayer;
|
||||
|
||||
impl<S, B> Transform<S, ServiceRequest> for LoggingLayer
|
||||
where
|
||||
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
|
||||
S::Future: 'static,
|
||||
B: 'static,
|
||||
{
|
||||
type Response = ServiceResponse<B>;
|
||||
type Error = Error;
|
||||
type InitError = ();
|
||||
type Transform = LoggingService<S>;
|
||||
type Future = Ready<Result<Self::Transform, Self::InitError>>;
|
||||
|
||||
fn new_transform(&self, service: S) -> Self::Future {
|
||||
ready(Ok(LoggingService { service }))
|
||||
}
|
||||
}
|
||||
|
||||
pub struct LoggingService<S> {
|
||||
service: S,
|
||||
}
|
||||
|
||||
impl<S, B> Service<ServiceRequest> for LoggingService<S>
|
||||
where
|
||||
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
|
||||
S::Future: 'static,
|
||||
B: 'static,
|
||||
{
|
||||
type Response = ServiceResponse<B>;
|
||||
type Error = Error;
|
||||
type Future =
|
||||
Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>>>>;
|
||||
|
||||
forward_ready!(service);
|
||||
|
||||
fn call(&self, req: ServiceRequest) -> Self::Future {
|
||||
println!("1. Middleware running before server fn.");
|
||||
|
||||
let fut = self.service.call(req);
|
||||
|
||||
Box::pin(async move {
|
||||
let res = fut.await?;
|
||||
println!("3. Middleware running after server fn.");
|
||||
Ok(res)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,8 +22,8 @@ pub mod ssr {
|
||||
}
|
||||
}
|
||||
|
||||
// This is an example of leptos's server functions using an alternative CBOR encoding. Both the function arguments being sent
|
||||
// to the server and the server response will be encoded with CBOR. Good for binary data that doesn't encode well via the default methods
|
||||
/// This is an example of a server function using an alternative CBOR encoding. Both the function arguments being sent
|
||||
/// to the server and the server response will be encoded with CBOR. Good for binary data that doesn't encode well via the default methods
|
||||
#[server(encoding = "Cbor")]
|
||||
pub async fn get_todos() -> Result<Vec<Todo>, ServerFnError> {
|
||||
use self::ssr::*;
|
||||
@@ -49,6 +49,7 @@ pub async fn get_todos() -> Result<Vec<Todo>, ServerFnError> {
|
||||
}
|
||||
|
||||
#[server]
|
||||
#[middleware(crate::middleware::LoggingLayer)]
|
||||
pub async fn add_todo(title: String) -> Result<(), ServerFnError> {
|
||||
use self::ssr::*;
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ async fn main() {
|
||||
let app = Router::new()
|
||||
// server function handlers are normally set up by .leptos_routes()
|
||||
// here, we're not actually doing server side rendering, so we set up a manual
|
||||
// handler for the server fns
|
||||
// handler for the server fns
|
||||
// this should include a get() handler if you have any GetUrl-based server fns
|
||||
.route("/api/*fn_name", post(leptos_axum::handle_server_fns))
|
||||
.fallback(file_or_index_handler)
|
||||
|
||||
@@ -1,148 +0,0 @@
|
||||
use leptos::*;
|
||||
use leptos_actix::generate_route_list;
|
||||
use leptos_router::{Route, Router, Routes, TrailingSlash};
|
||||
|
||||
#[component]
|
||||
fn DefaultApp() -> impl IntoView {
|
||||
let view = || view! { "" };
|
||||
view! {
|
||||
<Router>
|
||||
<Routes>
|
||||
<Route path="/foo" view/>
|
||||
<Route path="/bar/" view/>
|
||||
<Route path="/baz/:id" view/>
|
||||
<Route path="/baz/:name/" view/>
|
||||
<Route path="/baz/*any" view/>
|
||||
</Routes>
|
||||
</Router>
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_default_app() {
|
||||
let routes = generate_route_list(DefaultApp);
|
||||
|
||||
// We still have access to the original (albeit normalized) Leptos paths:
|
||||
assert_same(
|
||||
&routes,
|
||||
|r| r.leptos_path(),
|
||||
&["/bar", "/baz/*any", "/baz/:id", "/baz/:name", "/foo"],
|
||||
);
|
||||
|
||||
// ... But leptos-actix has also reformatted "paths" to work for Actix.
|
||||
assert_same(
|
||||
&routes,
|
||||
|r| r.path(),
|
||||
&["/bar", "/baz/{id}", "/baz/{name}", "/baz/{tail:.*}", "/foo"],
|
||||
);
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn ExactApp() -> impl IntoView {
|
||||
let view = || view! { "" };
|
||||
let trailing_slash = TrailingSlash::Exact;
|
||||
view! {
|
||||
<Router trailing_slash>
|
||||
<Routes>
|
||||
<Route path="/foo" view/>
|
||||
<Route path="/bar/" view/>
|
||||
<Route path="/baz/:id" view/>
|
||||
<Route path="/baz/:name/" view/>
|
||||
<Route path="/baz/*any" view/>
|
||||
</Routes>
|
||||
</Router>
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_exact_app() {
|
||||
let routes = generate_route_list(ExactApp);
|
||||
|
||||
// In Exact mode, the Leptos paths no longer have their trailing slashes stripped:
|
||||
assert_same(
|
||||
&routes,
|
||||
|r| r.leptos_path(),
|
||||
&["/bar/", "/baz/*any", "/baz/:id", "/baz/:name/", "/foo"],
|
||||
);
|
||||
|
||||
// Actix paths also have trailing slashes as a result:
|
||||
assert_same(
|
||||
&routes,
|
||||
|r| r.path(),
|
||||
&[
|
||||
"/bar/",
|
||||
"/baz/{id}",
|
||||
"/baz/{name}/",
|
||||
"/baz/{tail:.*}",
|
||||
"/foo",
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn RedirectApp() -> impl IntoView {
|
||||
let view = || view! { "" };
|
||||
let trailing_slash = TrailingSlash::Redirect;
|
||||
view! {
|
||||
<Router trailing_slash>
|
||||
<Routes>
|
||||
<Route path="/foo" view/>
|
||||
<Route path="/bar/" view/>
|
||||
<Route path="/baz/:id" view/>
|
||||
<Route path="/baz/:name/" view/>
|
||||
<Route path="/baz/*any" view/>
|
||||
</Routes>
|
||||
</Router>
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_redirect_app() {
|
||||
let routes = generate_route_list(RedirectApp);
|
||||
|
||||
assert_same(
|
||||
&routes,
|
||||
|r| r.leptos_path(),
|
||||
&[
|
||||
"/bar",
|
||||
"/bar/",
|
||||
"/baz/*any",
|
||||
"/baz/:id",
|
||||
"/baz/:id/",
|
||||
"/baz/:name",
|
||||
"/baz/:name/",
|
||||
"/foo",
|
||||
"/foo/",
|
||||
],
|
||||
);
|
||||
|
||||
// ... But leptos-actix has also reformatted "paths" to work for Actix.
|
||||
assert_same(
|
||||
&routes,
|
||||
|r| r.path(),
|
||||
&[
|
||||
"/bar",
|
||||
"/bar/",
|
||||
"/baz/{id}",
|
||||
"/baz/{id}/",
|
||||
"/baz/{name}",
|
||||
"/baz/{name}/",
|
||||
"/baz/{tail:.*}",
|
||||
"/foo",
|
||||
"/foo/",
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
fn assert_same<'t, T, F, U>(
|
||||
input: &'t Vec<T>,
|
||||
mapper: F,
|
||||
expected_sorted_values: &[U],
|
||||
) where
|
||||
F: Fn(&'t T) -> U + 't,
|
||||
U: Ord + std::fmt::Debug,
|
||||
{
|
||||
let mut values: Vec<U> = input.iter().map(mapper).collect();
|
||||
values.sort();
|
||||
assert_eq!(values, expected_sorted_values);
|
||||
}
|
||||
@@ -383,10 +383,10 @@ pub type PinnedHtmlStream =
|
||||
///
|
||||
/// ## Provided Context Types
|
||||
/// This function always provides context values including the following types:
|
||||
/// - [`Parts`]
|
||||
/// - [`ResponseOptions`]
|
||||
/// - [`MetaContext`](leptos_meta::MetaContext)
|
||||
/// - [`RouterIntegrationContext`](leptos_router::RouterIntegrationContext)
|
||||
/// - [RequestParts]
|
||||
/// - [ResponseOptions]
|
||||
/// - [MetaContext](leptos_meta::MetaContext)
|
||||
/// - [RouterIntegrationContext](leptos_router::RouterIntegrationContext)
|
||||
#[tracing::instrument(level = "trace", fields(error), skip_all)]
|
||||
pub fn render_app_to_stream<IV>(
|
||||
options: LeptosOptions,
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
[package]
|
||||
name = "leptos_pavex"
|
||||
version = { workspace = true }
|
||||
edition = "2021"
|
||||
authors = ["Ben Wishovich"]
|
||||
license = "MIT"
|
||||
repository = "https://github.com/leptos-rs/leptos"
|
||||
description = "Pavex integrations for the Leptos web framework."
|
||||
|
||||
[dependencies]
|
||||
pavex = { git = "https://github.com/LukeMathWalker/pavex", branch = "main" }
|
||||
futures = "0.3"
|
||||
leptos = { workspace = true, features = ["ssr"] }
|
||||
leptos_meta = { workspace = true, features = ["ssr"] }
|
||||
leptos_router = { workspace = true, features = ["ssr"] }
|
||||
leptos_integration_utils = { workspace = true }
|
||||
parking_lot = "0.12.1"
|
||||
regex = "1.7.0"
|
||||
tracing = "0.1.37"
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
|
||||
[features]
|
||||
nonce = ["leptos/nonce"]
|
||||
experimental-islands = ["leptos_integration_utils/experimental-islands"]
|
||||
@@ -1,4 +0,0 @@
|
||||
extend = { path = "../../cargo-make/main.toml" }
|
||||
|
||||
[tasks.check-format]
|
||||
env = { LEPTOS_PROJECT_DIRECTORY = "../../" }
|
||||
File diff suppressed because it is too large
Load Diff
@@ -61,7 +61,7 @@ nightly = [
|
||||
"leptos_server/nightly",
|
||||
]
|
||||
serde = ["leptos_reactive/serde"]
|
||||
serde-lite = ["leptos_reactive/serde-lite", "server_fn/serde-lite"]
|
||||
serde-lite = ["leptos_reactive/serde-lite"]
|
||||
miniserde = ["leptos_reactive/miniserde"]
|
||||
rkyv = ["leptos_reactive/rkyv"]
|
||||
tracing = ["leptos_macro/tracing"]
|
||||
|
||||
@@ -53,7 +53,7 @@ mod hydrate_only {
|
||||
}
|
||||
});
|
||||
|
||||
pub static IS_HYDRATING: Cell<bool> = Cell::new(true);
|
||||
pub static IS_HYDRATING: Cell<bool> = const { Cell::new(true) };
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
@@ -133,7 +133,7 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
thread_local!(static ID: RefCell<HydrationKey> = RefCell::new(HydrationKey { outlet: 0, fragment: 0, error: 0, id: 0 }));
|
||||
thread_local!(static ID: RefCell<HydrationKey> = const {RefCell::new(HydrationKey { outlet: 0, fragment: 0, error: 0, id: 0 })});
|
||||
|
||||
/// Control and utility methods for hydration.
|
||||
pub struct HydrationCtx;
|
||||
|
||||
@@ -117,13 +117,13 @@ fn match_primitive() {
|
||||
assert_eq!(prop, r#"{"name": "test", "value": -1}"#);
|
||||
|
||||
// f64
|
||||
let test = 3.14;
|
||||
let test = 3.25;
|
||||
let prop = (&&Match {
|
||||
name: stringify! {test},
|
||||
value: std::cell::Cell::new(Some(&test)),
|
||||
})
|
||||
.spez();
|
||||
assert_eq!(prop, r#"{"name": "test", "value": 3.14}"#);
|
||||
assert_eq!(prop, r#"{"name": "test", "value": 3.25}"#);
|
||||
|
||||
// bool
|
||||
let test = true;
|
||||
|
||||
@@ -871,12 +871,15 @@ pub fn slot(args: proc_macro::TokenStream, s: TokenStream) -> TokenStream {
|
||||
/// relative to the prefix (defaults to the function name followed by unique hash)
|
||||
/// - `input`: the encoding for the arguments (defaults to `PostUrl`)
|
||||
/// - `output`: the encoding for the response (defaults to `Json`)
|
||||
/// - `client`: a custom `Client` implementation that will be used for this server fn
|
||||
/// - `encoding`: (legacy, may be deprecated in future) specifies the encoding, which may be one
|
||||
/// of the following (not case sensitive)
|
||||
/// - `"Url"`: `POST` request with URL-encoded arguments and JSON response
|
||||
/// - `"GetUrl"`: `GET` request with URL-encoded arguments and JSON response
|
||||
/// - `"Cbor"`: `POST` request with CBOR-encoded arguments and response
|
||||
/// - `"GetCbor"`: `GET` request with URL-encoded arguments and CBOR response
|
||||
/// - `req` and `res` specify the HTTP request and response types to be used on the server (these
|
||||
/// should usually only be necessary if you are integrating with a server other than Actix/Axum)
|
||||
///
|
||||
/// ```rust,ignore
|
||||
/// #[server(
|
||||
@@ -949,6 +952,8 @@ pub fn server(args: proc_macro::TokenStream, s: TokenStream) -> TokenStream {
|
||||
s.into(),
|
||||
Some(syn::parse_quote!(::leptos::server_fn)),
|
||||
"/api",
|
||||
None,
|
||||
None,
|
||||
) {
|
||||
Err(e) => e.to_compile_error().into(),
|
||||
Ok(s) => s.to_token_stream().into(),
|
||||
|
||||
@@ -26,7 +26,7 @@ cfg_if::cfg_if! {
|
||||
use std::cell::Cell;
|
||||
|
||||
thread_local! {
|
||||
static IS_SPECIAL_ZONE: Cell<bool> = Cell::new(false);
|
||||
static IS_SPECIAL_ZONE: Cell<bool> = const { Cell::new(false) };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -332,7 +332,7 @@ impl Default for SharedContext {
|
||||
|
||||
#[cfg(feature = "experimental-islands")]
|
||||
thread_local! {
|
||||
pub static NO_HYDRATE: Cell<bool> = Cell::new(true);
|
||||
pub static NO_HYDRATE: Cell<bool> = const { Cell::new(true) };
|
||||
}
|
||||
|
||||
#[cfg(feature = "experimental-islands")]
|
||||
|
||||
@@ -1518,7 +1518,7 @@ impl<S, T> UnserializableResource for ResourceState<S, T> {
|
||||
}
|
||||
|
||||
thread_local! {
|
||||
static SUPPRESS_RESOURCE_LOAD: Cell<bool> = Cell::new(false);
|
||||
static SUPPRESS_RESOURCE_LOAD: Cell<bool> = const { Cell::new(false) };
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
|
||||
@@ -242,7 +242,7 @@ impl<T> StoredValue<T> {
|
||||
with_runtime(|runtime| {
|
||||
let n = {
|
||||
let values = runtime.stored_values.borrow();
|
||||
values.get(self.id).map(Rc::clone)
|
||||
values.get(self.id).cloned()
|
||||
};
|
||||
|
||||
if let Some(n) = n {
|
||||
|
||||
@@ -93,8 +93,8 @@ where
|
||||
any(debug_assertions, feature = "ssr"),
|
||||
tracing::instrument(level = "trace", skip_all,)
|
||||
)]
|
||||
pub fn dispatch(&self, input: impl Into<I>) {
|
||||
self.0.with_value(|a| a.dispatch(input.into()))
|
||||
pub fn dispatch(&self, input: I) {
|
||||
self.0.with_value(|a| a.dispatch(input))
|
||||
}
|
||||
|
||||
/// Create an [Action].
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
use crate::{
|
||||
matching::{resolve_path, PathMatch, RouteDefinition, RouteMatch},
|
||||
ParamsMap, RouterContext, SsrMode, StaticData, StaticMode, StaticParamsMap,
|
||||
TrailingSlash,
|
||||
};
|
||||
use leptos::{leptos_dom::Transparent, *};
|
||||
use std::{
|
||||
@@ -15,17 +14,7 @@ use std::{
|
||||
};
|
||||
|
||||
thread_local! {
|
||||
static ROUTE_ID: Cell<usize> = Cell::new(0);
|
||||
}
|
||||
|
||||
// RouteDefinition.id is `pub` and required to be unique.
|
||||
// Should we make this public so users can generate unique IDs?
|
||||
pub(in crate::components) fn new_route_id() -> usize {
|
||||
ROUTE_ID.with(|id| {
|
||||
let next = id.get() + 1;
|
||||
id.set(next);
|
||||
next
|
||||
})
|
||||
static ROUTE_ID: Cell<usize> = const { Cell::new(0) };
|
||||
}
|
||||
|
||||
/// Represents an HTTP method that can be handled by this route.
|
||||
@@ -76,11 +65,6 @@ pub fn Route<E, F, P>(
|
||||
/// accessed with [`use_route_data`](crate::use_route_data).
|
||||
#[prop(optional, into)]
|
||||
data: Option<Loader>,
|
||||
/// How this route should handle trailing slashes in its path.
|
||||
/// Overrides any setting applied to [`crate::components::Router`].
|
||||
/// Serves as a default for any inner Routes.
|
||||
#[prop(optional)]
|
||||
trailing_slash: Option<TrailingSlash>,
|
||||
/// `children` may be empty or include nested routes.
|
||||
#[prop(optional)]
|
||||
children: Option<Children>,
|
||||
@@ -99,7 +83,6 @@ where
|
||||
data,
|
||||
None,
|
||||
None,
|
||||
trailing_slash,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -132,11 +115,6 @@ pub fn ProtectedRoute<P, E, F, C>(
|
||||
/// accessed with [`use_route_data`](crate::use_route_data).
|
||||
#[prop(optional, into)]
|
||||
data: Option<Loader>,
|
||||
/// How this route should handle trailing slashes in its path.
|
||||
/// Overrides any setting applied to [`crate::components::Router`].
|
||||
/// Serves as a default for any inner Routes.
|
||||
#[prop(optional)]
|
||||
trailing_slash: Option<TrailingSlash>,
|
||||
/// `children` may be empty or include nested routes.
|
||||
#[prop(optional)]
|
||||
children: Option<Children>,
|
||||
@@ -165,7 +143,6 @@ where
|
||||
data,
|
||||
None,
|
||||
None,
|
||||
trailing_slash,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -194,11 +171,6 @@ pub fn StaticRoute<E, F, P, S>(
|
||||
/// accessed with [`use_route_data`](crate::use_route_data).
|
||||
#[prop(optional, into)]
|
||||
data: Option<Loader>,
|
||||
/// How this route should handle trailing slashes in its path.
|
||||
/// Overrides any setting applied to [`crate::components::Router`].
|
||||
/// Serves as a default for any inner Routes.
|
||||
#[prop(optional)]
|
||||
trailing_slash: Option<TrailingSlash>,
|
||||
/// `children` may be empty or include nested routes.
|
||||
#[prop(optional)]
|
||||
children: Option<Children>,
|
||||
@@ -221,7 +193,6 @@ where
|
||||
data,
|
||||
Some(mode),
|
||||
Some(Arc::new(static_params)),
|
||||
trailing_slash,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -239,7 +210,6 @@ pub(crate) fn define_route(
|
||||
data: Option<Loader>,
|
||||
static_mode: Option<StaticMode>,
|
||||
static_params: Option<StaticData>,
|
||||
trailing_slash: Option<TrailingSlash>,
|
||||
) -> RouteDefinition {
|
||||
let children = children
|
||||
.map(|children| {
|
||||
@@ -256,8 +226,14 @@ pub(crate) fn define_route(
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
let id = ROUTE_ID.with(|id| {
|
||||
let next = id.get() + 1;
|
||||
id.set(next);
|
||||
next
|
||||
});
|
||||
|
||||
RouteDefinition {
|
||||
id: new_route_id(),
|
||||
id,
|
||||
path,
|
||||
children,
|
||||
view,
|
||||
@@ -266,7 +242,6 @@ pub(crate) fn define_route(
|
||||
data,
|
||||
static_mode,
|
||||
static_params,
|
||||
trailing_slash,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,6 @@ use crate::{
|
||||
use crate::{unescape, Url};
|
||||
use cfg_if::cfg_if;
|
||||
use leptos::{
|
||||
logging::debug_warn,
|
||||
server_fn::{
|
||||
error::{ServerFnErrorSerde, ServerFnUrlError},
|
||||
redirect::RedirectHook,
|
||||
@@ -41,16 +40,13 @@ pub fn Router(
|
||||
/// A signal that will be set while the navigation process is underway.
|
||||
#[prop(optional, into)]
|
||||
set_is_routing: Option<SignalSetter<bool>>,
|
||||
/// How trailing slashes should be handled in [`Route`] paths.
|
||||
#[prop(optional)]
|
||||
trailing_slash: Option<TrailingSlash>,
|
||||
/// The `<Router/>` should usually wrap your whole page. It can contain
|
||||
/// any elements, and should include a [`Routes`](crate::Routes) component somewhere
|
||||
/// to define and display [`Route`](crate::Route)s.
|
||||
children: Children,
|
||||
) -> impl IntoView {
|
||||
// create a new RouterContext and provide it to every component beneath the router
|
||||
let router = RouterContext::new(base, fallback, trailing_slash);
|
||||
let router = RouterContext::new(base, fallback);
|
||||
provide_context(router);
|
||||
provide_context(GlobalSuspenseContext::new());
|
||||
if let Some(set_is_routing) = set_is_routing {
|
||||
@@ -70,9 +66,7 @@ pub fn Router(
|
||||
}
|
||||
});
|
||||
}) as RedirectHook;
|
||||
if server_fn::redirect::set_redirect_hook(router_hook).is_err() {
|
||||
debug_warn!("Error setting <Router/> server function redirect hook.");
|
||||
}
|
||||
_ = server_fn::redirect::set_redirect_hook(router_hook);
|
||||
|
||||
// provide ServerFnUrlError if it exists
|
||||
let location = use_location();
|
||||
@@ -99,7 +93,6 @@ pub(crate) struct RouterContextInner {
|
||||
id: usize,
|
||||
pub location: Location,
|
||||
pub base: RouteContext,
|
||||
trailing_slash: Option<TrailingSlash>,
|
||||
pub possible_routes: RefCell<Option<Vec<Branch>>>,
|
||||
#[allow(unused)] // used in CSR/hydrate
|
||||
base_path: String,
|
||||
@@ -136,7 +129,6 @@ impl RouterContext {
|
||||
pub(crate) fn new(
|
||||
base: Option<&'static str>,
|
||||
fallback: Option<fn() -> View>,
|
||||
trailing_slash: Option<TrailingSlash>,
|
||||
) -> Self {
|
||||
cfg_if! {
|
||||
if #[cfg(any(feature = "csr", feature = "hydrate"))] {
|
||||
@@ -218,7 +210,6 @@ impl RouterContext {
|
||||
path_stack: store_value(vec![location.pathname.get_untracked()]),
|
||||
location,
|
||||
base,
|
||||
trailing_slash,
|
||||
history: Box::new(history),
|
||||
|
||||
reference,
|
||||
@@ -257,10 +248,6 @@ impl RouterContext {
|
||||
self.inner.id
|
||||
}
|
||||
|
||||
pub(crate) fn trailing_slash(&self) -> Option<TrailingSlash> {
|
||||
self.inner.trailing_slash.clone()
|
||||
}
|
||||
|
||||
/// A list of all possible routes this router can match.
|
||||
pub fn possible_branches(&self) -> Vec<Branch> {
|
||||
self.inner
|
||||
@@ -518,71 +505,3 @@ impl Default for NavigateOptions {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Declares how you would like to handle trailing slashes in Route paths. This
|
||||
/// can be set on [`Router`] and overridden in [`crate::components::Route`]
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub enum TrailingSlash {
|
||||
/// This is the default behavior as of Leptos 0.5. Trailing slashes in your
|
||||
/// `Route` path are stripped. i.e.: the following two route declarations
|
||||
/// are equivalent:
|
||||
/// * `<Route path="/foo">`
|
||||
/// * `<Route path="/foo/">`
|
||||
Drop,
|
||||
|
||||
/// This mode will respect your path as it is written. Ex:
|
||||
/// * If you specify `<Route path="/foo">`, then `/foo` matches, but
|
||||
/// `/foo/` does not.
|
||||
/// * If you specify `<Route path="/foo/">`, then `/foo/` matches, but
|
||||
/// `/foo` does not.
|
||||
Exact,
|
||||
|
||||
/// Like `Exact`, this mode respects your path as-written. But it will also
|
||||
/// add redirects to the specified path if a user nagivates to a URL that is
|
||||
/// off by only the trailing slash.
|
||||
///
|
||||
/// Given `<Route path="/foo">`
|
||||
/// * Visiting `/foo` is valid.
|
||||
/// * Visiting `/foo/` serves a redirect to `/foo`
|
||||
///
|
||||
/// Given `<Route path="/foo/">`
|
||||
/// * Visiting `/foo` serves a redirect to `/foo/`
|
||||
/// * Visiting `/foo/` is valid.
|
||||
Redirect,
|
||||
}
|
||||
|
||||
impl Default for TrailingSlash {
|
||||
fn default() -> Self {
|
||||
// This is the behavior in 0.5. Keeping it the default for backward compatibility.
|
||||
// TODO: Change to `Redirect` in 0.6?
|
||||
Self::Drop
|
||||
}
|
||||
}
|
||||
|
||||
impl TrailingSlash {
|
||||
/// Should we redirect requests that come in with the wrong (extra/missing) trailng slash?
|
||||
pub(crate) fn should_redirect(&self) -> bool {
|
||||
use TrailingSlash::*;
|
||||
match self {
|
||||
Redirect => true,
|
||||
Drop | Exact => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn normalize_route_path(&self, path: &mut String) {
|
||||
if !self.should_drop() {
|
||||
return;
|
||||
}
|
||||
while path.ends_with('/') {
|
||||
path.pop();
|
||||
}
|
||||
}
|
||||
|
||||
fn should_drop(&self) -> bool {
|
||||
use TrailingSlash::*;
|
||||
match self {
|
||||
Redirect | Exact => false,
|
||||
Drop => true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
use crate::{
|
||||
animation::*,
|
||||
components::route::new_route_id,
|
||||
matching::{
|
||||
expand_optionals, get_route_matches, join_paths, Branch, Matcher,
|
||||
RouteDefinition, RouteMatch,
|
||||
},
|
||||
use_is_back_navigation, use_route, NavigateOptions, Redirect, RouteContext,
|
||||
RouterContext, SetIsRouting, TrailingSlash,
|
||||
use_is_back_navigation, RouteContext, RouterContext, SetIsRouting,
|
||||
};
|
||||
use leptos::{leptos_dom::HydrationCtx, *};
|
||||
use std::{
|
||||
@@ -84,7 +82,7 @@ pub fn Routes(
|
||||
let base_route = router.base();
|
||||
let base = base.unwrap_or_default();
|
||||
|
||||
Branches::initialize(&router, &base, children());
|
||||
Branches::initialize(router_id, &base, children());
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
if let Some(context) = use_context::<crate::PossibleBranchContext>() {
|
||||
@@ -166,7 +164,7 @@ pub fn AnimatedRoutes(
|
||||
let base_route = router.base();
|
||||
let base = base.unwrap_or_default();
|
||||
|
||||
Branches::initialize(&router, &base, children());
|
||||
Branches::initialize(router_id, &base, children());
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
if let Some(context) = use_context::<crate::PossibleBranchContext>() {
|
||||
@@ -203,8 +201,8 @@ pub fn AnimatedRoutes(
|
||||
let matches =
|
||||
get_route_matches(router_id, &base, next_route.clone());
|
||||
let same_route = prev_matches
|
||||
.and_then(|p| p.first().as_ref().map(|r| r.route.key.clone()))
|
||||
== matches.first().as_ref().map(|r| r.route.key.clone());
|
||||
.and_then(|p| p.first().map(|r| r.route.key.clone()))
|
||||
== matches.first().map(|r| r.route.key.clone());
|
||||
if same_route {
|
||||
(animation_state, next_route)
|
||||
} else {
|
||||
@@ -280,7 +278,7 @@ thread_local! {
|
||||
}
|
||||
|
||||
impl Branches {
|
||||
pub fn initialize(router: &RouterContext, base: &str, children: Fragment) {
|
||||
pub fn initialize(router_id: usize, base: &str, children: Fragment) {
|
||||
BRANCHES.with(|branches| {
|
||||
#[cfg(debug_assertions)]
|
||||
{
|
||||
@@ -295,9 +293,9 @@ impl Branches {
|
||||
}
|
||||
|
||||
let mut current = branches.borrow_mut();
|
||||
if !current.contains_key(&(router.id(), Cow::from(base))) {
|
||||
if !current.contains_key(&(router_id, Cow::from(base))) {
|
||||
let mut branches = Vec::new();
|
||||
let mut children = children
|
||||
let children = children
|
||||
.as_children()
|
||||
.iter()
|
||||
.filter_map(|child| {
|
||||
@@ -317,7 +315,6 @@ impl Branches {
|
||||
})
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
inherit_settings(&mut children, router);
|
||||
create_branches(
|
||||
&children,
|
||||
base,
|
||||
@@ -326,7 +323,7 @@ impl Branches {
|
||||
true,
|
||||
base,
|
||||
);
|
||||
current.insert((router.id(), Cow::Owned(base.into())), branches);
|
||||
current.insert((router_id, Cow::Owned(base.into())), branches);
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -347,38 +344,6 @@ impl Branches {
|
||||
}
|
||||
}
|
||||
|
||||
// <Route>s may inherit settings from each other or <Router>.
|
||||
// This mutates RouteDefinitions to propagate those settings.
|
||||
fn inherit_settings(children: &mut [RouteDefinition], router: &RouterContext) {
|
||||
struct InheritProps {
|
||||
trailing_slash: Option<TrailingSlash>,
|
||||
}
|
||||
fn route_def_inherit(
|
||||
children: &mut [RouteDefinition],
|
||||
inherited: InheritProps,
|
||||
) {
|
||||
for child in children {
|
||||
if child.trailing_slash.is_none() {
|
||||
child.trailing_slash = inherited.trailing_slash.clone();
|
||||
}
|
||||
route_def_inherit(
|
||||
&mut child.children,
|
||||
InheritProps {
|
||||
trailing_slash: child.trailing_slash.clone(),
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
route_def_inherit(
|
||||
children,
|
||||
InheritProps {
|
||||
trailing_slash: router
|
||||
.trailing_slash()
|
||||
.or(Some(TrailingSlash::default())),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
fn route_states(
|
||||
router_id: usize,
|
||||
base: String,
|
||||
@@ -588,7 +553,6 @@ pub(crate) struct RouterState {
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct RouteData {
|
||||
// This ID is always the same as key.id. Deprecate?
|
||||
pub id: usize,
|
||||
pub key: RouteDefinition,
|
||||
pub pattern: String,
|
||||
@@ -683,114 +647,24 @@ fn create_routes(
|
||||
parents_path, route_def.path
|
||||
);
|
||||
}
|
||||
let trailing_slash = route_def
|
||||
.trailing_slash
|
||||
.clone()
|
||||
.expect("trailng_slash should be set by this point");
|
||||
let mut acc = Vec::new();
|
||||
for original_path in expand_optionals(&route_def.path) {
|
||||
let mut path = join_paths(base, &original_path).to_string();
|
||||
trailing_slash.normalize_route_path(&mut path);
|
||||
let path = join_paths(base, &original_path);
|
||||
let pattern = if is_leaf {
|
||||
path
|
||||
} else if let Some((path, _splat)) = path.split_once("/*") {
|
||||
path.to_string()
|
||||
} else {
|
||||
path
|
||||
path.split("/*")
|
||||
.next()
|
||||
.map(|n| n.to_string())
|
||||
.unwrap_or(path)
|
||||
};
|
||||
|
||||
let route_data = RouteData {
|
||||
acc.push(RouteData {
|
||||
key: route_def.clone(),
|
||||
id: route_def.id,
|
||||
matcher: Matcher::new_with_partial(&pattern, !is_leaf),
|
||||
pattern,
|
||||
original_path: original_path.into_owned(),
|
||||
};
|
||||
|
||||
if route_data.matcher.is_wildcard() {
|
||||
// already handles trailing_slash
|
||||
} else if let Some(redirect_route) = redirect_route_for(route_def) {
|
||||
let pattern = &redirect_route.path;
|
||||
let redirect_route_data = RouteData {
|
||||
id: redirect_route.id,
|
||||
matcher: Matcher::new_with_partial(pattern, !is_leaf),
|
||||
pattern: pattern.to_owned(),
|
||||
original_path: pattern.to_owned(),
|
||||
key: redirect_route,
|
||||
};
|
||||
acc.push(redirect_route_data);
|
||||
}
|
||||
|
||||
acc.push(route_data);
|
||||
});
|
||||
}
|
||||
acc
|
||||
}
|
||||
|
||||
/// A new route that redirects to `route` with the correct trailng slash.
|
||||
fn redirect_route_for(route: &RouteDefinition) -> Option<RouteDefinition> {
|
||||
if matches!(route.path.as_str(), "" | "/") {
|
||||
// Root paths are an exception to the rule and are always equivalent:
|
||||
return None;
|
||||
}
|
||||
|
||||
let trailing_slash = route
|
||||
.trailing_slash
|
||||
.clone()
|
||||
.expect("trailing_slash should be defined by now");
|
||||
|
||||
if !trailing_slash.should_redirect() {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Are we creating a new route that adds or removes a slash?
|
||||
let add_slash = route.path.ends_with('/');
|
||||
let view = Rc::new(move || {
|
||||
view! {
|
||||
<FixTrailingSlash add_slash />
|
||||
}
|
||||
.into_view()
|
||||
});
|
||||
|
||||
let new_pattern = if add_slash {
|
||||
// If we need to add a slash, we need to match on the path w/o it:
|
||||
let mut path = route.path.clone();
|
||||
path.pop();
|
||||
path
|
||||
} else {
|
||||
format!("{}/", route.path)
|
||||
};
|
||||
let new_route = RouteDefinition {
|
||||
path: new_pattern,
|
||||
children: vec![],
|
||||
data: None,
|
||||
methods: route.methods,
|
||||
id: new_route_id(),
|
||||
view,
|
||||
ssr_mode: route.ssr_mode,
|
||||
static_mode: route.static_mode,
|
||||
static_params: None,
|
||||
trailing_slash: None, // Shouldn't be needed/used from here on out
|
||||
};
|
||||
|
||||
Some(new_route)
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn FixTrailingSlash(add_slash: bool) -> impl IntoView {
|
||||
let route = use_route();
|
||||
let path = if add_slash {
|
||||
format!("{}/", route.path())
|
||||
} else {
|
||||
let mut path = route.path().to_string();
|
||||
path.pop();
|
||||
path
|
||||
};
|
||||
let options = NavigateOptions {
|
||||
replace: true,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
view! {
|
||||
<Redirect path options/>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
mod test_extract_routes;
|
||||
|
||||
use crate::{
|
||||
provide_server_redirect, Branch, Method, RouterIntegrationContext,
|
||||
ServerIntegration, SsrMode, StaticDataMap, StaticMode, StaticParamsMap,
|
||||
StaticPath,
|
||||
Branch, Method, RouterIntegrationContext, ServerIntegration, SsrMode,
|
||||
StaticDataMap, StaticMode, StaticParamsMap, StaticPath,
|
||||
};
|
||||
use leptos::*;
|
||||
use std::{
|
||||
@@ -45,9 +42,6 @@ impl RouteListing {
|
||||
}
|
||||
|
||||
/// The path this route handles.
|
||||
///
|
||||
/// This should be formatted for whichever web server integegration is being used. (ex: leptos-actix.)
|
||||
/// When returned from leptos-router, it matches `self.leptos_path()`.
|
||||
pub fn path(&self) -> &str {
|
||||
&self.path
|
||||
}
|
||||
@@ -134,9 +128,21 @@ where
|
||||
{
|
||||
let runtime = create_runtime();
|
||||
|
||||
let branches = get_branches(app_fn, additional_context);
|
||||
let branches = branches.0.borrow();
|
||||
let integration = ServerIntegration {
|
||||
path: "http://leptos.rs/".to_string(),
|
||||
};
|
||||
|
||||
provide_context(RouterIntegrationContext::new(integration));
|
||||
let branches = PossibleBranchContext::default();
|
||||
provide_context(branches.clone());
|
||||
|
||||
additional_context();
|
||||
|
||||
leptos::suppress_resource_load(true);
|
||||
_ = app_fn().into_view();
|
||||
leptos::suppress_resource_load(false);
|
||||
|
||||
let branches = branches.0.borrow();
|
||||
let mut static_data_map: StaticDataMap = HashMap::new();
|
||||
let routes = branches
|
||||
.iter()
|
||||
@@ -176,29 +182,3 @@ where
|
||||
runtime.dispose();
|
||||
(routes, static_data_map)
|
||||
}
|
||||
|
||||
fn get_branches<IV>(
|
||||
app_fn: impl Fn() -> IV + 'static + Clone,
|
||||
additional_context: impl Fn() + 'static + Clone,
|
||||
) -> PossibleBranchContext
|
||||
where
|
||||
IV: IntoView + 'static,
|
||||
{
|
||||
let integration = ServerIntegration {
|
||||
path: "http://leptos.rs/".to_string(),
|
||||
};
|
||||
|
||||
provide_context(RouterIntegrationContext::new(integration));
|
||||
let branches = PossibleBranchContext::default();
|
||||
provide_context(branches.clone());
|
||||
// Suppress startup warning about using <Redirect/> without ServerRedirectFunction:
|
||||
provide_server_redirect(|_str| ());
|
||||
|
||||
additional_context();
|
||||
|
||||
leptos::suppress_resource_load(true);
|
||||
_ = app_fn().into_view();
|
||||
leptos::suppress_resource_load(false);
|
||||
|
||||
branches
|
||||
}
|
||||
|
||||
@@ -1,258 +0,0 @@
|
||||
// This is here, vs /router/tests/, because it accesses some `pub(crate)`
|
||||
// features to test crate internals that wouldn't be available there.
|
||||
|
||||
#![cfg(all(test, feature = "ssr"))]
|
||||
|
||||
use crate::*;
|
||||
use itertools::Itertools;
|
||||
use leptos::*;
|
||||
use std::{cell::RefCell, rc::Rc};
|
||||
|
||||
#[component]
|
||||
fn DefaultApp() -> impl IntoView {
|
||||
let view = || view! { "" };
|
||||
view! {
|
||||
<Router>
|
||||
<Routes>
|
||||
<Route path="/foo" view/>
|
||||
<Route path="/bar/" view/>
|
||||
<Route path="/baz/:id" view/>
|
||||
<Route path="/name/:name/" view/>
|
||||
<Route path="/any/*any" view/>
|
||||
</Routes>
|
||||
</Router>
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn ExactApp() -> impl IntoView {
|
||||
let view = || view! { "" };
|
||||
let trailing_slash = TrailingSlash::Exact;
|
||||
view! {
|
||||
<Router trailing_slash>
|
||||
<Routes>
|
||||
<Route path="/foo" view/>
|
||||
<Route path="/bar/" view/>
|
||||
<Route path="/baz/:id" view/>
|
||||
<Route path="/name/:name/" view/>
|
||||
<Route path="/any/*any" view/>
|
||||
</Routes>
|
||||
</Router>
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn RedirectApp() -> impl IntoView {
|
||||
let view = || view! { "" };
|
||||
let trailing_slash = TrailingSlash::Redirect;
|
||||
view! {
|
||||
<Router trailing_slash>
|
||||
<Routes>
|
||||
<Route path="/foo" view/>
|
||||
<Route path="/bar/" view/>
|
||||
<Route path="/baz/:id" view/>
|
||||
<Route path="/name/:name/" view/>
|
||||
<Route path="/any/*any" view/>
|
||||
</Routes>
|
||||
</Router>
|
||||
}
|
||||
}
|
||||
#[test]
|
||||
fn test_generated_routes_default() {
|
||||
// By default, we use the behavior as of Leptos 0.5, which is equivalent to TrailingSlash::Drop.
|
||||
assert_generated_paths(
|
||||
DefaultApp,
|
||||
&["/any/*any", "/bar", "/baz/:id", "/foo", "/name/:name"],
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_generated_routes_exact() {
|
||||
// Allow users to precisely define whether slashes are present:
|
||||
assert_generated_paths(
|
||||
ExactApp,
|
||||
&["/any/*any", "/bar/", "/baz/:id", "/foo", "/name/:name/"],
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_generated_routes_redirect() {
|
||||
// TralingSlashes::Redirect generates paths to redirect to the path with the "correct" trailing slash ending (or lack thereof).
|
||||
assert_generated_paths(
|
||||
RedirectApp,
|
||||
&[
|
||||
"/any/*any",
|
||||
"/bar",
|
||||
"/bar/",
|
||||
"/baz/:id",
|
||||
"/baz/:id/",
|
||||
"/foo",
|
||||
"/foo/",
|
||||
"/name/:name",
|
||||
"/name/:name/",
|
||||
],
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_rendered_redirect() {
|
||||
// Given an app that uses TrailngSlsahes::Redirect, rendering the redirected path
|
||||
// should render the redirect. Other paths should not.
|
||||
|
||||
let expected_redirects = &[
|
||||
("/bar", "/bar/"),
|
||||
("/baz/some_id/", "/baz/some_id"),
|
||||
("/name/some_name", "/name/some_name/"),
|
||||
("/foo/", "/foo"),
|
||||
];
|
||||
|
||||
let redirect_result = Rc::new(RefCell::new(Option::None));
|
||||
let rc = redirect_result.clone();
|
||||
let server_redirect = move |new_value: &str| {
|
||||
rc.replace(Some(new_value.to_string()));
|
||||
};
|
||||
|
||||
let _runtime = Disposable(create_runtime());
|
||||
let history = TestHistory::new("/");
|
||||
provide_context(RouterIntegrationContext::new(history.clone()));
|
||||
provide_server_redirect(server_redirect);
|
||||
|
||||
// We expect these redirects to exist:
|
||||
for (src, dest) in expected_redirects {
|
||||
let loc = format!("https://example.com{src}");
|
||||
history.goto(&loc);
|
||||
redirect_result.replace(None);
|
||||
RedirectApp().into_view().render_to_string();
|
||||
let redirected_to = redirect_result.borrow().clone();
|
||||
assert!(
|
||||
redirected_to.is_some(),
|
||||
"Should redirect from {src} to {dest}"
|
||||
);
|
||||
assert_eq!(redirected_to.unwrap(), *dest);
|
||||
}
|
||||
|
||||
// But the destination paths shouldn't themselves redirect:
|
||||
redirect_result.replace(None);
|
||||
for (_src, dest) in expected_redirects {
|
||||
let loc = format!("https://example.com{dest}");
|
||||
history.goto(&loc);
|
||||
RedirectApp().into_view().render_to_string();
|
||||
let redirected_to = redirect_result.borrow().clone();
|
||||
assert!(
|
||||
redirected_to.is_none(),
|
||||
"Destination of redirect shouldn't also redirect: {dest}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
struct Disposable(RuntimeId);
|
||||
|
||||
// If the test fails, and we don't dispose, we get irrelevant panics.
|
||||
impl Drop for Disposable {
|
||||
fn drop(&mut self) {
|
||||
self.0.dispose()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct TestHistory {
|
||||
loc: RwSignal<LocationChange>,
|
||||
}
|
||||
|
||||
impl TestHistory {
|
||||
fn new(initial: &str) -> Self {
|
||||
let lc = LocationChange {
|
||||
value: initial.to_owned(),
|
||||
..Default::default()
|
||||
};
|
||||
Self {
|
||||
loc: create_rw_signal(lc),
|
||||
}
|
||||
}
|
||||
|
||||
fn goto(&self, loc: &str) {
|
||||
let change = LocationChange {
|
||||
value: loc.to_string(),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
self.navigate(&change);
|
||||
}
|
||||
}
|
||||
|
||||
impl History for TestHistory {
|
||||
fn location(&self) -> ReadSignal<LocationChange> {
|
||||
self.loc.read_only()
|
||||
}
|
||||
|
||||
fn navigate(&self, new_loc: &LocationChange) {
|
||||
self.loc.update(|loc| loc.value = new_loc.value.clone())
|
||||
}
|
||||
}
|
||||
|
||||
// WARNING!
|
||||
//
|
||||
// Despite generate_route_list_inner() using a new leptos_reactive::RuntimeID
|
||||
// each time we call this function, somehow Routes are leaked between different
|
||||
// apps. To avoid that, make sure to put each call in a separate #[test] method.
|
||||
//
|
||||
// TODO: Better isolation for different apps to avoid this issue?
|
||||
fn assert_generated_paths<F, IV>(app: F, expected_sorted_paths: &[&str])
|
||||
where
|
||||
F: Clone + Fn() -> IV + 'static,
|
||||
IV: IntoView + 'static,
|
||||
{
|
||||
let (routes, static_data) = generate_route_list_inner(app);
|
||||
|
||||
let mut paths = routes.iter().map(|route| route.path()).collect_vec();
|
||||
paths.sort();
|
||||
|
||||
assert_eq!(paths, expected_sorted_paths);
|
||||
|
||||
let mut keys = static_data.keys().collect_vec();
|
||||
keys.sort();
|
||||
assert_eq!(paths, keys);
|
||||
|
||||
// integrations can update "path" to be valid for themselves, but
|
||||
// when routes are returned by leptos_router, these are equal:
|
||||
assert!(routes
|
||||
.iter()
|
||||
.all(|route| route.path() == route.leptos_path()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_unique_route_ids() {
|
||||
let branches = get_branches(RedirectApp);
|
||||
assert!(!branches.is_empty());
|
||||
|
||||
assert!(branches
|
||||
.iter()
|
||||
.flat_map(|branch| &branch.routes)
|
||||
.map(|route| route.id)
|
||||
.all_unique());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_unique_route_patterns() {
|
||||
let branches = get_branches(RedirectApp);
|
||||
assert!(!branches.is_empty());
|
||||
|
||||
assert!(branches
|
||||
.iter()
|
||||
.flat_map(|branch| &branch.routes)
|
||||
.map(|route| route.pattern.as_str())
|
||||
.all_unique());
|
||||
}
|
||||
|
||||
fn get_branches<F, IV>(app_fn: F) -> Vec<Branch>
|
||||
where
|
||||
F: Fn() -> IV + Clone + 'static,
|
||||
IV: IntoView + 'static,
|
||||
{
|
||||
let runtime = create_runtime();
|
||||
let additional_context = || ();
|
||||
let branches = super::get_branches(app_fn, additional_context);
|
||||
let branches = branches.0.borrow().clone();
|
||||
runtime.dispose();
|
||||
branches
|
||||
}
|
||||
@@ -31,11 +31,14 @@ impl Matcher {
|
||||
Some((p, s)) => (p, Some(s.to_string())),
|
||||
None => (path, None),
|
||||
};
|
||||
let segments = get_segments(pattern)
|
||||
.iter()
|
||||
.map(|s| s.to_string())
|
||||
let segments = pattern
|
||||
.split('/')
|
||||
.filter(|n| !n.is_empty())
|
||||
.map(|n| n.to_string())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let len = segments.len();
|
||||
|
||||
Self {
|
||||
splat,
|
||||
segments,
|
||||
@@ -45,28 +48,23 @@ impl Matcher {
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
pub fn test(&self, mut location: &str) -> Option<PathMatch> {
|
||||
// URL root paths "/" and "" are equivalent.
|
||||
// Web servers (at least, Axum and Actix-Web) will send us a path of "/"
|
||||
// even if we've routed "". Always treat these as equivalent:
|
||||
if location == "/" && self.len == 0 {
|
||||
location = ""
|
||||
}
|
||||
|
||||
let loc_segments = get_segments(location);
|
||||
pub fn test(&self, location: &str) -> Option<PathMatch> {
|
||||
let loc_segments = location
|
||||
.split('/')
|
||||
.filter(|n| !n.is_empty())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let loc_len = loc_segments.len();
|
||||
let len_diff: i32 = loc_len as i32 - self.len as i32;
|
||||
|
||||
let trailing_slashes =
|
||||
location.chars().rev().take_while(|n| *n == '/').count();
|
||||
let trailing_iter = location.chars().rev().take_while(|n| *n == '/');
|
||||
|
||||
// quick path: not a match if
|
||||
// 1) matcher has add'l segments not found in location
|
||||
// 2) location has add'l segments, there's no splat, and partial matches not allowed
|
||||
if loc_len < self.len
|
||||
|| (len_diff > 0 && self.splat.is_none() && !self.partial)
|
||||
|| (self.splat.is_none() && trailing_slashes > 1)
|
||||
|| (self.splat.is_none() && trailing_iter.clone().count() > 1)
|
||||
{
|
||||
None
|
||||
}
|
||||
@@ -91,12 +89,17 @@ impl Matcher {
|
||||
|
||||
if let Some(splat) = &self.splat {
|
||||
if !splat.is_empty() {
|
||||
let value = if len_diff > 0 {
|
||||
let mut value = if len_diff > 0 {
|
||||
loc_segments[self.len..].join("/")
|
||||
} else {
|
||||
"".into()
|
||||
};
|
||||
|
||||
// add trailing slashes to splat
|
||||
let trailing_slashes =
|
||||
trailing_iter.skip(1).collect::<String>();
|
||||
value.push_str(&trailing_slashes);
|
||||
|
||||
params.insert(splat.into(), value);
|
||||
}
|
||||
}
|
||||
@@ -104,19 +107,4 @@ impl Matcher {
|
||||
Some(PathMatch { path, params })
|
||||
}
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
pub(crate) fn is_wildcard(&self) -> bool {
|
||||
self.splat.is_some()
|
||||
}
|
||||
}
|
||||
|
||||
fn get_segments(pattern: &str) -> Vec<&str> {
|
||||
pattern
|
||||
.split('/')
|
||||
.enumerate()
|
||||
// Only remove a leading slash, not trailing slashes:
|
||||
.skip_while(|(i, part)| *i == 0 && part.is_empty())
|
||||
.map(|(_, part)| part)
|
||||
.collect()
|
||||
}
|
||||
|
||||
@@ -51,14 +51,7 @@ fn has_scheme(path: &str) -> bool {
|
||||
|
||||
#[doc(hidden)]
|
||||
fn normalize(path: &str, omit_slash: bool) -> Cow<'_, str> {
|
||||
let s = path.trim_start_matches('/');
|
||||
let trim_end = s
|
||||
.chars()
|
||||
.rev()
|
||||
.take_while(|c| *c == '/')
|
||||
.count()
|
||||
.saturating_sub(1);
|
||||
let s = &s[0..s.len() - trim_end];
|
||||
let s = path.trim_start_matches('/').trim_end_matches('/');
|
||||
if s.is_empty() || omit_slash || begins_with_query_or_hash(s) {
|
||||
s.into()
|
||||
} else {
|
||||
@@ -77,10 +70,9 @@ fn begins_with_query_or_hash(text: &str) -> bool {
|
||||
}
|
||||
|
||||
fn remove_wildcard(text: &str) -> String {
|
||||
text.rsplit_once('*')
|
||||
.map(|(prefix, _)| prefix)
|
||||
text.split_once('*')
|
||||
.map(|(prefix, _)| prefix.trim_end_matches('/'))
|
||||
.unwrap_or(text)
|
||||
.trim_end_matches('/')
|
||||
.to_string()
|
||||
}
|
||||
|
||||
@@ -91,14 +83,4 @@ mod tests {
|
||||
fn normalize_query_string_with_opening_slash() {
|
||||
assert_eq!(normalize("/?foo=bar", false), "?foo=bar");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn normalize_retain_trailing_slash() {
|
||||
assert_eq!(normalize("foo/bar/", false), "/foo/bar/");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn normalize_dedup_trailing_slashes() {
|
||||
assert_eq!(normalize("foo/bar/////", false), "/foo/bar/");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use crate::{Loader, Method, SsrMode, StaticData, StaticMode, TrailingSlash};
|
||||
use crate::{Loader, Method, SsrMode, StaticData, StaticMode};
|
||||
use leptos::leptos_dom::View;
|
||||
use std::rc::Rc;
|
||||
|
||||
@@ -25,8 +25,6 @@ pub struct RouteDefinition {
|
||||
pub static_mode: Option<StaticMode>,
|
||||
/// The data required to fill any dynamic segments in the path during static rendering.
|
||||
pub static_params: Option<StaticData>,
|
||||
/// How a trailng slash in `path` should be handled.
|
||||
pub trailing_slash: Option<TrailingSlash>,
|
||||
}
|
||||
|
||||
impl core::fmt::Debug for RouteDefinition {
|
||||
@@ -36,7 +34,6 @@ impl core::fmt::Debug for RouteDefinition {
|
||||
.field("children", &self.children)
|
||||
.field("ssr_mode", &self.ssr_mode)
|
||||
.field("static_render", &self.static_mode)
|
||||
.field("trailing_slash", &self.trailing_slash)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,14 +44,5 @@ cfg_if! {
|
||||
assert_eq!(join_paths("/foo", ":bar/baz"), "/foo/:bar/baz");
|
||||
assert_eq!(join_paths("", ":bar/baz"), "/:bar/baz");
|
||||
}
|
||||
|
||||
// Additional tests NOT from Solid Router:
|
||||
#[test]
|
||||
fn join_paths_for_root() {
|
||||
assert_eq!(join_paths("", ""), "");
|
||||
assert_eq!(join_paths("", "/"), "");
|
||||
assert_eq!(join_paths("/", ""), "");
|
||||
assert_eq!(join_paths("/", "/"), "");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,58 +0,0 @@
|
||||
//! Some extra tests for Matcher NOT based on SolidJS's tests cases (as in matcher.rs)
|
||||
|
||||
use leptos_router::{params_map, Matcher};
|
||||
|
||||
#[test]
|
||||
fn trailing_slashes_match_exactly() {
|
||||
let matcher = Matcher::new("/foo/");
|
||||
assert_matches(&matcher, "/foo/");
|
||||
assert_no_match(&matcher, "/foo");
|
||||
|
||||
let matcher = Matcher::new("/foo/bar/");
|
||||
assert_matches(&matcher, "/foo/bar/");
|
||||
assert_no_match(&matcher, "/foo/bar");
|
||||
|
||||
let matcher = Matcher::new("/");
|
||||
assert_matches(&matcher, "/");
|
||||
assert_no_match(&matcher, "");
|
||||
|
||||
let matcher = Matcher::new("");
|
||||
assert_matches(&matcher, "");
|
||||
|
||||
// Despite returning a pattern of "", web servers (known: Actix-Web and Axum)
|
||||
// may send us a path of "/". We should match those at the root:
|
||||
assert_matches(&matcher, "/");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn trailng_slashes_params_match_exactly() {
|
||||
let matcher = Matcher::new("/foo/:bar/");
|
||||
assert_matches(&matcher, "/foo/bar/");
|
||||
assert_matches(&matcher, "/foo/42/");
|
||||
assert_matches(&matcher, "/foo/%20/");
|
||||
|
||||
assert_no_match(&matcher, "/foo/bar");
|
||||
assert_no_match(&matcher, "/foo/42");
|
||||
assert_no_match(&matcher, "/foo/%20");
|
||||
|
||||
let m = matcher.test("/foo/asdf/").unwrap();
|
||||
assert_eq!(m.params, params_map! { "bar" => "asdf" });
|
||||
}
|
||||
|
||||
fn assert_matches(matcher: &Matcher, path: &str) {
|
||||
assert!(
|
||||
matches(matcher, path),
|
||||
"{matcher:?} should match path {path:?}"
|
||||
);
|
||||
}
|
||||
|
||||
fn assert_no_match(matcher: &Matcher, path: &str) {
|
||||
assert!(
|
||||
!matches(matcher, path),
|
||||
"{matcher:?} should NOT match path {path:?}"
|
||||
);
|
||||
}
|
||||
|
||||
fn matches(m: &Matcher, loc: &str) -> bool {
|
||||
m.test(loc).is_some()
|
||||
}
|
||||
@@ -1,7 +1,12 @@
|
||||
[package]
|
||||
name = "server_fn"
|
||||
version = "0.6.0"
|
||||
version = { workspace = true }
|
||||
edition = "2021"
|
||||
authors = ["Greg Johnston"]
|
||||
license = "MIT"
|
||||
repository = "https://github.com/leptos-rs/leptos"
|
||||
description = "RPC for any web framework."
|
||||
readme = "../README.md"
|
||||
|
||||
[dependencies]
|
||||
server_fn_macro_default = { workspace = true}
|
||||
@@ -93,7 +98,10 @@ multipart = ["dep:multer"]
|
||||
url = ["dep:serde_qs"]
|
||||
cbor = ["dep:ciborium"]
|
||||
rkyv = ["dep:rkyv"]
|
||||
default-tls = ["reqwest/default-tls"]
|
||||
rustls = ["reqwest/rustls-tls"]
|
||||
default-tls = ["reqwest?/default-tls"]
|
||||
rustls = ["reqwest?/rustls-tls"]
|
||||
reqwest = ["dep:reqwest"]
|
||||
ssr = ["inventory"]
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
all-features = true
|
||||
|
||||
@@ -34,12 +34,15 @@ use syn::__private::ToTokens;
|
||||
/// relative to the prefix (defaults to the function name followed by unique hash)
|
||||
/// - `input`: the encoding for the arguments (defaults to `PostUrl`)
|
||||
/// - `output`: the encoding for the response (defaults to `Json`)
|
||||
/// - `client`: a custom `Client` implementation that will be used for this server fn
|
||||
/// - `encoding`: (legacy, may be deprecated in future) specifies the encoding, which may be one
|
||||
/// of the following (not case sensitive)
|
||||
/// - `"Url"`: `POST` request with URL-encoded arguments and JSON response
|
||||
/// - `"GetUrl"`: `GET` request with URL-encoded arguments and JSON response
|
||||
/// - `"Cbor"`: `POST` request with CBOR-encoded arguments and response
|
||||
/// - `"GetCbor"`: `GET` request with URL-encoded arguments and CBOR response
|
||||
/// - `req` and `res` specify the HTTP request and response types to be used on the server (these
|
||||
/// should usually only be necessary if you are integrating with a server other than Actix/Axum)
|
||||
/// ```rust,ignore
|
||||
/// #[server(
|
||||
/// name = SomeStructName,
|
||||
@@ -71,6 +74,8 @@ pub fn server(args: proc_macro::TokenStream, s: TokenStream) -> TokenStream {
|
||||
s.into(),
|
||||
Some(syn::parse_quote!(server_fns)),
|
||||
"/api",
|
||||
None,
|
||||
None,
|
||||
) {
|
||||
Err(e) => e.to_compile_error().into(),
|
||||
Ok(s) => s.to_token_stream().into(),
|
||||
|
||||
@@ -33,7 +33,7 @@ pub trait Client<CustErr> {
|
||||
) -> impl Future<Output = Result<Self::Response, ServerFnError<CustErr>>> + Send;
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "browser", doc))]
|
||||
#[cfg(feature = "browser")]
|
||||
/// Implements [`Client`] for a `fetch` request in the browser.
|
||||
pub mod browser {
|
||||
use super::Client;
|
||||
@@ -67,7 +67,7 @@ pub mod browser {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "reqwest", doc))]
|
||||
#[cfg(feature = "reqwest")]
|
||||
/// Implements [`Client`] for a request made by [`reqwest`].
|
||||
pub mod reqwest {
|
||||
use super::Client;
|
||||
|
||||
@@ -11,37 +11,37 @@
|
||||
//! Rather than a limited number of encodings, this crate allows you to define server functions that
|
||||
//! mix and match the input encoding and output encoding. To define a new encoding, you simply implement
|
||||
//! an input combination ([`IntoReq`] and [`FromReq`]) and/or an output encoding ([`IntoRes`] and [`FromRes`]).
|
||||
//! This genuinely is an and/or: while some encodings can be used for both input and output ([`Json`], [`Cbor`], [`Rkyv`]),
|
||||
//! others can only be used for input ([`GetUrl`], [`MultipartData`]) or only output ([`ByteStream`], [`StreamingText`]).
|
||||
//! This genuinely is an and/or: while some encodings can be used for both input and output (`Json`, `Cbor`, `Rkyv`),
|
||||
//! others can only be used for input (`GetUrl`, `MultipartData`).
|
||||
|
||||
#[cfg(feature = "cbor")]
|
||||
mod cbor;
|
||||
#[cfg(any(feature = "cbor", doc))]
|
||||
#[cfg(feature = "cbor")]
|
||||
pub use cbor::*;
|
||||
|
||||
#[cfg(feature = "json")]
|
||||
mod json;
|
||||
#[cfg(any(feature = "json", doc))]
|
||||
#[cfg(feature = "json")]
|
||||
pub use json::*;
|
||||
|
||||
#[cfg(feature = "serde-lite")]
|
||||
mod serde_lite;
|
||||
#[cfg(any(feature = "serde-lite", doc))]
|
||||
#[cfg(feature = "serde-lite")]
|
||||
pub use serde_lite::*;
|
||||
|
||||
#[cfg(feature = "rkyv")]
|
||||
mod rkyv;
|
||||
#[cfg(any(feature = "rkyv", doc))]
|
||||
#[cfg(feature = "rkyv")]
|
||||
pub use rkyv::*;
|
||||
|
||||
#[cfg(feature = "url")]
|
||||
mod url;
|
||||
#[cfg(any(feature = "url", doc))]
|
||||
#[cfg(feature = "url")]
|
||||
pub use url::*;
|
||||
|
||||
#[cfg(feature = "multipart")]
|
||||
mod multipart;
|
||||
#[cfg(any(feature = "multipart", doc))]
|
||||
#[cfg(feature = "multipart")]
|
||||
pub use multipart::*;
|
||||
|
||||
mod stream;
|
||||
@@ -52,7 +52,7 @@ pub use stream::*;
|
||||
|
||||
/// Serializes a data type into an HTTP request, on the client.
|
||||
///
|
||||
/// Implementations use the methods of the [`ClientReq`](crate::ClientReq) trait to
|
||||
/// Implementations use the methods of the [`ClientReq`](crate::request::ClientReq) trait to
|
||||
/// convert data into a request body. They are often quite short, usually consisting
|
||||
/// of just two steps:
|
||||
/// 1. Serializing the data into some [`String`], [`Bytes`](bytes::Bytes), or [`Stream`](futures::Stream).
|
||||
|
||||
@@ -5,11 +5,12 @@ use crate::{
|
||||
response::{ClientRes, Res},
|
||||
};
|
||||
use bytes::Bytes;
|
||||
use futures::StreamExt;
|
||||
use http::Method;
|
||||
use rkyv::{
|
||||
de::deserializers::SharedDeserializeMap, ser::serializers::AllocSerializer,
|
||||
validation::validators::DefaultValidator, Archive, CheckBytes, Deserialize,
|
||||
Serialize,
|
||||
validation::validators::DefaultValidator, AlignedVec, Archive, CheckBytes,
|
||||
Deserialize, Serialize,
|
||||
};
|
||||
|
||||
/// Pass arguments and receive responses using `rkyv` in a `POST` request.
|
||||
@@ -49,8 +50,21 @@ where
|
||||
+ Deserialize<T, SharedDeserializeMap>,
|
||||
{
|
||||
async fn from_req(req: Request) -> Result<Self, ServerFnError<CustErr>> {
|
||||
let body_bytes = req.try_into_bytes().await?;
|
||||
rkyv::from_bytes::<T>(body_bytes.as_ref())
|
||||
let mut aligned = AlignedVec::new();
|
||||
let mut body_stream = Box::pin(req.try_into_stream()?);
|
||||
while let Some(chunk) = body_stream.next().await {
|
||||
match chunk {
|
||||
Err(e) => {
|
||||
return Err(ServerFnError::Deserialization(e.to_string()))
|
||||
}
|
||||
Ok(bytes) => {
|
||||
for byte in bytes {
|
||||
aligned.push(byte);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
rkyv::from_bytes::<T>(aligned.as_ref())
|
||||
.map_err(|e| ServerFnError::Args(e.to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,17 +1,27 @@
|
||||
use super::{Encoding, FromRes};
|
||||
use super::{Encoding, FromReq, FromRes, IntoReq};
|
||||
use crate::{
|
||||
error::{NoCustomError, ServerFnError},
|
||||
request::{ClientReq, Req},
|
||||
response::{ClientRes, Res},
|
||||
IntoRes,
|
||||
};
|
||||
use bytes::Bytes;
|
||||
use futures::{Stream, StreamExt};
|
||||
use http::Method;
|
||||
use std::pin::Pin;
|
||||
use std::{fmt::Debug, pin::Pin};
|
||||
|
||||
/// An encoding that represents a stream of bytes.
|
||||
///
|
||||
/// A server function that uses this as its output encoding should return [`ByteStream`].
|
||||
///
|
||||
/// ## Browser Support for Streaming Input
|
||||
///
|
||||
/// Browser fetch requests do not currently support full request duplexing, which
|
||||
/// means that that they do begin handling responses until the full request has been sent.
|
||||
/// This means that if you use a streaming input encoding, the input stream needs to
|
||||
/// end before the output will begin.
|
||||
///
|
||||
/// Streaming requests are only allowed over HTTP2 or HTTP3.
|
||||
pub struct Streaming;
|
||||
|
||||
impl Encoding for Streaming {
|
||||
@@ -19,29 +29,44 @@ impl Encoding for Streaming {
|
||||
const METHOD: Method = Method::POST;
|
||||
}
|
||||
|
||||
/* impl<CustErr, T, Request> IntoReq<ByteStream, Request, CustErr> for T
|
||||
impl<CustErr, T, Request> IntoReq<Streaming, Request, CustErr> for T
|
||||
where
|
||||
Request: ClientReq<CustErr>,
|
||||
T: Stream<Item = Bytes> + Send,
|
||||
T: Stream<Item = Bytes> + Send + Sync + 'static,
|
||||
{
|
||||
fn into_req(self, path: &str, accepts: &str) -> Result<Request, ServerFnError<CustErr>> {
|
||||
Request::try_new_stream(path, ByteStream::CONTENT_TYPE, self)
|
||||
fn into_req(
|
||||
self,
|
||||
path: &str,
|
||||
accepts: &str,
|
||||
) -> Result<Request, ServerFnError<CustErr>> {
|
||||
Request::try_new_streaming(path, accepts, Streaming::CONTENT_TYPE, self)
|
||||
}
|
||||
} */
|
||||
}
|
||||
|
||||
/* impl<CustErr, T, Request> FromReq<ByteStream, Request, CustErr> for T
|
||||
impl<CustErr, T, Request> FromReq<Streaming, Request, CustErr> for T
|
||||
where
|
||||
Request: Req<CustErr> + Send + 'static,
|
||||
T: Stream<Item = Bytes> + Send,
|
||||
T: From<ByteStream> + 'static,
|
||||
{
|
||||
async fn from_req(req: Request) -> Result<Self, ServerFnError<CustErr>> {
|
||||
req.try_into_stream().await
|
||||
let data = req.try_into_stream()?;
|
||||
let s = ByteStream::new(data);
|
||||
Ok(s.into())
|
||||
}
|
||||
} */
|
||||
}
|
||||
|
||||
/// A stream of bytes.
|
||||
///
|
||||
/// A server function can return this type if its output encoding is [`Streaming`].
|
||||
///
|
||||
/// ## Browser Support for Streaming Input
|
||||
///
|
||||
/// Browser fetch requests do not currently support full request duplexing, which
|
||||
/// means that that they do begin handling responses until the full request has been sent.
|
||||
/// This means that if you use a streaming input encoding, the input stream needs to
|
||||
/// end before the output will begin.
|
||||
///
|
||||
/// Streaming requests are only allowed over HTTP2 or HTTP3.
|
||||
pub struct ByteStream<CustErr = NoCustomError>(
|
||||
Pin<Box<dyn Stream<Item = Result<Bytes, ServerFnError<CustErr>>> + Send>>,
|
||||
);
|
||||
@@ -55,6 +80,24 @@ impl<CustErr> ByteStream<CustErr> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<CustErr> Debug for ByteStream<CustErr> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_tuple("ByteStream").finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl ByteStream {
|
||||
/// Creates a new `ByteStream` from the given stream.
|
||||
pub fn new<T>(
|
||||
value: impl Stream<Item = Result<T, ServerFnError>> + Send + 'static,
|
||||
) -> Self
|
||||
where
|
||||
T: Into<Bytes>,
|
||||
{
|
||||
Self(Box::pin(value.map(|value| value.map(Into::into))))
|
||||
}
|
||||
}
|
||||
|
||||
impl<S, T> From<S> for ByteStream
|
||||
where
|
||||
S: Stream<Item = T> + Send + 'static,
|
||||
@@ -89,6 +132,15 @@ where
|
||||
/// An encoding that represents a stream of text.
|
||||
///
|
||||
/// A server function that uses this as its output encoding should return [`TextStream`].
|
||||
///
|
||||
/// ## Browser Support for Streaming Input
|
||||
///
|
||||
/// Browser fetch requests do not currently support full request duplexing, which
|
||||
/// means that that they do begin handling responses until the full request has been sent.
|
||||
/// This means that if you use a streaming input encoding, the input stream needs to
|
||||
/// end before the output will begin.
|
||||
///
|
||||
/// Streaming requests are only allowed over HTTP2 or HTTP3.
|
||||
pub struct StreamingText;
|
||||
|
||||
impl Encoding for StreamingText {
|
||||
@@ -96,13 +148,37 @@ impl Encoding for StreamingText {
|
||||
const METHOD: Method = Method::POST;
|
||||
}
|
||||
|
||||
/// A stream of bytes.
|
||||
/// A stream of text.
|
||||
///
|
||||
/// A server function can return this type if its output encoding is [`StreamingText`].
|
||||
///
|
||||
/// ## Browser Support for Streaming Input
|
||||
///
|
||||
/// Browser fetch requests do not currently support full request duplexing, which
|
||||
/// means that that they do begin handling responses until the full request has been sent.
|
||||
/// This means that if you use a streaming input encoding, the input stream needs to
|
||||
/// end before the output will begin.
|
||||
///
|
||||
/// Streaming requests are only allowed over HTTP2 or HTTP3.
|
||||
pub struct TextStream<CustErr = NoCustomError>(
|
||||
Pin<Box<dyn Stream<Item = Result<String, ServerFnError<CustErr>>> + Send>>,
|
||||
);
|
||||
|
||||
impl<CustErr> Debug for TextStream<CustErr> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_tuple("TextStream").finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl TextStream {
|
||||
/// Creates a new `ByteStream` from the given stream.
|
||||
pub fn new(
|
||||
value: impl Stream<Item = Result<String, ServerFnError>> + Send + 'static,
|
||||
) -> Self {
|
||||
Self(Box::pin(value.map(|value| value.map(Into::into))))
|
||||
}
|
||||
}
|
||||
|
||||
impl<CustErr> TextStream<CustErr> {
|
||||
/// Consumes the wrapper, returning a stream of text.
|
||||
pub fn into_inner(
|
||||
@@ -122,6 +198,43 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
impl<CustErr, T, Request> IntoReq<StreamingText, Request, CustErr> for T
|
||||
where
|
||||
Request: ClientReq<CustErr>,
|
||||
T: Into<TextStream>,
|
||||
{
|
||||
fn into_req(
|
||||
self,
|
||||
path: &str,
|
||||
accepts: &str,
|
||||
) -> Result<Request, ServerFnError<CustErr>> {
|
||||
let data = self.into();
|
||||
Request::try_new_streaming(
|
||||
path,
|
||||
accepts,
|
||||
Streaming::CONTENT_TYPE,
|
||||
data.0.map(|chunk| chunk.unwrap_or_default().into()),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl<CustErr, T, Request> FromReq<StreamingText, Request, CustErr> for T
|
||||
where
|
||||
Request: Req<CustErr> + Send + 'static,
|
||||
T: From<TextStream> + 'static,
|
||||
{
|
||||
async fn from_req(req: Request) -> Result<Self, ServerFnError<CustErr>> {
|
||||
let data = req.try_into_stream()?;
|
||||
let s = TextStream::new(data.map(|chunk| {
|
||||
chunk.and_then(|bytes| {
|
||||
String::from_utf8(bytes.to_vec())
|
||||
.map_err(|e| ServerFnError::Deserialization(e.to_string()))
|
||||
})
|
||||
}));
|
||||
Ok(s.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl<CustErr, Response> IntoRes<StreamingText, Response, CustErr>
|
||||
for TextStream<CustErr>
|
||||
where
|
||||
|
||||
@@ -79,14 +79,13 @@ impl FromStr for NoCustomError {
|
||||
}
|
||||
}
|
||||
|
||||
/// Wraps some error type, which may implement any of [`Error`], [`Clone`], or
|
||||
/// Wraps some error type, which may implement any of [`Error`](trait@std::error::Error), [`Clone`], or
|
||||
/// [`Display`].
|
||||
#[derive(Debug)]
|
||||
pub struct WrapError<T>(pub T);
|
||||
|
||||
/// This helper macro lets you call the gnarly autoref-specialization call
|
||||
/// without having to worry about things like how many & you need.
|
||||
/// Mostly used when you impl From<ServerFnError> for YourError
|
||||
/// A helper macro to convert a variety of different types into `ServerFnError`.
|
||||
/// This should mostly be used if you are implementing `From<ServerFnError>` for `YourError`.
|
||||
#[macro_export]
|
||||
macro_rules! server_fn_error {
|
||||
() => {{
|
||||
@@ -161,10 +160,10 @@ impl<E> ViaError<E> for WrapError<E> {
|
||||
|
||||
/// Type for errors that can occur when using server functions.
|
||||
///
|
||||
/// Unlike [`ServerFnErrorErr`], this does not implement [`Error`](std::error::Error).
|
||||
/// Unlike [`ServerFnErrorErr`], this does not implement [`Error`](trait@std::error::Error).
|
||||
/// This means that other error types can easily be converted into it using the
|
||||
/// `?` operator.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(tag = "type")]
|
||||
pub enum ServerFnError<E = NoCustomError> {
|
||||
/// A user-defined custom error type, which defaults to [`NoCustomError`].
|
||||
@@ -345,7 +344,7 @@ where
|
||||
///
|
||||
/// [`ServerFnError`] and [`ServerFnErrorErr`] mutually implement [`From`], so
|
||||
/// it is easy to convert between the two types.
|
||||
#[derive(Error, Debug, Clone)]
|
||||
#[derive(Error, Debug, Clone, PartialEq, Eq)]
|
||||
pub enum ServerFnErrorErr<E = NoCustomError> {
|
||||
/// A user-defined custom error type, which defaults to [`NoCustomError`].
|
||||
#[error("internal error: {0}")]
|
||||
|
||||
@@ -32,7 +32,8 @@
|
||||
//! indicate that it should only run on the server (i.e., when you have an `ssr` feature in your
|
||||
//! crate that is enabled).
|
||||
//!
|
||||
//! **Important**: Before calling a server function on a non-web platform, you must set the server URL by calling [`set_server_url`].
|
||||
//! **Important**: Before calling a server function on a non-web platform, you must set the server URL by calling
|
||||
//! [`set_server_url`](crate::client::set_server_url).
|
||||
//!
|
||||
//! ```rust,ignore
|
||||
//! #[server]
|
||||
@@ -148,7 +149,7 @@ pub use xxhash_rust;
|
||||
/// Defines a function that runs only on the server, but can be called from the server or the client.
|
||||
///
|
||||
/// The type for which `ServerFn` is implemented is actually the type of the arguments to the function,
|
||||
/// while the function body itself is implemented in [`run_body`].
|
||||
/// while the function body itself is implemented in [`run_body`](ServerFn::run_body).
|
||||
///
|
||||
/// This means that `Self` here is usually a struct, in which each field is an argument to the function.
|
||||
/// In other words,
|
||||
@@ -405,6 +406,16 @@ impl<Req, Res> ServerFnTraitObj<Req, Res> {
|
||||
pub fn method(&self) -> Method {
|
||||
self.method.clone()
|
||||
}
|
||||
|
||||
/// The handler for this server function.
|
||||
pub fn handler(&self, req: Req) -> impl Future<Output = Res> + Send {
|
||||
(self.handler)(req)
|
||||
}
|
||||
|
||||
/// The set of middleware that should be applied to this function.
|
||||
pub fn middleware(&self) -> MiddlewareSet<Req, Res> {
|
||||
(self.middleware)()
|
||||
}
|
||||
}
|
||||
|
||||
impl<Req, Res> Service<Req, Res> for ServerFnTraitObj<Req, Res>
|
||||
@@ -433,6 +444,17 @@ impl<Req, Res> Clone for ServerFnTraitObj<Req, Res> {
|
||||
type LazyServerFnMap<Req, Res> =
|
||||
Lazy<DashMap<&'static str, ServerFnTraitObj<Req, Res>>>;
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
impl<Req: 'static, Res: 'static> inventory::Collect
|
||||
for ServerFnTraitObj<Req, Res>
|
||||
{
|
||||
#[inline]
|
||||
fn registry() -> &'static inventory::Registry {
|
||||
static REGISTRY: inventory::Registry = inventory::Registry::new();
|
||||
®ISTRY
|
||||
}
|
||||
}
|
||||
|
||||
/// Axum integration.
|
||||
#[cfg(feature = "axum")]
|
||||
pub mod axum {
|
||||
@@ -443,8 +465,6 @@ pub mod axum {
|
||||
use axum::body::Body;
|
||||
use http::{Method, Request, Response, StatusCode};
|
||||
|
||||
inventory::collect!(ServerFnTraitObj<Request<Body>, Response<Body>>);
|
||||
|
||||
static REGISTERED_SERVER_FUNCTIONS: LazyServerFnMap<
|
||||
Request<Body>,
|
||||
Response<Body>,
|
||||
@@ -529,8 +549,6 @@ pub mod actix {
|
||||
#[doc(hidden)]
|
||||
pub use send_wrapper::SendWrapper;
|
||||
|
||||
inventory::collect!(ServerFnTraitObj<ActixRequest, ActixResponse>);
|
||||
|
||||
static REGISTERED_SERVER_FUNCTIONS: LazyServerFnMap<
|
||||
ActixRequest,
|
||||
ActixResponse,
|
||||
|
||||
@@ -2,7 +2,7 @@ use std::{future::Future, pin::Pin};
|
||||
|
||||
/// An abstraction over a middleware layer, which can be used to add additional
|
||||
/// middleware layer to a [`Service`].
|
||||
pub trait Layer<Req, Res>: Send + Sync + 'static {
|
||||
pub trait Layer<Req, Res>: 'static {
|
||||
/// Adds this layer to the inner service.
|
||||
fn layer(&self, inner: BoxedService<Req, Res>) -> BoxedService<Req, Res>;
|
||||
}
|
||||
@@ -104,18 +104,53 @@ mod axum {
|
||||
|
||||
#[cfg(feature = "actix")]
|
||||
mod actix {
|
||||
use super::BoxedService;
|
||||
use crate::{
|
||||
request::actix::ActixRequest,
|
||||
response::{actix::ActixResponse, Res},
|
||||
ServerFnError,
|
||||
};
|
||||
use actix_web::{HttpRequest, HttpResponse};
|
||||
use actix_web::{
|
||||
dev::{Service, Transform},
|
||||
HttpRequest, HttpResponse,
|
||||
};
|
||||
use std::{
|
||||
fmt::{Debug, Display},
|
||||
future::Future,
|
||||
pin::Pin,
|
||||
task::{Context, Poll},
|
||||
};
|
||||
|
||||
impl<T> super::Layer<HttpRequest, HttpResponse> for T
|
||||
where
|
||||
T: Transform<BoxedService<HttpRequest, HttpResponse>, HttpRequest>
|
||||
+ 'static, /* + Send
|
||||
+ Sync
|
||||
+ 'static,*/
|
||||
{
|
||||
fn layer(
|
||||
&self,
|
||||
inner: BoxedService<HttpRequest, HttpResponse>,
|
||||
) -> BoxedService<HttpRequest, HttpResponse> {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> super::Layer<ActixRequest, ActixResponse> for T
|
||||
where
|
||||
T: Transform<BoxedService<ActixRequest, ActixResponse>, HttpRequest>
|
||||
//+ Send
|
||||
//+ Sync
|
||||
+ 'static,
|
||||
{
|
||||
fn layer(
|
||||
&self,
|
||||
inner: BoxedService<ActixRequest, ActixResponse>,
|
||||
) -> BoxedService<ActixRequest, ActixResponse> {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
impl<S> super::Service<HttpRequest, HttpResponse> for S
|
||||
where
|
||||
S: actix_web::dev::Service<HttpRequest, Response = HttpResponse>,
|
||||
@@ -157,4 +192,46 @@ mod actix {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<Req> Service<Req> for BoxedService<HttpRequest, HttpResponse> {
|
||||
type Response = HttpResponse;
|
||||
type Error = ServerFnError;
|
||||
type Future = Pin<
|
||||
Box<
|
||||
dyn Future<Output = Result<Self::Response, Self::Error>> + Send,
|
||||
>,
|
||||
>;
|
||||
|
||||
fn poll_ready(
|
||||
&self,
|
||||
ctx: &mut Context<'_>,
|
||||
) -> Poll<Result<(), Self::Error>> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn call(&self, req: Req) -> Self::Future {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
impl<Req> Service<Req> for BoxedService<ActixRequest, ActixResponse> {
|
||||
type Response = HttpResponse;
|
||||
type Error = ServerFnError;
|
||||
type Future = Pin<
|
||||
Box<
|
||||
dyn Future<Output = Result<Self::Response, Self::Error>> + Send,
|
||||
>,
|
||||
>;
|
||||
|
||||
fn poll_ready(
|
||||
&self,
|
||||
ctx: &mut Context<'_>,
|
||||
) -> Poll<Result<(), Self::Error>> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn call(&self, req: Req) -> Self::Future {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,7 +33,10 @@ impl From<(HttpRequest, Payload)> for ActixRequest {
|
||||
}
|
||||
}
|
||||
|
||||
impl<CustErr> Req<CustErr> for ActixRequest {
|
||||
impl<CustErr> Req<CustErr> for ActixRequest
|
||||
where
|
||||
CustErr: 'static,
|
||||
{
|
||||
fn as_query(&self) -> Option<&str> {
|
||||
self.0 .0.uri().query()
|
||||
}
|
||||
|
||||
@@ -8,7 +8,10 @@ use http::{
|
||||
use http_body_util::BodyExt;
|
||||
use std::borrow::Cow;
|
||||
|
||||
impl<CustErr> Req<CustErr> for Request<Body> {
|
||||
impl<CustErr> Req<CustErr> for Request<Body>
|
||||
where
|
||||
CustErr: 'static,
|
||||
{
|
||||
fn as_query(&self) -> Option<&str> {
|
||||
self.uri().query()
|
||||
}
|
||||
@@ -49,7 +52,7 @@ impl<CustErr> Req<CustErr> for Request<Body> {
|
||||
fn try_into_stream(
|
||||
self,
|
||||
) -> Result<
|
||||
impl Stream<Item = Result<Bytes, ServerFnError>> + Send,
|
||||
impl Stream<Item = Result<Bytes, ServerFnError>> + Send + 'static,
|
||||
ServerFnError<CustErr>,
|
||||
> {
|
||||
Ok(self.into_body().into_data_stream().map(|chunk| {
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
use super::ClientReq;
|
||||
use crate::{client::get_server_url, error::ServerFnError};
|
||||
use bytes::Bytes;
|
||||
use futures::{Stream, StreamExt};
|
||||
pub use gloo_net::http::Request;
|
||||
use js_sys::Uint8Array;
|
||||
use js_sys::{Reflect, Uint8Array};
|
||||
use send_wrapper::SendWrapper;
|
||||
use web_sys::{FormData, UrlSearchParams};
|
||||
use wasm_bindgen::JsValue;
|
||||
use wasm_streams::ReadableStream;
|
||||
use web_sys::{FormData, Headers, RequestInit, UrlSearchParams};
|
||||
|
||||
/// A `fetch` request made in the browser.
|
||||
#[derive(Debug)]
|
||||
@@ -134,4 +137,43 @@ impl<CustErr> ClientReq<CustErr> for BrowserRequest {
|
||||
.map_err(|e| ServerFnError::Request(e.to_string()))?,
|
||||
)))
|
||||
}
|
||||
|
||||
fn try_new_streaming(
|
||||
path: &str,
|
||||
accepts: &str,
|
||||
content_type: &str,
|
||||
body: impl Stream<Item = Bytes> + 'static,
|
||||
) -> Result<Self, ServerFnError<CustErr>> {
|
||||
let req = streaming_request(path, accepts, content_type, body)
|
||||
.map_err(|e| ServerFnError::Request(format!("{e:?}")))?;
|
||||
Ok(Self(SendWrapper::new(req)))
|
||||
}
|
||||
}
|
||||
|
||||
fn streaming_request(
|
||||
path: &str,
|
||||
accepts: &str,
|
||||
content_type: &str,
|
||||
body: impl Stream<Item = Bytes> + 'static,
|
||||
) -> Result<Request, JsValue> {
|
||||
let stream = ReadableStream::from_stream(body.map(|bytes| {
|
||||
let data = Uint8Array::from(bytes.as_ref());
|
||||
let data = JsValue::from(data);
|
||||
Ok(data) as Result<JsValue, JsValue>
|
||||
}))
|
||||
.into_raw();
|
||||
let headers = Headers::new()?;
|
||||
headers.append("Content-Type", content_type)?;
|
||||
headers.append("Accept", accepts)?;
|
||||
let mut init = RequestInit::new();
|
||||
init.headers(&headers).method("POST").body(Some(&stream));
|
||||
|
||||
// Chrome requires setting `duplex: "half"` on streaming requests
|
||||
Reflect::set(
|
||||
&init,
|
||||
&JsValue::from_str("duplex"),
|
||||
&JsValue::from_str("half"),
|
||||
)?;
|
||||
let req = web_sys::Request::new_with_str_and_init(path, &init)?;
|
||||
Ok(Request::from(req))
|
||||
}
|
||||
|
||||
@@ -4,16 +4,16 @@ use futures::Stream;
|
||||
use std::{borrow::Cow, future::Future};
|
||||
|
||||
/// Request types for Actix.
|
||||
#[cfg(any(feature = "actix", doc))]
|
||||
#[cfg(feature = "actix")]
|
||||
pub mod actix;
|
||||
/// Request types for Axum.
|
||||
#[cfg(any(feature = "axum", doc))]
|
||||
#[cfg(feature = "axum")]
|
||||
pub mod axum;
|
||||
/// Request types for the browser.
|
||||
#[cfg(any(feature = "browser", doc))]
|
||||
#[cfg(feature = "browser")]
|
||||
pub mod browser;
|
||||
/// Request types for [`reqwest`].
|
||||
#[cfg(any(feature = "reqwest", doc))]
|
||||
#[cfg(feature = "reqwest")]
|
||||
pub mod reqwest;
|
||||
|
||||
/// Represents a request as made by the client.
|
||||
@@ -62,6 +62,14 @@ where
|
||||
accepts: &str,
|
||||
body: Self::FormData,
|
||||
) -> Result<Self, ServerFnError<CustErr>>;
|
||||
|
||||
/// Attempts to construct a new `POST` request with a streaming body.
|
||||
fn try_new_streaming(
|
||||
path: &str,
|
||||
accepts: &str,
|
||||
content_type: &str,
|
||||
body: impl Stream<Item = Bytes> + Send + 'static,
|
||||
) -> Result<Self, ServerFnError<CustErr>>;
|
||||
}
|
||||
|
||||
/// Represents the request as received by the server.
|
||||
@@ -91,11 +99,11 @@ where
|
||||
self,
|
||||
) -> impl Future<Output = Result<String, ServerFnError<CustErr>>> + Send;
|
||||
|
||||
/// Attempts to convert the body of the request into a string.
|
||||
/// Attempts to convert the body of the request into a stream of bytes.
|
||||
fn try_into_stream(
|
||||
self,
|
||||
) -> Result<
|
||||
impl Stream<Item = Result<Bytes, ServerFnError>> + Send,
|
||||
impl Stream<Item = Result<Bytes, ServerFnError>> + Send + 'static,
|
||||
ServerFnError<CustErr>,
|
||||
>;
|
||||
}
|
||||
@@ -104,7 +112,10 @@ where
|
||||
/// when compiling for the browser.
|
||||
pub struct BrowserMockReq;
|
||||
|
||||
impl<CustErr> Req<CustErr> for BrowserMockReq {
|
||||
impl<CustErr> Req<CustErr> for BrowserMockReq
|
||||
where
|
||||
CustErr: 'static,
|
||||
{
|
||||
fn as_query(&self) -> Option<&str> {
|
||||
unreachable!()
|
||||
}
|
||||
|
||||
@@ -1,8 +1,15 @@
|
||||
use super::ClientReq;
|
||||
use crate::{client::get_server_url, error::ServerFnError};
|
||||
use crate::{
|
||||
client::get_server_url,
|
||||
error::{ServerFnError, ServerFnErrorErr},
|
||||
};
|
||||
use bytes::Bytes;
|
||||
use futures::{Stream, StreamExt};
|
||||
use once_cell::sync::Lazy;
|
||||
use reqwest::header::{ACCEPT, CONTENT_TYPE};
|
||||
use reqwest::{
|
||||
header::{ACCEPT, CONTENT_TYPE},
|
||||
Body,
|
||||
};
|
||||
pub use reqwest::{multipart::Form, Client, Method, Request, Url};
|
||||
|
||||
pub(crate) static CLIENT: Lazy<Client> = Lazy::new(Client::new);
|
||||
@@ -88,4 +95,25 @@ impl<CustErr> ClientReq<CustErr> for Request {
|
||||
.build()
|
||||
.map_err(|e| ServerFnError::Request(e.to_string()))
|
||||
}
|
||||
|
||||
fn try_new_streaming(
|
||||
path: &str,
|
||||
accepts: &str,
|
||||
content_type: &str,
|
||||
body: impl Stream<Item = Bytes> + 'static,
|
||||
) -> Result<Self, ServerFnError<CustErr>> {
|
||||
todo!("Streaming requests are not yet implemented for reqwest.")
|
||||
/* let url = format!("{}{}", get_server_url(), path);
|
||||
let body = Body::wrap_stream(
|
||||
body.map(|chunk| Ok(chunk) as Result<Bytes, ServerFnErrorErr>),
|
||||
);
|
||||
CLIENT
|
||||
.post(url)
|
||||
.header(CONTENT_TYPE, content_type)
|
||||
.header(ACCEPT, accepts)
|
||||
.body(body)
|
||||
.build()
|
||||
.map_err(|e| ServerFnError::Request(e.to_string()))
|
||||
}*/
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
/// Response types for Actix.
|
||||
#[cfg(any(feature = "actix", doc))]
|
||||
#[cfg(feature = "actix")]
|
||||
pub mod actix;
|
||||
/// Response types for the browser.
|
||||
#[cfg(any(feature = "browser", doc))]
|
||||
#[cfg(feature = "browser")]
|
||||
pub mod browser;
|
||||
/// Response types for Axum.
|
||||
#[cfg(any(feature = "axum", doc))]
|
||||
#[cfg(feature = "axum")]
|
||||
pub mod http;
|
||||
/// Response types for [`reqwest`].
|
||||
#[cfg(any(feature = "reqwest", doc))]
|
||||
#[cfg(feature = "reqwest")]
|
||||
pub mod reqwest;
|
||||
|
||||
use crate::error::ServerFnError;
|
||||
@@ -64,7 +64,7 @@ pub trait ClientRes<CustErr> {
|
||||
fn try_into_stream(
|
||||
self,
|
||||
) -> Result<
|
||||
impl Stream<Item = Result<Bytes, ServerFnError>> + Send + 'static,
|
||||
impl Stream<Item = Result<Bytes, ServerFnError>> + Send + Sync + 'static,
|
||||
ServerFnError<CustErr>,
|
||||
>;
|
||||
|
||||
|
||||
@@ -20,3 +20,4 @@ nightly = []
|
||||
ssr = []
|
||||
actix = []
|
||||
axum = []
|
||||
reqwest = []
|
||||
|
||||
@@ -35,6 +35,8 @@ pub fn server_macro_impl(
|
||||
body: TokenStream2,
|
||||
server_fn_path: Option<Path>,
|
||||
default_path: &str,
|
||||
preset_req: Option<Type>,
|
||||
preset_res: Option<Type>,
|
||||
) -> Result<TokenStream2> {
|
||||
let mut body = syn::parse::<ServerFnBody>(body.into())?;
|
||||
|
||||
@@ -65,7 +67,12 @@ pub fn server_macro_impl(
|
||||
output,
|
||||
fn_path,
|
||||
builtin_encoding,
|
||||
req_ty,
|
||||
res_ty,
|
||||
client,
|
||||
custom_wrapper,
|
||||
} = args;
|
||||
_ = custom_wrapper; // TODO: this should be used to enable custom encodings
|
||||
let prefix = prefix.unwrap_or_else(|| Literal::string(default_path));
|
||||
let fn_path = fn_path.unwrap_or_else(|| Literal::string(""));
|
||||
let input_ident = match &input {
|
||||
@@ -351,7 +358,9 @@ pub fn server_macro_impl(
|
||||
Clone, #server_fn_path::rkyv::Archive, #server_fn_path::rkyv::Serialize, #server_fn_path::rkyv::Deserialize
|
||||
},
|
||||
),
|
||||
Some("MultipartFormData") => (PathInfo::None, quote! {}),
|
||||
Some("MultipartFormData")
|
||||
| Some("Streaming")
|
||||
| Some("StreamingText") => (PathInfo::None, quote! {}),
|
||||
Some("SerdeLite") => (
|
||||
PathInfo::Serde,
|
||||
quote! {
|
||||
@@ -378,9 +387,16 @@ pub fn server_macro_impl(
|
||||
PathInfo::None => quote! {},
|
||||
};
|
||||
|
||||
// TODO reqwest
|
||||
let client = quote! {
|
||||
#server_fn_path::client::browser::BrowserClient
|
||||
let client = if let Some(client) = client {
|
||||
client.to_token_stream()
|
||||
} else if cfg!(feature = "reqwest") {
|
||||
quote! {
|
||||
#server_fn_path::client::reqwest::ReqwestClient
|
||||
}
|
||||
} else {
|
||||
quote! {
|
||||
#server_fn_path::client::browser::BrowserClient
|
||||
}
|
||||
};
|
||||
|
||||
let req = if !cfg!(feature = "ssr") {
|
||||
@@ -395,11 +411,16 @@ pub fn server_macro_impl(
|
||||
quote! {
|
||||
#server_fn_path::request::actix::ActixRequest
|
||||
}
|
||||
} else if let Some(req_ty) = req_ty {
|
||||
req_ty.to_token_stream()
|
||||
} else if let Some(req_ty) = preset_req {
|
||||
req_ty.to_token_stream()
|
||||
} else {
|
||||
return Err(syn::Error::new(
|
||||
Span::call_site(),
|
||||
"If the `ssr` feature is enabled, either the `actix` or `axum` \
|
||||
features should also be enabled.",
|
||||
features should also be enabled, or the `req = ` argument should \
|
||||
be provided to specify the request type.",
|
||||
));
|
||||
};
|
||||
let res = if !cfg!(feature = "ssr") {
|
||||
@@ -414,11 +435,16 @@ pub fn server_macro_impl(
|
||||
quote! {
|
||||
#server_fn_path::response::actix::ActixResponse
|
||||
}
|
||||
} else if let Some(res_ty) = res_ty {
|
||||
res_ty.to_token_stream()
|
||||
} else if let Some(res_ty) = preset_res {
|
||||
res_ty.to_token_stream()
|
||||
} else {
|
||||
return Err(syn::Error::new(
|
||||
Span::call_site(),
|
||||
"If the `ssr` feature is enabled, either the `actix` or `axum` \
|
||||
features should also be enabled.",
|
||||
features should also be enabled, or the `res = ` argument should \
|
||||
be provided to specify the response type.",
|
||||
));
|
||||
};
|
||||
|
||||
@@ -612,6 +638,10 @@ struct ServerFnArgs {
|
||||
input: Option<Type>,
|
||||
output: Option<Type>,
|
||||
fn_path: Option<Literal>,
|
||||
req_ty: Option<Type>,
|
||||
res_ty: Option<Type>,
|
||||
client: Option<Type>,
|
||||
custom_wrapper: Option<Type>,
|
||||
builtin_encoding: bool,
|
||||
}
|
||||
|
||||
@@ -626,6 +656,10 @@ impl Parse for ServerFnArgs {
|
||||
// new arguments: can only be keyed by name
|
||||
let mut input: Option<Type> = None;
|
||||
let mut output: Option<Type> = None;
|
||||
let mut req_ty: Option<Type> = None;
|
||||
let mut res_ty: Option<Type> = None;
|
||||
let mut client: Option<Type> = None;
|
||||
let mut custom_wrapper: Option<Type> = None;
|
||||
|
||||
let mut use_key_and_value = false;
|
||||
let mut arg_pos = 0;
|
||||
@@ -701,6 +735,38 @@ impl Parse for ServerFnArgs {
|
||||
));
|
||||
}
|
||||
output = Some(stream.parse()?);
|
||||
} else if key == "req" {
|
||||
if req_ty.is_some() {
|
||||
return Err(syn::Error::new(
|
||||
key.span(),
|
||||
"keyword argument repeated: `req`",
|
||||
));
|
||||
}
|
||||
req_ty = Some(stream.parse()?);
|
||||
} else if key == "res" {
|
||||
if res_ty.is_some() {
|
||||
return Err(syn::Error::new(
|
||||
key.span(),
|
||||
"keyword argument repeated: `res`",
|
||||
));
|
||||
}
|
||||
res_ty = Some(stream.parse()?);
|
||||
} else if key == "client" {
|
||||
if client.is_some() {
|
||||
return Err(syn::Error::new(
|
||||
key.span(),
|
||||
"keyword argument repeated: `client`",
|
||||
));
|
||||
}
|
||||
client = Some(stream.parse()?);
|
||||
} else if key == "custom" {
|
||||
if custom_wrapper.is_some() {
|
||||
return Err(syn::Error::new(
|
||||
key.span(),
|
||||
"keyword argument repeated: `custom`",
|
||||
));
|
||||
}
|
||||
custom_wrapper = Some(stream.parse()?);
|
||||
} else {
|
||||
return Err(lookahead.error());
|
||||
}
|
||||
@@ -786,13 +852,16 @@ impl Parse for ServerFnArgs {
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
_attrs,
|
||||
struct_name,
|
||||
prefix,
|
||||
input,
|
||||
output,
|
||||
fn_path,
|
||||
builtin_encoding,
|
||||
req_ty,
|
||||
res_ty,
|
||||
client,
|
||||
custom_wrapper,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user