Compare commits

..

98 Commits

Author SHA1 Message Date
Greg Johnston
55ef106c50 initial work on Actix middleware support 2024-01-19 16:35:51 -05:00
Greg Johnston
26d9d75cf2 cargo fmt 2024-01-19 15:56:57 -05:00
Greg Johnston
46e7abf9ba allow custom req/res/client types 2024-01-19 15:48:14 -05:00
Greg Johnston
1b1e02729e clean up examples 2024-01-19 15:17:17 -05:00
Greg Johnston
fdd576535a clean up examples 2024-01-19 15:14:39 -05:00
Greg Johnston
2a9e502893 fix rkyv deserialization 2024-01-19 15:03:21 -05:00
Greg Johnston
a519859a66 Revert "use &[u8] instead of Bytes for requests"
This reverts commit e179db1d42.
2024-01-19 14:37:03 -05:00
Greg Johnston
25120c0e9f fix streaming requests and clarify in docs 2024-01-19 14:17:26 -05:00
Greg Johnston
94cb4c0ec3 remove pavex work (now in pavex branch) 2024-01-19 14:17:26 -05:00
Greg Johnston
f9cd8539e4 add missing PartialEq/Eq implementations on ServerFnError (closes #2198) 2024-01-19 14:17:26 -05:00
Greg Johnston
14072457d0 clean up docs (closes #2197) 2024-01-19 14:17:26 -05:00
Greg Johnston
e179db1d42 use &[u8] instead of Bytes for requests 2024-01-19 14:17:26 -05:00
Greg Johnston
2fa60103b4 share inventory collect across types 2024-01-19 14:17:26 -05:00
Greg Johnston
a3a15f244d expose all fields of ServerFnTraitObj via methods 2024-01-19 14:17:26 -05:00
Greg Johnston
0df5dfeaf8 weak dependency on Cargo.toml 2024-01-19 14:17:26 -05:00
Greg Johnston
3f22906053 fix warning 2024-01-19 14:17:26 -05:00
Greg Johnston
33ad30515d serde-lite support should be enabled directly on server_fn 2024-01-19 14:17:26 -05:00
Greg Johnston
c5bab09423 partial support for streaming requests (doesn't actually work in the browser) 2024-01-19 14:17:26 -05:00
Greg Johnston
320179bc04 remove misleading warning 2024-01-19 14:17:26 -05:00
Greg Johnston
5065bed594 example of middleware that can run before and/or after server fn 2024-01-19 14:17:26 -05:00
Greg Johnston
22b4537f27 fix version numbers 2024-01-19 14:17:26 -05:00
Greg Johnston
8d23d5136a add package metadata 2024-01-19 14:17:25 -05:00
Greg Johnston
c7fac64054 fix merge error 2024-01-19 14:17:25 -05:00
Greg Johnston
047235e7c1 clippy 2024-01-19 14:17:25 -05:00
Greg Johnston
7a086ad159 update version number 2024-01-19 14:17:25 -05:00
Greg Johnston
bb923b3f9b erroneous hyphen 2024-01-19 14:16:59 -05:00
Greg Johnston
6a8c26a820 streaming example with filesystem watcher 2024-01-19 14:16:59 -05:00
Greg Johnston
21f8085851 add streaming/file watcher example 2024-01-19 14:16:59 -05:00
Greg Johnston
9a5a102ce3 add middleware to kitchen-sink example 2024-01-19 14:16:59 -05:00
Greg Johnston
4d602c21f8 example with custom errors 2024-01-19 14:16:59 -05:00
Greg Johnston
7d114c7414 file upload example 2024-01-19 14:16:58 -05:00
Greg Johnston
1f017a2ade hm custom encodings have orphan rule issues 2024-01-19 14:16:58 -05:00
Greg Johnston
35e8e74dcf get rkyv working and work on custom encoding example 2024-01-19 14:16:58 -05:00
Markus Kohlhase
4366d786ac Update login example (CSR only) (#2155) 2024-01-19 14:16:58 -05:00
Ari Seyhun
1777a4057a fix!: remove clone in Cow<'static, str> IntoView impl (#1946) 2024-01-19 14:16:58 -05:00
Greg Johnston
0571ebbc36 working on example 2024-01-19 14:16:58 -05:00
Greg Johnston
06c478b7cb feature-gate the form redirect stuff, and clear old errors from query 2024-01-19 14:16:58 -05:00
Greg Johnston
90ba3529e9 working on Axum version 2024-01-19 14:16:58 -05:00
Greg Johnston
13a2691806 working on server fn example 2024-01-19 14:16:58 -05:00
Greg Johnston
1ad7ee8a03 generalize error redirect behavior across integrations 2024-01-19 14:16:58 -05:00
Greg Johnston
88fee243a8 support setting server URL on either platform 2024-01-19 14:16:58 -05:00
Greg Johnston
5e08253521 get both client and server side working 2024-01-19 14:16:58 -05:00
Greg Johnston
cc6f65cd83 initial version of server action error handling without JS 2024-01-19 14:16:58 -05:00
Greg Johnston
9488114801 docs 2024-01-19 14:16:58 -05:00
Greg Johnston
b0cdeab906 remove old code 2024-01-19 14:16:58 -05:00
Greg Johnston
def4be80b2 docs 2024-01-19 14:16:58 -05:00
Greg Johnston
15b04a8a85 more docs 2024-01-19 14:16:58 -05:00
Greg Johnston
0a9cdba22e getting started on docs 2024-01-19 14:16:58 -05:00
Greg Johnston
1d1de4ac38 remove cfg-if from all examples 2024-01-19 14:16:58 -05:00
Greg Johnston
31b2b9e94c remove explicit handle_server_fns in most cases because it's now included in .leptos_routes() 2024-01-19 14:16:58 -05:00
Greg Johnston
8f07818687 nicer formatting, remove cfg-if 2024-01-19 14:16:58 -05:00
Greg Johnston
a5cbfa0aad remove viz integration (see #2177) 2024-01-19 14:16:58 -05:00
Greg Johnston
6c8e704fb3 smh 2024-01-19 14:16:58 -05:00
Greg Johnston
81fb5160e5 missing makefiles 2024-01-19 14:16:58 -05:00
Greg Johnston
2af0d3d781 update session_auth_axum 2024-01-19 14:16:58 -05:00
Greg Johnston
7f532cda70 update todo_app_sqlite_csrs 2024-01-19 14:16:58 -05:00
Greg Johnston
c7941f7639 clippy 2024-01-19 14:16:58 -05:00
Greg Johnston
61148026d1 allow type paths for input/output, and properly namespace built-in encodings 2024-01-19 14:16:58 -05:00
Greg Johnston
738eeefe73 chore: clear warnings 2024-01-19 14:16:18 -05:00
Greg Johnston
be084a5d1d remove list of magic identifiers, use rust-analyzer to help with imports instead 2024-01-19 14:16:18 -05:00
Greg Johnston
f5c007df7b use server fns directly in ActionForm and MultiActionForm 2024-01-19 14:16:18 -05:00
Rakshith Ravi
a1bd84f3dc feat: add serde-lite codec for server functions (#2168) 2024-01-19 14:16:18 -05:00
Rakshith Ravi
f6ce82c9d1 Fixed tests for server_fn (#2167)
* Fixed server_fn tests

* Changed type_name to TypeId

* Fixed handling of leading slashes for server_fn endpoint
2024-01-19 14:16:18 -05:00
Greg Johnston
853c080707 add missing server fn registration 2024-01-19 14:16:18 -05:00
Greg Johnston
f6b95e40f4 make sure endpoint names begin with a / 2024-01-19 14:16:18 -05:00
Greg Johnston
db1497b9c2 set version, input, etc. correctly 2024-01-19 14:16:18 -05:00
Greg Johnston
f53ac1a4ae remove unused var 2024-01-19 14:16:18 -05:00
Greg Johnston
5e6f4403ca set up redirects in Actix 2024-01-19 14:16:18 -05:00
Greg Johnston
4e3f1c834c handle client-side and server-side redirects correctly (in Axum) 2024-01-19 14:16:18 -05:00
Greg Johnston
566df034ff actually use server functions in ActionForm 2024-01-19 14:16:17 -05:00
Greg Johnston
fd97e2e027 Restore the previous full functionality of Form 2024-01-19 14:16:17 -05:00
Greg Johnston
c8fbee18c8 finished Actix support? 2024-01-19 14:16:17 -05:00
Greg Johnston
e1a9856ca9 more Actix work 2024-01-19 14:16:17 -05:00
Greg Johnston
60efaefff4 start Actix work 2024-01-19 14:16:17 -05:00
Greg Johnston
db4158f5c3 clear up warnings 2024-01-19 14:16:17 -05:00
Greg Johnston
af62d2e900 automatically include server function handler in .leptos_router() 2024-01-19 14:16:17 -05:00
Greg Johnston
c3e3ce7878 changes to get todo_app_sqlite_axum example working 2024-01-19 14:16:17 -05:00
Greg Johnston
dec17fc65b fix server actions and server multi actions 2024-01-19 14:16:03 -05:00
Greg Johnston
2dbc5899f3 cargo fmt 2024-01-19 14:16:03 -05:00
Greg Johnston
dd368a845c @ealmloff changes to reexport actix/axum 2024-01-19 14:16:03 -05:00
Greg Johnston
9c258219dd fix Actix implementation with middleware 2024-01-19 14:16:03 -05:00
Greg Johnston
6a1685936b fix rkyv 2024-01-19 14:16:03 -05:00
Greg Johnston
7d45e6bb13 clean up my mistake 2024-01-19 14:16:03 -05:00
Greg Johnston
8fae76828e FromStr-based lightweight ServerFnError deserialization 2024-01-19 14:16:03 -05:00
Greg Johnston
d5b9e84f36 properly gate inventory 2024-01-19 14:16:03 -05:00
benwis
197edebd51 Made some progress, started work on pavex integration as well 2024-01-19 14:16:03 -05:00
benwis
2a5c855595 It starts to compile! 2024-01-19 14:16:03 -05:00
benwis
c9627bfeb4 Setup folder structure as before. Got a cyclical dependency though 2024-01-19 14:16:03 -05:00
benwis
c7422cd96e First commit, checkpoint for cyclical dependency error 2024-01-19 14:15:51 -05:00
Daniel Santana
cadd217078 Update integration with support for axum 0.7 (#2082)
* chore: update to axum 0.7

Removed http, since it's included in axum, and replaced hyper by http-body-util, which is a smaller.

* chore: update samples to work with nre axum

Missing sessions_axum_auth, pending PR merge.

* chore: all dependencies update to axum 0.7

* chore: cargo fmt

* chore: fix doctests

* chore: Fix example that in reality doesn't use axum.

Fixed anyway.

* chore: more examples support for axum 0.7

* Small tweak
2024-01-19 14:13:55 -05:00
Greg Johnston
0c4cf5471d v0.5.7 2024-01-19 13:03:44 -05:00
Greg Johnston
dd0c349554 examples: update axum-session because old version was yanked (#2205) 2024-01-19 12:54:08 -05:00
Greg Johnston
dd5a0ae094 Merge pull request #2203 from leptos-rs/2201
fix: routing regressions caused by `trailing_slash` support
2024-01-19 12:14:20 -05:00
Greg Johnston
5cacb57283 chore: new clippy warnings 2024-01-19 11:14:36 -05:00
Greg Johnston
b356d3cd28 ci: add regression test for #2190 2024-01-19 10:29:30 -05:00
Greg Johnston
ae1de88916 Revert "Better handling for trailing slashes. (#2154) (#2172)"
This reverts commit 1eaf886481.
2024-01-19 10:27:38 -05:00
Greg Johnston
67dd188358 ci: add regression test for matching main page correctly in router example 2024-01-19 10:26:58 -05:00
Joseph Cruz
1d4772251a fix: ci stopped detecting leptos or example changes (#2194) 2024-01-17 18:58:21 -05:00
100 changed files with 737 additions and 4433 deletions

View File

@@ -24,7 +24,7 @@ jobs:
uses: tj-actions/changed-files@v41
with:
files: |
examples
examples/**
!examples/cargo-make
!examples/gtk
!examples/Makefile.toml

View File

@@ -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 }}'

View File

@@ -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" }

View File

@@ -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());

View File

@@ -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();

View File

@@ -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};

View File

@@ -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
}

View File

@@ -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

View File

@@ -1,3 +0,0 @@
/target
.env
.direnv

View File

@@ -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

View File

@@ -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).

View File

@@ -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
}

View File

@@ -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";
};
});
}

View File

@@ -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"]

View File

@@ -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>
}
}
/>
}
}

View File

@@ -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>
}
}

View File

@@ -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]

View File

@@ -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);
}

View File

@@ -1,4 +0,0 @@
body {
font-family: sans-serif;
text-align: center;
}

View File

@@ -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

View File

@@ -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(())
}

View File

@@ -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));
}

View File

@@ -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
}
}

View File

@@ -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}"),
)),
}
}

View File

@@ -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)
}

View File

@@ -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;

View File

@@ -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()
}

View File

@@ -1,3 +0,0 @@
pub mod greet;
pub mod status;

View File

@@ -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
}

View File

@@ -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(),
}
}

View File

@@ -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()
}

View File

@@ -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"

View File

@@ -1,3 +0,0 @@
server:
ip: "0.0.0.0"
port: 8000

View File

@@ -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

View File

@@ -1,3 +0,0 @@
server:
ip: "0.0.0.0"
port: 8000

View File

@@ -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

View File

@@ -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(())
}

View File

@@ -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
)),
}
}
}

View File

@@ -1,2 +0,0 @@
pub mod configuration;
pub mod telemetry;

View File

@@ -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)
}

View File

@@ -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"
);
}

View File

@@ -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.")
}
}

View File

@@ -1,4 +0,0 @@
mod greet;
mod helpers;
mod ping;

View File

@@ -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());
}

View File

@@ -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" }

View File

@@ -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: [],
)

View File

@@ -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)
}
}
}

View File

@@ -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" },

View File

@@ -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"]

View File

@@ -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())

View File

@@ -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)
}
}
}
}

View File

@@ -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 }

View File

@@ -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)

View File

@@ -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>
// Well load the home page with out-of-order streaming and <Suspense/>

View File

@@ -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>
// Well load the home page with out-of-order streaming and <Suspense/>

View File

@@ -1,3 +1,5 @@
#[cfg(feature = "ssr")]
pub mod middleware;
pub mod todo;
#[cfg(feature = "hydrate")]

View 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)
})
}
}

View File

@@ -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::*;

View File

@@ -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)

View File

@@ -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);
}

View File

@@ -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,

View File

@@ -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"]

View File

@@ -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

View File

@@ -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"]

View File

@@ -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;

View File

@@ -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;

View File

@@ -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(),

View File

@@ -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) };
}
}
}

View File

@@ -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")]

View File

@@ -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)]

View File

@@ -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 {

View File

@@ -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].

View File

@@ -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,
}
}

View File

@@ -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,
}
}
}

View File

@@ -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/>
}
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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()
}

View File

@@ -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/");
}
}

View File

@@ -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()
}
}

View File

@@ -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("/", "/"), "");
}
}
}

View File

@@ -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()
}

View File

@@ -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

View File

@@ -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(),

View File

@@ -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;

View File

@@ -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).

View File

@@ -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()))
}
}

View File

@@ -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

View File

@@ -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}")]

View File

@@ -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();
&REGISTRY
}
}
/// 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,

View File

@@ -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!()
}
}
}

View File

@@ -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()
}

View File

@@ -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| {

View File

@@ -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))
}

View File

@@ -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!()
}

View File

@@ -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()))
}*/
}
}

View File

@@ -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>,
>;

View File

@@ -20,3 +20,4 @@ nightly = []
ssr = []
actix = []
axum = []
reqwest = []

View File

@@ -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,
})
}
}