mirror of
https://github.com/leptos-rs/leptos
synced 2024-11-10 06:44:17 +00:00
Made some progress, started work on pavex integration as well
This commit is contained in:
parent
2a5c855595
commit
197edebd51
46 changed files with 2315 additions and 389 deletions
3
examples/pavex_demo/.gitignore
vendored
Normal file
3
examples/pavex_demo/.gitignore
vendored
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
/target
|
||||||
|
.env
|
||||||
|
.direnv
|
92
examples/pavex_demo/Cargo.toml
Normal file
92
examples/pavex_demo/Cargo.toml
Normal file
|
@ -0,0 +1,92 @@
|
||||||
|
[workspace]
|
||||||
|
members = ["todo_app_sqlite_pavex", "todo_app_sqlite_pavex_server_sdk", "todo_app_sqlite_pavex_server", "leptos_app"]
|
||||||
|
# By setting `todo_app_sqlite_pavex_server` as the default member, `cargo run` will default to running the server binary
|
||||||
|
# when executed from the root of the workspace.
|
||||||
|
# Otherwise, you would have to use `cargo run --bin api` to run the server binary.
|
||||||
|
default-members = ["todo_app_sqlite_pavex_server"]
|
||||||
|
resolver = "2"
|
||||||
|
|
||||||
|
# need to be applied only to wasm build
|
||||||
|
[profile.wasm_release]
|
||||||
|
codegen-units = 1
|
||||||
|
lto = true
|
||||||
|
opt-level = 'z'
|
||||||
|
|
||||||
|
[workspace.dependencies]
|
||||||
|
leptos = { version = "0.5", features = ["nightly"] }
|
||||||
|
leptos_meta = { version = "0.5", features = ["nightly"] }
|
||||||
|
leptos_router = { version = "0.5", features = ["nightly"] }
|
||||||
|
leptos_pavex = { version = "0.5" }
|
||||||
|
cfg_if = "1"
|
||||||
|
thiserror = "1"
|
||||||
|
|
||||||
|
# See https://github.com/akesson/cargo-leptos for documentation of all the parameters.
|
||||||
|
|
||||||
|
# A leptos project defines which workspace members
|
||||||
|
# that are used together frontend (lib) & server (bin)
|
||||||
|
[[workspace.metadata.leptos]]
|
||||||
|
# this name is used for the wasm, js and css file names
|
||||||
|
name = "start-pavex-workspace"
|
||||||
|
|
||||||
|
# the package in the workspace that contains the server binary (binary crate)
|
||||||
|
bin-package = "server"
|
||||||
|
|
||||||
|
# the package in the workspace that contains the frontend wasm binary (library crate)
|
||||||
|
lib-package = "leptos_frontend"
|
||||||
|
|
||||||
|
# The site root folder is where cargo-leptos generate all output. WARNING: all content of this folder will be erased on a rebuild. Use it in your server setup.
|
||||||
|
site-root = "target/site"
|
||||||
|
|
||||||
|
# The site-root relative folder where all compiled output (JS, WASM and CSS) is written
|
||||||
|
# Defaults to pkg
|
||||||
|
site-pkg-dir = "pkg"
|
||||||
|
|
||||||
|
# [Optional] The source CSS file. If it ends with .sass or .scss then it will be compiled by dart-sass into CSS. The CSS is optimized by Lightning CSS before being written to <site-root>/<site-pkg>/app.css
|
||||||
|
style-file = "style/main.scss"
|
||||||
|
|
||||||
|
# Assets source dir. All files found here will be copied and synchronized to site-root.
|
||||||
|
# The assets-dir cannot have a sub directory with the same name/path as site-pkg-dir.
|
||||||
|
#
|
||||||
|
# Optional. Env: LEPTOS_ASSETS_DIR.
|
||||||
|
assets-dir = "public"
|
||||||
|
|
||||||
|
# The IP and port (ex: 127.0.0.1:3000) where the server serves the content. Use it in your server setup.
|
||||||
|
site-addr = "127.0.0.1:3000"
|
||||||
|
|
||||||
|
# The port to use for automatic reload monitoring
|
||||||
|
reload-port = 3001
|
||||||
|
|
||||||
|
# [Optional] Command to use when running end2end tests. It will run in the end2end dir.
|
||||||
|
# [Windows] for non-WSL use "npx.cmd playwright test"
|
||||||
|
# This binary name can be checked in Powershell with Get-Command npx
|
||||||
|
end2end-cmd = "npx playwright test"
|
||||||
|
end2end-dir = "end2end"
|
||||||
|
|
||||||
|
# The browserlist query used for optimizing the CSS.
|
||||||
|
browserquery = "defaults"
|
||||||
|
|
||||||
|
# Set by cargo-leptos watch when building with that tool. Controls whether autoreload JS will be included in the head
|
||||||
|
watch = false
|
||||||
|
|
||||||
|
# The environment Leptos will run in, usually either "DEV" or "PROD"
|
||||||
|
env = "DEV"
|
||||||
|
|
||||||
|
# The features to use when compiling the bin target
|
||||||
|
#
|
||||||
|
# Optional. Can be over-ridden with the command line parameter --bin-features
|
||||||
|
bin-features = []
|
||||||
|
|
||||||
|
# If the --no-default-features flag should be used when compiling the bin target
|
||||||
|
#
|
||||||
|
# Optional. Defaults to false.
|
||||||
|
bin-default-features = false
|
||||||
|
|
||||||
|
# The features to use when compiling the lib target
|
||||||
|
#
|
||||||
|
# Optional. Can be over-ridden with the command line parameter --lib-features
|
||||||
|
lib-features = []
|
||||||
|
|
||||||
|
# If the --no-default-features flag should be used when compiling the lib target
|
||||||
|
#
|
||||||
|
# Optional. Defaults to false.
|
||||||
|
lib-default-features = false
|
71
examples/pavex_demo/README.md
Normal file
71
examples/pavex_demo/README.md
Normal file
|
@ -0,0 +1,71 @@
|
||||||
|
# todo_app_sqlite_pavex
|
||||||
|
|
||||||
|
# Getting started
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
- Rust (see [here](https://www.rust-lang.org/tools/install) for instructions)
|
||||||
|
- `cargo-px`:
|
||||||
|
```bash
|
||||||
|
cargo install --locked cargo-px --version="~0.1"
|
||||||
|
```
|
||||||
|
- [Pavex](https://pavex.dev)
|
||||||
|
|
||||||
|
## Useful commands
|
||||||
|
|
||||||
|
`todo_app_sqlite_pavex` is built using the [Pavex](https://pavex.dev) web framework, which relies on code generation.
|
||||||
|
You need to use the `cargo px` command instead of `cargo`: it ensures that the
|
||||||
|
`todo_app_sqlite_pavex_server_sdk` crate is correctly regenerated when the
|
||||||
|
application blueprint changes.
|
||||||
|
|
||||||
|
`cargo px` is a wrapper around `cargo` that will automatically regenerate the
|
||||||
|
server SDK when needed. Check out its [documentation](https://github.com/LukeMathWalker/cargo-px)
|
||||||
|
for more details.
|
||||||
|
|
||||||
|
### Build
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo px build
|
||||||
|
```
|
||||||
|
|
||||||
|
### Run
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo px run
|
||||||
|
```
|
||||||
|
|
||||||
|
### Test
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo px test
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
All configurable parameters are listed in `todo_app_sqlite_pavex/src/configuration.rs`.
|
||||||
|
|
||||||
|
Configuration values are loaded from two sources:
|
||||||
|
|
||||||
|
- Configuration files
|
||||||
|
- Environment variables
|
||||||
|
|
||||||
|
Environment variables take precedence over configuration files.
|
||||||
|
|
||||||
|
All configuration files are in the `todo_app_sqlite_pavex_server/configuration` folder.
|
||||||
|
The application can be run in three different profiles: `dev`, `test` and `prod`.
|
||||||
|
The settings that you want to share across all profiles should be placed in `todo_app_sqlite_pavex_server/configuration/base.yml`.
|
||||||
|
Profile-specific configuration files can be then used
|
||||||
|
to override or supply additional values on top of the default settings (e.g. `todo_app_sqlite_pavex_server/configuration/dev.yml`).
|
||||||
|
|
||||||
|
You can specify the app profile that you want to use by setting the `APP_PROFILE` environment variable; e.g.:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
APP_PROFILE=prod cargo px run
|
||||||
|
```
|
||||||
|
|
||||||
|
for running the application with the `prod` profile.
|
||||||
|
|
||||||
|
By default, the `dev` profile is used since `APP_PROFILE` is set to `dev` in the `.env` file at the root of the project.
|
||||||
|
The `.env` file should not be committed to version control: it is meant to be used for local development only,
|
||||||
|
so that each developer can specify their own environment variables for secret values (e.g. database credentials)
|
||||||
|
that shouldn't be stored in configuration files (given their sensitive nature).
|
119
examples/pavex_demo/flake.lock
Normal file
119
examples/pavex_demo/flake.lock
Normal file
|
@ -0,0 +1,119 @@
|
||||||
|
{
|
||||||
|
"nodes": {
|
||||||
|
"cargo-pavex-git": {
|
||||||
|
"flake": false,
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1703610192,
|
||||||
|
"narHash": "sha256-+oM6VGRRt/DQdhEFWJFIpKfY29w72V0vRpud8NsOI7c=",
|
||||||
|
"owner": "LukeMathWalker",
|
||||||
|
"repo": "pavex",
|
||||||
|
"rev": "e302f99e3641a55fe5624ba6c8154ce64e732a89",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "LukeMathWalker",
|
||||||
|
"repo": "pavex",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"cargo-px-git": {
|
||||||
|
"flake": false,
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1702137928,
|
||||||
|
"narHash": "sha256-FbwHEOQnIYKhxp4Ne9XBIUJXu1o+ak6y9MhzRenIW40=",
|
||||||
|
"owner": "LukeMathWalker",
|
||||||
|
"repo": "cargo-px",
|
||||||
|
"rev": "d1bb9075c4993130f31f31c95642567a2255bd8e",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "LukeMathWalker",
|
||||||
|
"repo": "cargo-px",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"flake-utils": {
|
||||||
|
"inputs": {
|
||||||
|
"systems": "systems"
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1701680307,
|
||||||
|
"narHash": "sha256-kAuep2h5ajznlPMD9rnQyffWG8EM/C73lejGofXvdM8=",
|
||||||
|
"owner": "numtide",
|
||||||
|
"repo": "flake-utils",
|
||||||
|
"rev": "4022d587cbbfd70fe950c1e2083a02621806a725",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "numtide",
|
||||||
|
"repo": "flake-utils",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nixpkgs": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1703499205,
|
||||||
|
"narHash": "sha256-lF9rK5mSUfIZJgZxC3ge40tp1gmyyOXZ+lRY3P8bfbg=",
|
||||||
|
"owner": "NixOS",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"rev": "e1fa12d4f6c6fe19ccb59cac54b5b3f25e160870",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "NixOS",
|
||||||
|
"ref": "nixpkgs-unstable",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": {
|
||||||
|
"inputs": {
|
||||||
|
"cargo-pavex-git": "cargo-pavex-git",
|
||||||
|
"cargo-px-git": "cargo-px-git",
|
||||||
|
"flake-utils": "flake-utils",
|
||||||
|
"nixpkgs": "nixpkgs",
|
||||||
|
"rust-overlay": "rust-overlay"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"rust-overlay": {
|
||||||
|
"inputs": {
|
||||||
|
"flake-utils": [
|
||||||
|
"flake-utils"
|
||||||
|
],
|
||||||
|
"nixpkgs": [
|
||||||
|
"nixpkgs"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1703643208,
|
||||||
|
"narHash": "sha256-UL4KO8JxnD5rOycwHqBAf84lExF1/VnYMDC7b/wpPDU=",
|
||||||
|
"owner": "oxalica",
|
||||||
|
"repo": "rust-overlay",
|
||||||
|
"rev": "ce117f3e0de8262be8cd324ee6357775228687cf",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "oxalica",
|
||||||
|
"repo": "rust-overlay",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"systems": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1681028828,
|
||||||
|
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||||
|
"owner": "nix-systems",
|
||||||
|
"repo": "default",
|
||||||
|
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "nix-systems",
|
||||||
|
"repo": "default",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": "root",
|
||||||
|
"version": 7
|
||||||
|
}
|
129
examples/pavex_demo/flake.nix
Normal file
129
examples/pavex_demo/flake.nix
Normal file
|
@ -0,0 +1,129 @@
|
||||||
|
{
|
||||||
|
description = "Build Pavex tools";
|
||||||
|
|
||||||
|
inputs = {
|
||||||
|
nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
|
||||||
|
|
||||||
|
flake-utils.url = "github:numtide/flake-utils";
|
||||||
|
|
||||||
|
cargo-px-git = {
|
||||||
|
url = "github:/LukeMathWalker/cargo-px";
|
||||||
|
flake = false;
|
||||||
|
};
|
||||||
|
cargo-pavex-git = {
|
||||||
|
url = "github:LukeMathWalker/pavex";
|
||||||
|
flake = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
rust-overlay = {
|
||||||
|
url = "github:oxalica/rust-overlay";
|
||||||
|
inputs = {
|
||||||
|
nixpkgs.follows = "nixpkgs";
|
||||||
|
flake-utils.follows = "flake-utils";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
outputs = { self, nixpkgs, flake-utils, rust-overlay, ... } @inputs:
|
||||||
|
flake-utils.lib.eachDefaultSystem
|
||||||
|
(system:
|
||||||
|
let
|
||||||
|
pkgs = import nixpkgs {
|
||||||
|
inherit system;
|
||||||
|
overlays = [ (import rust-overlay) ];
|
||||||
|
};
|
||||||
|
inherit (pkgs) lib;
|
||||||
|
rustTarget = pkgs.rust-bin.selectLatestNightlyWith( toolchain: toolchain.default.override {
|
||||||
|
extensions = [ "rust-src" "rust-analyzer" "rustc-codegen-cranelift-preview" "rust-docs-json"];
|
||||||
|
targets = [ "wasm32-unknown-unknown" ];
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
cargo-pavex_cli-git = pkgs.rustPlatform.buildRustPackage rec {
|
||||||
|
pname = "cargo-pavex-cli";
|
||||||
|
version = "0.2.22";
|
||||||
|
#buildFeatures = ["no_downloads"]; # cargo-leptos will try to download Ruby and other things without this feature
|
||||||
|
|
||||||
|
src = inputs.cargo-pavex-git;
|
||||||
|
sourceRoot = "source/libs";
|
||||||
|
cargoLock = {
|
||||||
|
lockFile = inputs.cargo-pavex-git + "/libs/Cargo.lock";
|
||||||
|
outputHashes = {
|
||||||
|
"matchit-0.7.3" = "sha256-1bhbWvLlDb6/UJ4j2FqoG7j3DD1dTOLl6RaiY9kasmQ=";
|
||||||
|
#"pavex-0.1.0" = "sha256-NC7T1pcXJiWPtAWeiMUNzf2MUsYaRYxjLIL9fCqhExo=";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
#buildAndTestSubdir = "libs";
|
||||||
|
cargoSha256 = "";
|
||||||
|
nativeBuildInputs = [pkgs.pkg-config pkgs.openssl pkgs.git];
|
||||||
|
|
||||||
|
buildInputs = with pkgs;
|
||||||
|
[openssl pkg-config git]
|
||||||
|
++ lib.optionals stdenv.isDarwin [
|
||||||
|
Security
|
||||||
|
];
|
||||||
|
|
||||||
|
doCheck = false; # integration tests depend on changing cargo config
|
||||||
|
|
||||||
|
meta = with lib; {
|
||||||
|
description = "An easy-to-use Rust framework for building robust and performant APIs";
|
||||||
|
homepage = "https://github.com/LukeMatthewWalker/pavex";
|
||||||
|
changelog = "https://github.com/LukeMatthewWalker/pavex/blob/v${version}/CHANGELOG.md";
|
||||||
|
license = with licenses; [mit];
|
||||||
|
maintainers = with maintainers; [benwis];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
cargo-px-git = pkgs.rustPlatform.buildRustPackage rec {
|
||||||
|
pname = "cargo-px";
|
||||||
|
version = "0.2.22";
|
||||||
|
#buildFeatures = ["no_downloads"]; # cargo-leptos will try to download Ruby and other things without this feature
|
||||||
|
|
||||||
|
src = inputs.cargo-px-git;
|
||||||
|
|
||||||
|
cargoSha256 ="sha256-+pyeqh0IoZ1JMgbhWxhEJw1MPgG7XeocVrqJoSNjgDA=";
|
||||||
|
|
||||||
|
nativeBuildInputs = [pkgs.pkg-config pkgs.openssl pkgs.git];
|
||||||
|
|
||||||
|
buildInputs = with pkgs;
|
||||||
|
[openssl pkg-config git]
|
||||||
|
++ lib.optionals stdenv.isDarwin [
|
||||||
|
Security
|
||||||
|
];
|
||||||
|
|
||||||
|
doCheck = false; # integration tests depend on changing cargo config
|
||||||
|
|
||||||
|
meta = with lib; {
|
||||||
|
description = "A cargo subcommand that extends cargo's capabilities when it comes to code generation.";
|
||||||
|
homepage = "https://github.com/LukeMatthewWalker/cargo-px";
|
||||||
|
changelog = "https://github.com/LukeMatthewWalker/cargo-px/blob/v${version}/CHANGELOG.md";
|
||||||
|
license = with licenses; [mit];
|
||||||
|
maintainers = with maintainers; [benwis];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
in
|
||||||
|
{
|
||||||
|
|
||||||
|
devShells.default = pkgs.mkShell {
|
||||||
|
|
||||||
|
# Extra inputs can be added here
|
||||||
|
nativeBuildInputs = with pkgs; [
|
||||||
|
#rustTarget
|
||||||
|
rustup
|
||||||
|
openssl
|
||||||
|
pkg-config
|
||||||
|
clang
|
||||||
|
tailwindcss
|
||||||
|
mold-wrapped
|
||||||
|
cargo-px-git
|
||||||
|
cargo-pavex_cli-git
|
||||||
|
];
|
||||||
|
#RUST_SRC_PATH = "${rustTarget}/lib/rustlib/src/rust/library";
|
||||||
|
MOLD_PATH = "${pkgs.mold-wrapped}/bin/mold";
|
||||||
|
|
||||||
|
shellHook = ''
|
||||||
|
sed -i -e '/rustflags = \["-C", "link-arg=-fuse-ld=/ s|ld=.*|ld=${pkgs.mold-wrapped}/bin/mold"]|' .cargo/config.toml
|
||||||
|
'';
|
||||||
|
LIBCLANG_PATH = "${pkgs.llvmPackages.libclang.lib}/lib";
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
21
examples/pavex_demo/leptos_app/Cargo.toml
Normal file
21
examples/pavex_demo/leptos_app/Cargo.toml
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
[package]
|
||||||
|
name = "leptos_app"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
leptos.workspace = true
|
||||||
|
leptos_meta.workspace = true
|
||||||
|
leptos_router.workspace = true
|
||||||
|
leptos_pavex = { workspace = true, optional = true }
|
||||||
|
|
||||||
|
#http.workspace = true
|
||||||
|
cfg_if.workspace = true
|
||||||
|
thiserror.workspace = true
|
||||||
|
|
||||||
|
[features]
|
||||||
|
default = []
|
||||||
|
hydrate = ["leptos/hydrate", "leptos_meta/hydrate", "leptos_router/hydrate"]
|
||||||
|
ssr = ["leptos/ssr", "leptos_meta/ssr", "leptos_router/ssr", "dep:leptos_pavex"]
|
73
examples/pavex_demo/leptos_app/src/error_template.rs
Normal file
73
examples/pavex_demo/leptos_app/src/error_template.rs
Normal file
|
@ -0,0 +1,73 @@
|
||||||
|
use cfg_if::cfg_if;
|
||||||
|
use http::status::StatusCode;
|
||||||
|
use leptos::*;
|
||||||
|
#[cfg(feature = "ssr")]
|
||||||
|
use leptos_axum::ResponseOptions;
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Error)]
|
||||||
|
pub enum AppError {
|
||||||
|
#[error("Not Found")]
|
||||||
|
NotFound,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AppError {
|
||||||
|
pub fn status_code(&self) -> StatusCode {
|
||||||
|
match self {
|
||||||
|
AppError::NotFound => StatusCode::NOT_FOUND,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A basic function to display errors served by the error boundaries.
|
||||||
|
// Feel free to do more complicated things here than just displaying the error.
|
||||||
|
#[component]
|
||||||
|
pub fn ErrorTemplate(
|
||||||
|
#[prop(optional)] outside_errors: Option<Errors>,
|
||||||
|
#[prop(optional)] errors: Option<RwSignal<Errors>>,
|
||||||
|
) -> impl IntoView {
|
||||||
|
let errors = match outside_errors {
|
||||||
|
Some(e) => create_rw_signal(e),
|
||||||
|
None => match errors {
|
||||||
|
Some(e) => e,
|
||||||
|
None => panic!("No Errors found and we expected errors!"),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
// Get Errors from Signal
|
||||||
|
let errors = errors.get_untracked();
|
||||||
|
|
||||||
|
// Downcast lets us take a type that implements `std::error::Error`
|
||||||
|
let errors: Vec<AppError> = errors
|
||||||
|
.into_iter()
|
||||||
|
.filter_map(|(_k, v)| v.downcast_ref::<AppError>().cloned())
|
||||||
|
.collect();
|
||||||
|
println!("Errors: {errors:#?}");
|
||||||
|
|
||||||
|
// Only the response code for the first error is actually sent from the server
|
||||||
|
// this may be customized by the specific application
|
||||||
|
cfg_if! { if #[cfg(feature="ssr")] {
|
||||||
|
let response = use_context::<ResponseOptions>();
|
||||||
|
if let Some(response) = response {
|
||||||
|
response.set_status(errors[0].status_code());
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
|
||||||
|
view! {
|
||||||
|
<h1>{if errors.len() > 1 { "Errors" } else { "Error" }}</h1>
|
||||||
|
<For
|
||||||
|
// a function that returns the items we're iterating over; a signal is fine
|
||||||
|
each=move || { errors.clone().into_iter().enumerate() }
|
||||||
|
// a unique key for each item as a reference
|
||||||
|
key=|(index, _error)| *index
|
||||||
|
// renders each item to a view
|
||||||
|
children=move |error| {
|
||||||
|
let error_string = error.1.to_string();
|
||||||
|
let error_code = error.1.status_code();
|
||||||
|
view! {
|
||||||
|
<h2>{error_code.to_string()}</h2>
|
||||||
|
<p>"Error: " {error_string}</p>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
}
|
45
examples/pavex_demo/leptos_app/src/lib.rs
Normal file
45
examples/pavex_demo/leptos_app/src/lib.rs
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
use crate::error_template::{AppError, ErrorTemplate};
|
||||||
|
use leptos::*;
|
||||||
|
use leptos_meta::*;
|
||||||
|
use leptos_router::*;
|
||||||
|
|
||||||
|
pub mod error_template;
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
pub fn App() -> impl IntoView {
|
||||||
|
// Provides context that manages stylesheets, titles, meta tags, etc.
|
||||||
|
provide_meta_context();
|
||||||
|
|
||||||
|
view! {
|
||||||
|
<Stylesheet id="leptos" href="/pkg/start-axum-workspace.css"/>
|
||||||
|
|
||||||
|
// sets the document title
|
||||||
|
<Title text="Welcome to Leptos"/>
|
||||||
|
|
||||||
|
// content for this welcome page
|
||||||
|
<Router fallback=|| {
|
||||||
|
let mut outside_errors = Errors::default();
|
||||||
|
outside_errors.insert_with_default_key(AppError::NotFound);
|
||||||
|
view! { <ErrorTemplate outside_errors/> }.into_view()
|
||||||
|
}>
|
||||||
|
<main>
|
||||||
|
<Routes>
|
||||||
|
<Route path="" view=HomePage/>
|
||||||
|
</Routes>
|
||||||
|
</main>
|
||||||
|
</Router>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Renders the home page of your application.
|
||||||
|
#[component]
|
||||||
|
fn HomePage() -> impl IntoView {
|
||||||
|
// Creates a reactive value to update the button
|
||||||
|
let (count, set_count) = create_signal(0);
|
||||||
|
let on_click = move |_| set_count.update(|count| *count += 1);
|
||||||
|
|
||||||
|
view! {
|
||||||
|
<h1>"Welcome to Leptos on Pavex!"</h1>
|
||||||
|
<button on:click=on_click>"Click Me: " {count}</button>
|
||||||
|
}
|
||||||
|
}
|
8
examples/pavex_demo/leptos_front/Cargo.toml
Normal file
8
examples/pavex_demo/leptos_front/Cargo.toml
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
[package]
|
||||||
|
name = "leptos_front"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
[dependencies]
|
13
examples/pavex_demo/leptos_front/src/lib.rs
Normal file
13
examples/pavex_demo/leptos_front/src/lib.rs
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
use leptos::*;
|
||||||
|
use leptos_app::*;
|
||||||
|
use wasm_bindgen::prelude::wasm_bindgen;
|
||||||
|
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub fn hydrate() {
|
||||||
|
// initializes logging using the `log` crate
|
||||||
|
_ = console_log::init_with_level(log::Level::Debug);
|
||||||
|
console_error_panic_hook::set_once();
|
||||||
|
|
||||||
|
leptos::mount_to_body(App);
|
||||||
|
}
|
||||||
|
|
4
examples/pavex_demo/style/main.scss
Normal file
4
examples/pavex_demo/style/main.scss
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
body {
|
||||||
|
font-family: sans-serif;
|
||||||
|
text-align: center;
|
||||||
|
}
|
22
examples/pavex_demo/todo_app_sqlite_pavex/Cargo.toml
Normal file
22
examples/pavex_demo/todo_app_sqlite_pavex/Cargo.toml
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
[package]
|
||||||
|
name = "todo_app_sqlite_pavex"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
path = "src/bin/bp.rs"
|
||||||
|
name = "bp"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
cargo_px_env = "0.1"
|
||||||
|
pavex = { git = "https://github.com/LukeMathWalker/pavex", branch = "main" }
|
||||||
|
pavex_cli_client = { git = "https://github.com/LukeMathWalker/pavex", branch = "main" }
|
||||||
|
tracing = "0.1"
|
||||||
|
|
||||||
|
# Configuration
|
||||||
|
serde = { version = "1", features = ["derive"] }
|
||||||
|
serde-aux = "4"
|
||||||
|
|
||||||
|
# Leptos
|
||||||
|
leptos_pavex.workspace = true
|
||||||
|
|
17
examples/pavex_demo/todo_app_sqlite_pavex/src/bin/bp.rs
Normal file
17
examples/pavex_demo/todo_app_sqlite_pavex/src/bin/bp.rs
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
use cargo_px_env::generated_pkg_manifest_path;
|
||||||
|
use todo_app_sqlite_pavex::blueprint;
|
||||||
|
use pavex_cli_client::Client;
|
||||||
|
use std::error::Error;
|
||||||
|
|
||||||
|
/// Generate the `todo_app_sqlite_pavex_server_sdk` crate using Pavex's CLI.
|
||||||
|
///
|
||||||
|
/// Pavex will automatically wire all our routes, constructors and error handlers
|
||||||
|
/// into the a "server SDK" that can be used by the final API server binary to launch
|
||||||
|
/// the application.
|
||||||
|
fn main() -> Result<(), Box<dyn Error>> {
|
||||||
|
let generated_dir = generated_pkg_manifest_path()?.parent().unwrap().into();
|
||||||
|
Client::new()
|
||||||
|
.generate(blueprint(), generated_dir)
|
||||||
|
.execute()?;
|
||||||
|
Ok(())
|
||||||
|
}
|
98
examples/pavex_demo/todo_app_sqlite_pavex/src/blueprint.rs
Normal file
98
examples/pavex_demo/todo_app_sqlite_pavex/src/blueprint.rs
Normal file
|
@ -0,0 +1,98 @@
|
||||||
|
use leptos_pavex::{LeptosOptions, RouteListing};
|
||||||
|
use pavex::{
|
||||||
|
blueprint::{
|
||||||
|
constructor::{CloningStrategy, Lifecycle},
|
||||||
|
router::{ANY, GET},
|
||||||
|
Blueprint,
|
||||||
|
},
|
||||||
|
f,
|
||||||
|
};
|
||||||
|
/// The main blueprint, containing all the routes, constructors and error handlers
|
||||||
|
/// required by our API.
|
||||||
|
pub fn blueprint() -> Blueprint {
|
||||||
|
let mut bp = Blueprint::new();
|
||||||
|
register_common_constructors(&mut bp);
|
||||||
|
|
||||||
|
bp.constructor(
|
||||||
|
f!(crate::user_agent::UserAgent::extract),
|
||||||
|
Lifecycle::RequestScoped,
|
||||||
|
)
|
||||||
|
.error_handler(f!(crate::user_agent::invalid_user_agent));
|
||||||
|
|
||||||
|
add_telemetry_middleware(&mut bp);
|
||||||
|
|
||||||
|
bp.route(GET, "/test/ping", f!(crate::routes::status::ping));
|
||||||
|
bp.route(GET, "/test/greet/:name", f!(crate::routes::greet::greet));
|
||||||
|
// Handle all /api requests as those are Leptos server fns
|
||||||
|
bp.route(ANY, "/api/*fn_name", f!(leptos_pavex::handle_server_fns));
|
||||||
|
bp.route(ANY, "/");
|
||||||
|
bp.fallback(f!(file_handler));
|
||||||
|
bp
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Common constructors used by all routes.
|
||||||
|
fn register_common_constructors(bp: &mut Blueprint) {
|
||||||
|
// Configuration Options
|
||||||
|
bp.constructor(
|
||||||
|
f!(crate::leptos::get_cargo_leptos_conf(), Lifecycle::Singleton),
|
||||||
|
Lifecycle::Singleton,
|
||||||
|
);
|
||||||
|
// List of Routes
|
||||||
|
bp.constructor(
|
||||||
|
f!(crate::leptos::get_app_route_listing(), Lifecycle::Singleton),
|
||||||
|
Lifecycle::Singleton,
|
||||||
|
);
|
||||||
|
|
||||||
|
bp.constructor(
|
||||||
|
f!(leptos_pavex::PavexRequest::extract),
|
||||||
|
LifeCycle::RequestScoped,
|
||||||
|
);
|
||||||
|
// Query parameters
|
||||||
|
bp.constructor(
|
||||||
|
f!(pavex::request::query::QueryParams::extract),
|
||||||
|
Lifecycle::RequestScoped,
|
||||||
|
)
|
||||||
|
.error_handler(f!(
|
||||||
|
pavex::request::query::errors::ExtractQueryParamsError::into_response
|
||||||
|
));
|
||||||
|
|
||||||
|
// Route parameters
|
||||||
|
bp.constructor(
|
||||||
|
f!(pavex::request::route::RouteParams::extract),
|
||||||
|
Lifecycle::RequestScoped,
|
||||||
|
)
|
||||||
|
.error_handler(f!(
|
||||||
|
pavex::request::route::errors::ExtractRouteParamsError::into_response
|
||||||
|
));
|
||||||
|
|
||||||
|
// Json body
|
||||||
|
bp.constructor(
|
||||||
|
f!(pavex::request::body::JsonBody::extract),
|
||||||
|
Lifecycle::RequestScoped,
|
||||||
|
)
|
||||||
|
.error_handler(f!(
|
||||||
|
pavex::request::body::errors::ExtractJsonBodyError::into_response
|
||||||
|
));
|
||||||
|
bp.constructor(
|
||||||
|
f!(pavex::request::body::BufferedBody::extract),
|
||||||
|
Lifecycle::RequestScoped,
|
||||||
|
)
|
||||||
|
.error_handler(f!(
|
||||||
|
pavex::request::body::errors::ExtractBufferedBodyError::into_response
|
||||||
|
));
|
||||||
|
bp.constructor(
|
||||||
|
f!(<pavex::request::body::BodySizeLimit as std::default::Default>::default),
|
||||||
|
Lifecycle::RequestScoped,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add the telemetry middleware, as well as the constructors of its dependencies.
|
||||||
|
fn add_telemetry_middleware(bp: &mut Blueprint) {
|
||||||
|
bp.constructor(
|
||||||
|
f!(crate::telemetry::RootSpan::new),
|
||||||
|
Lifecycle::RequestScoped,
|
||||||
|
)
|
||||||
|
.cloning(CloningStrategy::CloneIfNecessary);
|
||||||
|
|
||||||
|
bp.wrap(f!(crate::telemetry::logger));
|
||||||
|
}
|
|
@ -0,0 +1,32 @@
|
||||||
|
use pavex::server::IncomingStream;
|
||||||
|
use serde_aux::field_attributes::deserialize_number_from_string;
|
||||||
|
use std::net::SocketAddr;
|
||||||
|
|
||||||
|
#[derive(serde::Deserialize)]
|
||||||
|
/// The top-level configuration, holding all the values required
|
||||||
|
/// to configure the entire application.
|
||||||
|
pub struct Config {
|
||||||
|
pub server: ServerConfig,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(serde::Deserialize, Clone)]
|
||||||
|
/// Configuration for the HTTP server used to expose our API
|
||||||
|
/// to users.
|
||||||
|
pub struct ServerConfig {
|
||||||
|
/// The port that the server must listen on.
|
||||||
|
#[serde(deserialize_with = "deserialize_number_from_string")]
|
||||||
|
pub port: u16,
|
||||||
|
/// The network interface that the server must be bound to.
|
||||||
|
///
|
||||||
|
/// E.g. `0.0.0.0` for listening to incoming requests from
|
||||||
|
/// all sources.
|
||||||
|
pub ip: std::net::IpAddr,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ServerConfig {
|
||||||
|
/// Bind a TCP listener according to the specified parameters.
|
||||||
|
pub async fn listener(&self) -> Result<IncomingStream, std::io::Error> {
|
||||||
|
let addr = SocketAddr::new(self.ip, self.port);
|
||||||
|
IncomingStream::bind(addr).await
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,45 @@
|
||||||
|
use app::error_template::AppError;
|
||||||
|
use app::error_template::ErrorTemplate;
|
||||||
|
use app::App;
|
||||||
|
use axum::response::Response as AxumResponse;
|
||||||
|
use axum::{
|
||||||
|
body::{boxed, Body, BoxBody},
|
||||||
|
extract::State,
|
||||||
|
http::{Request, Response, StatusCode, Uri},
|
||||||
|
response::IntoResponse,
|
||||||
|
};
|
||||||
|
use leptos::*;
|
||||||
|
use tower::ServiceExt;
|
||||||
|
use tower_http::services::ServeDir;
|
||||||
|
|
||||||
|
pub async fn file_and_error_handler(
|
||||||
|
uri: Uri,
|
||||||
|
State(options): State<LeptosOptions>,
|
||||||
|
req: Request<Body>,
|
||||||
|
) -> AxumResponse {
|
||||||
|
let root = options.site_root.clone();
|
||||||
|
let res = get_static_file(uri.clone(), &root).await.unwrap();
|
||||||
|
|
||||||
|
if res.status() == StatusCode::OK {
|
||||||
|
res.into_response()
|
||||||
|
} else {
|
||||||
|
let handler = leptos_axum::render_app_to_stream(options.to_owned(), move || view! { <App/> });
|
||||||
|
handler(req).await.into_response()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_static_file(uri: Uri, root: &str) -> Result<Response<BoxBody>, (StatusCode, String)> {
|
||||||
|
let req = Request::builder()
|
||||||
|
.uri(uri.clone())
|
||||||
|
.body(Body::empty())
|
||||||
|
.unwrap();
|
||||||
|
// `ServeDir` implements `tower::Service` so we can call it with `tower::ServiceExt::oneshot`
|
||||||
|
// This path is relative to the cargo root
|
||||||
|
match ServeDir::new(root).oneshot(req).await {
|
||||||
|
Ok(res) => Ok(res.map(boxed)),
|
||||||
|
Err(err) => Err((
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
format!("Something went wrong: {err}"),
|
||||||
|
)),
|
||||||
|
}
|
||||||
|
}
|
19
examples/pavex_demo/todo_app_sqlite_pavex/src/leptos.rs
Normal file
19
examples/pavex_demo/todo_app_sqlite_pavex/src/leptos.rs
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
use leptos::{get_configuration, leptos_config::ConfFile};
|
||||||
|
use leptos_pavex::generate_route_list;
|
||||||
|
use leptos_router::RouteListing;
|
||||||
|
use pavex::{
|
||||||
|
http::header::{ToStrError, USER_AGENT},
|
||||||
|
request::RequestHead,
|
||||||
|
response::Response,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Easier to do this to avoid having to register things with Blueprints
|
||||||
|
/// Provide LeptosOptions via env vars provided by cargo-leptos or the user
|
||||||
|
pub fn get_cargo_leptos_conf() -> ConfFile {
|
||||||
|
get_configuration(None)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Generate all possible non server fn routes for our app
|
||||||
|
pub fn get_app_route_listing() -> Vec<RouteListing> {
|
||||||
|
generate_route_list(TodoApp)
|
||||||
|
}
|
7
examples/pavex_demo/todo_app_sqlite_pavex/src/lib.rs
Normal file
7
examples/pavex_demo/todo_app_sqlite_pavex/src/lib.rs
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
mod blueprint;
|
||||||
|
pub mod configuration;
|
||||||
|
pub mod leptos;
|
||||||
|
pub mod routes;
|
||||||
|
pub mod telemetry;
|
||||||
|
pub mod user_agent;
|
||||||
|
pub use blueprint::blueprint;
|
|
@ -0,0 +1,21 @@
|
||||||
|
use crate::user_agent::UserAgent;
|
||||||
|
use pavex::{request::route::RouteParams, response::Response};
|
||||||
|
|
||||||
|
#[RouteParams]
|
||||||
|
pub struct GreetParams {
|
||||||
|
pub name: String,
|
||||||
|
}
|
||||||
|
pub fn greet(
|
||||||
|
params: RouteParams<GreetParams>,
|
||||||
|
user_agent: UserAgent,
|
||||||
|
) -> Response {
|
||||||
|
if let UserAgent::Unknown = user_agent {
|
||||||
|
return Response::unauthorized()
|
||||||
|
.set_typed_body("You must provide a `User-Agent` header")
|
||||||
|
.box_body();
|
||||||
|
}
|
||||||
|
let GreetParams { name } = params.0;
|
||||||
|
Response::ok()
|
||||||
|
.set_typed_body(format!("Hello, {name}!"))
|
||||||
|
.box_body()
|
||||||
|
}
|
|
@ -0,0 +1,3 @@
|
||||||
|
pub mod greet;
|
||||||
|
pub mod status;
|
||||||
|
|
|
@ -0,0 +1,7 @@
|
||||||
|
use pavex::http::StatusCode;
|
||||||
|
|
||||||
|
/// Respond with a `200 OK` status code to indicate that the server is alive
|
||||||
|
/// and ready to accept new requests.
|
||||||
|
pub fn ping() -> StatusCode {
|
||||||
|
StatusCode::OK
|
||||||
|
}
|
84
examples/pavex_demo/todo_app_sqlite_pavex/src/telemetry.rs
Normal file
84
examples/pavex_demo/todo_app_sqlite_pavex/src/telemetry.rs
Normal file
|
@ -0,0 +1,84 @@
|
||||||
|
use pavex::request::route::MatchedRouteTemplate;
|
||||||
|
use pavex::http::Version;
|
||||||
|
use pavex::middleware::Next;
|
||||||
|
use pavex::request::RequestHead;
|
||||||
|
use pavex::response::Response;
|
||||||
|
use std::borrow::Cow;
|
||||||
|
use std::future::IntoFuture;
|
||||||
|
use tracing::Instrument;
|
||||||
|
|
||||||
|
/// A logging middleware that wraps the request pipeline in the root span.
|
||||||
|
/// It takes care to record key information about the request and the response.
|
||||||
|
pub async fn logger<T>(next: Next<T>, root_span: RootSpan) -> Response
|
||||||
|
where
|
||||||
|
T: IntoFuture<Output = Response>,
|
||||||
|
{
|
||||||
|
let response = next
|
||||||
|
.into_future()
|
||||||
|
.instrument(root_span.clone().into_inner())
|
||||||
|
.await;
|
||||||
|
root_span.record_response_data(&response);
|
||||||
|
response
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A root span is the top-level *logical* span for an incoming request.
|
||||||
|
///
|
||||||
|
/// It is not necessarily the top-level *physical* span, as it may be a child of
|
||||||
|
/// another span (e.g. a span representing the underlying HTTP connection).
|
||||||
|
///
|
||||||
|
/// We use the root span to attach as much information as possible about the
|
||||||
|
/// incoming request, and to record the final outcome of the request (success or
|
||||||
|
/// failure).
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct RootSpan(tracing::Span);
|
||||||
|
|
||||||
|
impl RootSpan {
|
||||||
|
/// Create a new root span for the given request.
|
||||||
|
///
|
||||||
|
/// We follow OpenTelemetry's HTTP semantic conventions as closely as
|
||||||
|
/// possible for field naming.
|
||||||
|
pub fn new(request_head: &RequestHead, matched_route: MatchedRouteTemplate) -> Self {
|
||||||
|
let user_agent = request_head
|
||||||
|
.headers
|
||||||
|
.get("User-Agent")
|
||||||
|
.map(|h| h.to_str().unwrap_or_default())
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
let span = tracing::info_span!(
|
||||||
|
"HTTP request",
|
||||||
|
http.method = %request_head.method,
|
||||||
|
http.flavor = %http_flavor(request_head.version),
|
||||||
|
user_agent.original = %user_agent,
|
||||||
|
http.response.status_code = tracing::field::Empty,
|
||||||
|
http.route = %matched_route,
|
||||||
|
http.target = %request_head.uri.path_and_query().map(|p| p.as_str()).unwrap_or(""),
|
||||||
|
);
|
||||||
|
Self(span)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn record_response_data(&self, response: &Response) {
|
||||||
|
self.0
|
||||||
|
.record("http.response.status_code", &response.status().as_u16());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get a reference to the underlying [`tracing::Span`].
|
||||||
|
pub fn inner(&self) -> &tracing::Span {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Deconstruct the root span into its underlying [`tracing::Span`].
|
||||||
|
pub fn into_inner(self) -> tracing::Span {
|
||||||
|
self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn http_flavor(version: Version) -> Cow<'static, str> {
|
||||||
|
match version {
|
||||||
|
Version::HTTP_09 => "0.9".into(),
|
||||||
|
Version::HTTP_10 => "1.0".into(),
|
||||||
|
Version::HTTP_11 => "1.1".into(),
|
||||||
|
Version::HTTP_2 => "2.0".into(),
|
||||||
|
Version::HTTP_3 => "3.0".into(),
|
||||||
|
other => format!("{other:?}").into(),
|
||||||
|
}
|
||||||
|
}
|
27
examples/pavex_demo/todo_app_sqlite_pavex/src/user_agent.rs
Normal file
27
examples/pavex_demo/todo_app_sqlite_pavex/src/user_agent.rs
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
use pavex::{
|
||||||
|
http::header::{ToStrError, USER_AGENT},
|
||||||
|
request::RequestHead,
|
||||||
|
response::Response,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub enum UserAgent {
|
||||||
|
/// No User-Agent header was provided
|
||||||
|
Unknown,
|
||||||
|
/// The value of the 'User-Agent' header for the incoming request
|
||||||
|
Known(String),
|
||||||
|
}
|
||||||
|
impl UserAgent {
|
||||||
|
pub fn extract(request_head: &RequestHead) -> Result<Self, ToStrError> {
|
||||||
|
let Some(user_agent) = request_head.headers.get(USER_AGENT) else {
|
||||||
|
return Ok(UserAgent::Unknown);
|
||||||
|
};
|
||||||
|
|
||||||
|
user_agent.to_str().map(|s| UserAgent::Known(s.into()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn invalid_user_agent(_e: &ToStrError) -> Response {
|
||||||
|
Response::bad_request()
|
||||||
|
.set_typed_body("The `User-Agent` header must be a valid UTF-8 string")
|
||||||
|
.box_body()
|
||||||
|
}
|
29
examples/pavex_demo/todo_app_sqlite_pavex_server/Cargo.toml
Normal file
29
examples/pavex_demo/todo_app_sqlite_pavex_server/Cargo.toml
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
[package]
|
||||||
|
name = "todo_app_sqlite_pavex_server"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
path = "src/bin/api.rs"
|
||||||
|
name = "api"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
anyhow = "1"
|
||||||
|
pavex = { git = "https://github.com/LukeMathWalker/pavex", branch = "main" }
|
||||||
|
tokio = { version = "1", features = ["full"] }
|
||||||
|
todo_app_sqlite_pavex_server_sdk = { path = "../todo_app_sqlite_pavex_server_sdk" }
|
||||||
|
todo_app_sqlite_pavex = { path = "../todo_app_sqlite_pavex" }
|
||||||
|
|
||||||
|
# Configuration
|
||||||
|
dotenvy = "0.15"
|
||||||
|
figment = { version = "0.10", features = ["env", "yaml"] }
|
||||||
|
serde = { version = "1", features = ["derive"]}
|
||||||
|
|
||||||
|
# Telemetry
|
||||||
|
tracing = "0.1"
|
||||||
|
tracing-bunyan-formatter = "0.3"
|
||||||
|
tracing-panic = "0.1"
|
||||||
|
tracing-subscriber = { version = "0.3", default-features = false, features = ["env-filter", "registry", "smallvec", "std", "tracing-log"] }
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
reqwest = "0.11"
|
|
@ -0,0 +1,3 @@
|
||||||
|
server:
|
||||||
|
ip: "0.0.0.0"
|
||||||
|
port: 8000
|
|
@ -0,0 +1,6 @@
|
||||||
|
# This file contains the configuration for the dev environment.
|
||||||
|
# None of the values here are actually secret, so it's fine
|
||||||
|
# to commit this file to the repository.
|
||||||
|
server:
|
||||||
|
ip: "127.0.0.1"
|
||||||
|
port: 8000
|
|
@ -0,0 +1,3 @@
|
||||||
|
server:
|
||||||
|
ip: "0.0.0.0"
|
||||||
|
port: 8000
|
|
@ -0,0 +1,8 @@
|
||||||
|
# This file contains the configuration for the API when spawned
|
||||||
|
# in black-box tests.
|
||||||
|
# None of the values here are actually secret, so it's fine
|
||||||
|
# to commit this file to the repository.
|
||||||
|
server:
|
||||||
|
ip: "127.0.0.1"
|
||||||
|
# The OS will assign a random port to the test server.
|
||||||
|
port: 0
|
|
@ -0,0 +1,49 @@
|
||||||
|
use anyhow::Context;
|
||||||
|
use todo_app_sqlite_pavex_server::{
|
||||||
|
configuration::load_configuration,
|
||||||
|
telemetry::{get_subscriber, init_telemetry},
|
||||||
|
};
|
||||||
|
use todo_app_sqlite_pavex_server_sdk::{build_application_state, run};
|
||||||
|
use pavex::server::Server;
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() -> anyhow::Result<()> {
|
||||||
|
let subscriber = get_subscriber("todo_app_sqlite_pavex".into(), "info".into(), std::io::stdout);
|
||||||
|
init_telemetry(subscriber)?;
|
||||||
|
|
||||||
|
// We isolate all the server setup and launch logic in a separate function
|
||||||
|
// in order to have a single choke point where we make sure to log fatal errors
|
||||||
|
// that will cause the application to exit.
|
||||||
|
if let Err(e) = _main().await {
|
||||||
|
tracing::error!(
|
||||||
|
error.msg = %e,
|
||||||
|
error.error_chain = ?e,
|
||||||
|
"The application is exiting due to an error"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn _main() -> anyhow::Result<()> {
|
||||||
|
// Load environment variables from a .env file, if it exists.
|
||||||
|
let _ = dotenvy::dotenv();
|
||||||
|
|
||||||
|
let config = load_configuration(None)?;
|
||||||
|
let application_state = build_application_state()
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let tcp_listener = config
|
||||||
|
.server
|
||||||
|
.listener()
|
||||||
|
.await
|
||||||
|
.context("Failed to bind the server TCP listener")?;
|
||||||
|
let address = tcp_listener
|
||||||
|
.local_addr()
|
||||||
|
.context("The server TCP listener doesn't have a local socket address")?;
|
||||||
|
let server_builder = Server::new().listen(tcp_listener);
|
||||||
|
|
||||||
|
tracing::info!("Starting to listen for incoming requests at {}", address);
|
||||||
|
run(server_builder, application_state).await;
|
||||||
|
Ok(())
|
||||||
|
}
|
|
@ -0,0 +1,140 @@
|
||||||
|
use std::env::VarError;
|
||||||
|
|
||||||
|
use anyhow::Context;
|
||||||
|
use todo_app_sqlite_pavex::configuration::Config;
|
||||||
|
use figment::{
|
||||||
|
providers::{Env, Format, Yaml},
|
||||||
|
Figment,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Retrieve the application configuration by merging together multiple configuration sources.
|
||||||
|
///
|
||||||
|
/// # Application profiles
|
||||||
|
///
|
||||||
|
/// We use the concept of application profiles to allow for
|
||||||
|
/// different configuration values depending on the type of environment
|
||||||
|
/// the application is running in.
|
||||||
|
///
|
||||||
|
/// We don't rely on `figment`'s built-in support for profiles because
|
||||||
|
/// we want to make sure that values for different profiles are not co-located in
|
||||||
|
/// the same configuration file.
|
||||||
|
/// This makes it easier to avoid leaking sensitive information by mistake (e.g.
|
||||||
|
/// by committing configuration values for the `dev` profile to the repository).
|
||||||
|
///
|
||||||
|
/// You primary mechanism to specify the desired application profile is the `APP_PROFILE`
|
||||||
|
/// environment variable.
|
||||||
|
/// You can pass a `default_profile` value that will be used if the environment variable
|
||||||
|
/// is not set.
|
||||||
|
///
|
||||||
|
/// # Hierarchy
|
||||||
|
///
|
||||||
|
/// The configuration sources are:
|
||||||
|
///
|
||||||
|
/// 1. `base.yml` - Contains the default configuration values, common to all profiles.
|
||||||
|
/// 2. `<profile>.yml` - Contains the configuration values specific to the desired profile.
|
||||||
|
/// 3. Environment variables - Contains the configuration values specific to the current environment.
|
||||||
|
///
|
||||||
|
/// The configuration sources are listed in priority order, i.e.
|
||||||
|
/// the last source in the list will override any previous source.
|
||||||
|
///
|
||||||
|
/// For example, if the same configuration key is defined in both
|
||||||
|
/// the YAML file and the environment, the value from the environment
|
||||||
|
/// will be used.
|
||||||
|
pub fn load_configuration(
|
||||||
|
default_profile: Option<ApplicationProfile>,
|
||||||
|
) -> Result<Config, anyhow::Error> {
|
||||||
|
let application_profile = load_app_profile(default_profile)
|
||||||
|
.context("Failed to load the desired application profile")?;
|
||||||
|
|
||||||
|
let configuration_dir = {
|
||||||
|
let manifest_dir = env!(
|
||||||
|
"CARGO_MANIFEST_DIR",
|
||||||
|
"`CARGO_MANIFEST_DIR` was not set. Are you using a custom build system?"
|
||||||
|
);
|
||||||
|
std::path::Path::new(manifest_dir).join("configuration")
|
||||||
|
};
|
||||||
|
|
||||||
|
let base_filepath = configuration_dir.join("base.yml");
|
||||||
|
|
||||||
|
let profile_filename = format!("{}.yml", application_profile.as_str());
|
||||||
|
let profile_filepath = configuration_dir.join(profile_filename);
|
||||||
|
|
||||||
|
let figment = Figment::new()
|
||||||
|
.merge(Yaml::file(base_filepath))
|
||||||
|
.merge(Yaml::file(profile_filepath))
|
||||||
|
.merge(Env::prefixed("APP_"));
|
||||||
|
|
||||||
|
let configuration: Config = figment
|
||||||
|
.extract()
|
||||||
|
.context("Failed to load hierarchical configuration")?;
|
||||||
|
Ok(configuration)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Load the application profile from the `APP_PROFILE` environment variable.
|
||||||
|
fn load_app_profile(
|
||||||
|
default_profile: Option<ApplicationProfile>,
|
||||||
|
) -> Result<ApplicationProfile, anyhow::Error> {
|
||||||
|
static PROFILE_ENV_VAR: &str = "APP_PROFILE";
|
||||||
|
|
||||||
|
match std::env::var(PROFILE_ENV_VAR) {
|
||||||
|
Ok(raw_value) => raw_value.parse().with_context(|| {
|
||||||
|
format!("Failed to parse the `{PROFILE_ENV_VAR}` environment variable")
|
||||||
|
}),
|
||||||
|
Err(VarError::NotPresent) if default_profile.is_some() => Ok(default_profile.unwrap()),
|
||||||
|
Err(e) => Err(anyhow::anyhow!(e).context(format!(
|
||||||
|
"Failed to read the `{PROFILE_ENV_VAR}` environment variable"
|
||||||
|
))),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The application profile, i.e. the type of environment the application is running in.
|
||||||
|
/// See [`load_configuration`] for more details.
|
||||||
|
pub enum ApplicationProfile {
|
||||||
|
/// Test profile.
|
||||||
|
///
|
||||||
|
/// This is the profile used by the integration test suite.
|
||||||
|
Test,
|
||||||
|
/// Local development profile.
|
||||||
|
///
|
||||||
|
/// This is the profile you should use when running the application locally
|
||||||
|
/// for exploratory testing.
|
||||||
|
///
|
||||||
|
/// The corresponding configuration file is `dev.yml` and it's *never* committed to the repository.
|
||||||
|
Dev,
|
||||||
|
/// Production profile.
|
||||||
|
///
|
||||||
|
/// This is the profile you should use when running the application in production—e.g.
|
||||||
|
/// when deploying it to a staging or production environment, exposed to live traffic.
|
||||||
|
///
|
||||||
|
/// The corresponding configuration file is `prod.yml`.
|
||||||
|
/// It's committed to the repository, but it's meant to contain exclusively
|
||||||
|
/// non-sensitive configuration values.
|
||||||
|
Prod,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ApplicationProfile {
|
||||||
|
/// Return the environment as a string.
|
||||||
|
pub fn as_str(&self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
ApplicationProfile::Test => "test",
|
||||||
|
ApplicationProfile::Dev => "dev",
|
||||||
|
ApplicationProfile::Prod => "prod",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::str::FromStr for ApplicationProfile {
|
||||||
|
type Err = anyhow::Error;
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
match s.to_lowercase().as_str() {
|
||||||
|
"test" => Ok(ApplicationProfile::Test),
|
||||||
|
"dev" | "development" => Ok(ApplicationProfile::Dev),
|
||||||
|
"prod" | "production" => Ok(ApplicationProfile::Prod),
|
||||||
|
s => Err(anyhow::anyhow!(
|
||||||
|
"`{}` is not a valid application profile.\nValid options are: `test`, `dev`, `prod`.",
|
||||||
|
s
|
||||||
|
)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,2 @@
|
||||||
|
pub mod configuration;
|
||||||
|
pub mod telemetry;
|
|
@ -0,0 +1,40 @@
|
||||||
|
use anyhow::Context;
|
||||||
|
use tracing::subscriber::set_global_default;
|
||||||
|
use tracing::Subscriber;
|
||||||
|
use tracing_bunyan_formatter::{BunyanFormattingLayer, JsonStorageLayer};
|
||||||
|
use tracing_subscriber::fmt::MakeWriter;
|
||||||
|
use tracing_subscriber::{layer::SubscriberExt, EnvFilter, Registry};
|
||||||
|
|
||||||
|
/// Perform all the required setup steps for our telemetry:
|
||||||
|
///
|
||||||
|
/// - Register a subscriber as global default to process span data
|
||||||
|
/// - Register a panic hook to capture any panic and record its details
|
||||||
|
///
|
||||||
|
/// It should only be called once!
|
||||||
|
pub fn init_telemetry(subscriber: impl Subscriber + Sync + Send) -> Result<(), anyhow::Error> {
|
||||||
|
std::panic::set_hook(Box::new(tracing_panic::panic_hook));
|
||||||
|
set_global_default(subscriber).context("Failed to set a `tracing` global subscriber")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Compose multiple layers into a `tracing`'s subscriber.
|
||||||
|
///
|
||||||
|
/// # Implementation Notes
|
||||||
|
///
|
||||||
|
/// We are using `impl Subscriber` as return type to avoid having to spell out the actual
|
||||||
|
/// type of the returned subscriber, which is indeed quite complex.
|
||||||
|
pub fn get_subscriber<Sink>(
|
||||||
|
application_name: String,
|
||||||
|
default_env_filter: String,
|
||||||
|
sink: Sink,
|
||||||
|
) -> impl Subscriber + Sync + Send
|
||||||
|
where
|
||||||
|
Sink: for<'a> MakeWriter<'a> + Send + Sync + 'static,
|
||||||
|
{
|
||||||
|
let env_filter =
|
||||||
|
EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new(default_env_filter));
|
||||||
|
let formatting_layer = BunyanFormattingLayer::new(application_name, sink);
|
||||||
|
Registry::default()
|
||||||
|
.with(env_filter)
|
||||||
|
.with(JsonStorageLayer)
|
||||||
|
.with(formatting_layer)
|
||||||
|
}
|
|
@ -0,0 +1,37 @@
|
||||||
|
use crate::helpers::TestApi;
|
||||||
|
use pavex::http::StatusCode;
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn greet_happy_path() {
|
||||||
|
let api = TestApi::spawn().await;
|
||||||
|
let name = "Ursula";
|
||||||
|
|
||||||
|
let response = api
|
||||||
|
.api_client
|
||||||
|
.get(&format!("{}/api/greet/{name}", &api.api_address))
|
||||||
|
.header("User-Agent", "Test runner")
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.expect("Failed to execute request.");
|
||||||
|
assert_eq!(response.status().as_u16(), StatusCode::OK.as_u16());
|
||||||
|
assert_eq!(response.text().await.unwrap(), "Hello, Ursula!");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn non_utf8_agent_is_rejected() {
|
||||||
|
let api = TestApi::spawn().await;
|
||||||
|
let name = "Ursula";
|
||||||
|
|
||||||
|
let response = api
|
||||||
|
.api_client
|
||||||
|
.get(&format!("{}/api/greet/{name}", &api.api_address))
|
||||||
|
.header("User-Agent", b"hello\xfa".as_slice())
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.expect("Failed to execute request.");
|
||||||
|
assert_eq!(response.status().as_u16(), StatusCode::BAD_REQUEST.as_u16());
|
||||||
|
assert_eq!(
|
||||||
|
response.text().await.unwrap(),
|
||||||
|
"The `User-Agent` header must be a valid UTF-8 string"
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,52 @@
|
||||||
|
use todo_app_sqlite_pavex_server::configuration::{load_configuration, ApplicationProfile};
|
||||||
|
use todo_app_sqlite_pavex_server_sdk::{build_application_state, run};
|
||||||
|
use todo_app_sqlite_pavex::configuration::Config;
|
||||||
|
use pavex::server::Server;
|
||||||
|
|
||||||
|
pub struct TestApi {
|
||||||
|
pub api_address: String,
|
||||||
|
pub api_client: reqwest::Client,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TestApi {
|
||||||
|
pub async fn spawn() -> Self {
|
||||||
|
let config = Self::get_config();
|
||||||
|
|
||||||
|
let application_state = build_application_state().await;
|
||||||
|
|
||||||
|
let tcp_listener = config
|
||||||
|
.server
|
||||||
|
.listener()
|
||||||
|
.await
|
||||||
|
.expect("Failed to bind the server TCP listener");
|
||||||
|
let address = tcp_listener
|
||||||
|
.local_addr()
|
||||||
|
.expect("The server TCP listener doesn't have a local socket address");
|
||||||
|
let server_builder = Server::new().listen(tcp_listener);
|
||||||
|
|
||||||
|
tokio::spawn(async move {
|
||||||
|
run(server_builder, application_state).await
|
||||||
|
});
|
||||||
|
|
||||||
|
TestApi {
|
||||||
|
api_address: format!("http://{}:{}", config.server.ip, address.port()),
|
||||||
|
api_client: reqwest::Client::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_config() -> Config {
|
||||||
|
load_configuration(Some(ApplicationProfile::Test)).expect("Failed to load test configuration")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convenient methods for calling the API under test.
|
||||||
|
impl TestApi {
|
||||||
|
pub async fn get_ping(&self) -> reqwest::Response
|
||||||
|
{
|
||||||
|
self.api_client
|
||||||
|
.get(&format!("{}/api/ping", &self.api_address))
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.expect("Failed to execute request.")
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,4 @@
|
||||||
|
mod greet;
|
||||||
|
mod helpers;
|
||||||
|
mod ping;
|
||||||
|
|
|
@ -0,0 +1,11 @@
|
||||||
|
use crate::helpers::TestApi;
|
||||||
|
use pavex::http::StatusCode;
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn ping_works() {
|
||||||
|
let api = TestApi::spawn().await;
|
||||||
|
|
||||||
|
let response = api.get_ping().await;
|
||||||
|
|
||||||
|
assert_eq!(response.status().as_u16(), StatusCode::OK.as_u16());
|
||||||
|
}
|
|
@ -0,0 +1,21 @@
|
||||||
|
[package]
|
||||||
|
name = "todo_app_sqlite_pavex_server_sdk"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[package.metadata.px.generate]
|
||||||
|
generator_type = "cargo_workspace_binary"
|
||||||
|
generator_name = "bp"
|
||||||
|
|
||||||
|
[lints]
|
||||||
|
clippy = { all = "allow" }
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
bytes = { version = "1.5.0", package = "bytes" }
|
||||||
|
http = { version = "1.0.0", package = "http" }
|
||||||
|
http_body_util = { version = "0.1.0", package = "http-body-util" }
|
||||||
|
hyper = { version = "1.1.0", package = "hyper" }
|
||||||
|
matchit = { version = "0.7.3", git = "https://github.com/ibraheemdev/matchit", branch = "master", package = "matchit" }
|
||||||
|
pavex = { version = "0.1.0", git = "https://github.com/LukeMathWalker/pavex", branch = "main", package = "pavex" }
|
||||||
|
thiserror = { version = "1.0.52", package = "thiserror" }
|
||||||
|
todo_app_sqlite_pavex = { version = "0.1.0", path = "../todo_app_sqlite_pavex", package = "todo_app_sqlite_pavex" }
|
|
@ -0,0 +1,233 @@
|
||||||
|
(
|
||||||
|
creation_location: (
|
||||||
|
line: 13,
|
||||||
|
column: 18,
|
||||||
|
file: "todo_app_sqlite_pavex/src/blueprint.rs",
|
||||||
|
),
|
||||||
|
constructors: [
|
||||||
|
(
|
||||||
|
constructor: (
|
||||||
|
callable: (
|
||||||
|
registered_at: "todo_app_sqlite_pavex",
|
||||||
|
import_path: "pavex::request::query::QueryParams::extract",
|
||||||
|
),
|
||||||
|
location: (
|
||||||
|
line: 32,
|
||||||
|
column: 8,
|
||||||
|
file: "todo_app_sqlite_pavex/src/blueprint.rs",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
lifecycle: RequestScoped,
|
||||||
|
cloning_strategy: None,
|
||||||
|
error_handler: Some((
|
||||||
|
callable: (
|
||||||
|
registered_at: "todo_app_sqlite_pavex",
|
||||||
|
import_path: "pavex::request::query::errors::ExtractQueryParamsError::into_response",
|
||||||
|
),
|
||||||
|
location: (
|
||||||
|
line: 36,
|
||||||
|
column: 6,
|
||||||
|
file: "todo_app_sqlite_pavex/src/blueprint.rs",
|
||||||
|
),
|
||||||
|
)),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
constructor: (
|
||||||
|
callable: (
|
||||||
|
registered_at: "todo_app_sqlite_pavex",
|
||||||
|
import_path: "pavex::request::route::RouteParams::extract",
|
||||||
|
),
|
||||||
|
location: (
|
||||||
|
line: 41,
|
||||||
|
column: 8,
|
||||||
|
file: "todo_app_sqlite_pavex/src/blueprint.rs",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
lifecycle: RequestScoped,
|
||||||
|
cloning_strategy: None,
|
||||||
|
error_handler: Some((
|
||||||
|
callable: (
|
||||||
|
registered_at: "todo_app_sqlite_pavex",
|
||||||
|
import_path: "pavex::request::route::errors::ExtractRouteParamsError::into_response",
|
||||||
|
),
|
||||||
|
location: (
|
||||||
|
line: 45,
|
||||||
|
column: 6,
|
||||||
|
file: "todo_app_sqlite_pavex/src/blueprint.rs",
|
||||||
|
),
|
||||||
|
)),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
constructor: (
|
||||||
|
callable: (
|
||||||
|
registered_at: "todo_app_sqlite_pavex",
|
||||||
|
import_path: "pavex::request::body::JsonBody::extract",
|
||||||
|
),
|
||||||
|
location: (
|
||||||
|
line: 50,
|
||||||
|
column: 8,
|
||||||
|
file: "todo_app_sqlite_pavex/src/blueprint.rs",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
lifecycle: RequestScoped,
|
||||||
|
cloning_strategy: None,
|
||||||
|
error_handler: Some((
|
||||||
|
callable: (
|
||||||
|
registered_at: "todo_app_sqlite_pavex",
|
||||||
|
import_path: "pavex::request::body::errors::ExtractJsonBodyError::into_response",
|
||||||
|
),
|
||||||
|
location: (
|
||||||
|
line: 54,
|
||||||
|
column: 6,
|
||||||
|
file: "todo_app_sqlite_pavex/src/blueprint.rs",
|
||||||
|
),
|
||||||
|
)),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
constructor: (
|
||||||
|
callable: (
|
||||||
|
registered_at: "todo_app_sqlite_pavex",
|
||||||
|
import_path: "pavex::request::body::BufferedBody::extract",
|
||||||
|
),
|
||||||
|
location: (
|
||||||
|
line: 57,
|
||||||
|
column: 8,
|
||||||
|
file: "todo_app_sqlite_pavex/src/blueprint.rs",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
lifecycle: RequestScoped,
|
||||||
|
cloning_strategy: None,
|
||||||
|
error_handler: Some((
|
||||||
|
callable: (
|
||||||
|
registered_at: "todo_app_sqlite_pavex",
|
||||||
|
import_path: "pavex::request::body::errors::ExtractBufferedBodyError::into_response",
|
||||||
|
),
|
||||||
|
location: (
|
||||||
|
line: 61,
|
||||||
|
column: 6,
|
||||||
|
file: "todo_app_sqlite_pavex/src/blueprint.rs",
|
||||||
|
),
|
||||||
|
)),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
constructor: (
|
||||||
|
callable: (
|
||||||
|
registered_at: "todo_app_sqlite_pavex",
|
||||||
|
import_path: "<pavex::request::body::BodySizeLimit as std::default::Default>::default",
|
||||||
|
),
|
||||||
|
location: (
|
||||||
|
line: 64,
|
||||||
|
column: 8,
|
||||||
|
file: "todo_app_sqlite_pavex/src/blueprint.rs",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
lifecycle: RequestScoped,
|
||||||
|
cloning_strategy: None,
|
||||||
|
error_handler: None,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
constructor: (
|
||||||
|
callable: (
|
||||||
|
registered_at: "todo_app_sqlite_pavex",
|
||||||
|
import_path: "crate::user_agent::UserAgent::extract",
|
||||||
|
),
|
||||||
|
location: (
|
||||||
|
line: 16,
|
||||||
|
column: 8,
|
||||||
|
file: "todo_app_sqlite_pavex/src/blueprint.rs",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
lifecycle: RequestScoped,
|
||||||
|
cloning_strategy: None,
|
||||||
|
error_handler: Some((
|
||||||
|
callable: (
|
||||||
|
registered_at: "todo_app_sqlite_pavex",
|
||||||
|
import_path: "crate::user_agent::invalid_user_agent",
|
||||||
|
),
|
||||||
|
location: (
|
||||||
|
line: 20,
|
||||||
|
column: 6,
|
||||||
|
file: "todo_app_sqlite_pavex/src/blueprint.rs",
|
||||||
|
),
|
||||||
|
)),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
constructor: (
|
||||||
|
callable: (
|
||||||
|
registered_at: "todo_app_sqlite_pavex",
|
||||||
|
import_path: "crate::telemetry::RootSpan::new",
|
||||||
|
),
|
||||||
|
location: (
|
||||||
|
line: 72,
|
||||||
|
column: 8,
|
||||||
|
file: "todo_app_sqlite_pavex/src/blueprint.rs",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
lifecycle: RequestScoped,
|
||||||
|
cloning_strategy: Some(CloneIfNecessary),
|
||||||
|
error_handler: None,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
middlewares: [
|
||||||
|
(
|
||||||
|
middleware: (
|
||||||
|
callable: (
|
||||||
|
registered_at: "todo_app_sqlite_pavex",
|
||||||
|
import_path: "crate::telemetry::logger",
|
||||||
|
),
|
||||||
|
location: (
|
||||||
|
line: 78,
|
||||||
|
column: 8,
|
||||||
|
file: "todo_app_sqlite_pavex/src/blueprint.rs",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
error_handler: None,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
routes: [
|
||||||
|
(
|
||||||
|
path: "/api/ping",
|
||||||
|
method_guard: (
|
||||||
|
inner: Some((
|
||||||
|
bitset: 256,
|
||||||
|
extensions: [],
|
||||||
|
)),
|
||||||
|
),
|
||||||
|
request_handler: (
|
||||||
|
callable: (
|
||||||
|
registered_at: "todo_app_sqlite_pavex",
|
||||||
|
import_path: "crate::routes::status::ping",
|
||||||
|
),
|
||||||
|
location: (
|
||||||
|
line: 24,
|
||||||
|
column: 8,
|
||||||
|
file: "todo_app_sqlite_pavex/src/blueprint.rs",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
error_handler: None,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
path: "/api/greet/:name",
|
||||||
|
method_guard: (
|
||||||
|
inner: Some((
|
||||||
|
bitset: 256,
|
||||||
|
extensions: [],
|
||||||
|
)),
|
||||||
|
),
|
||||||
|
request_handler: (
|
||||||
|
callable: (
|
||||||
|
registered_at: "todo_app_sqlite_pavex",
|
||||||
|
import_path: "crate::routes::greet::greet",
|
||||||
|
),
|
||||||
|
location: (
|
||||||
|
line: 25,
|
||||||
|
column: 8,
|
||||||
|
file: "todo_app_sqlite_pavex/src/blueprint.rs",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
error_handler: None,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
fallback_request_handler: None,
|
||||||
|
nested_blueprints: [],
|
||||||
|
)
|
254
examples/pavex_demo/todo_app_sqlite_pavex_server_sdk/src/lib.rs
Normal file
254
examples/pavex_demo/todo_app_sqlite_pavex_server_sdk/src/lib.rs
Normal file
|
@ -0,0 +1,254 @@
|
||||||
|
//! Do NOT edit this code.
|
||||||
|
//! It was automatically generated by Pavex.
|
||||||
|
//! All manual edits will be lost next time the code is generated.
|
||||||
|
extern crate alloc;
|
||||||
|
struct ServerState {
|
||||||
|
router: matchit::Router<u32>,
|
||||||
|
#[allow(dead_code)]
|
||||||
|
application_state: ApplicationState,
|
||||||
|
}
|
||||||
|
pub struct ApplicationState {}
|
||||||
|
pub async fn build_application_state() -> crate::ApplicationState {
|
||||||
|
crate::ApplicationState {}
|
||||||
|
}
|
||||||
|
pub fn run(
|
||||||
|
server_builder: pavex::server::Server,
|
||||||
|
application_state: ApplicationState,
|
||||||
|
) -> pavex::server::ServerHandle {
|
||||||
|
let server_state = std::sync::Arc::new(ServerState {
|
||||||
|
router: build_router(),
|
||||||
|
application_state,
|
||||||
|
});
|
||||||
|
server_builder.serve(route_request, server_state)
|
||||||
|
}
|
||||||
|
fn build_router() -> matchit::Router<u32> {
|
||||||
|
let mut router = matchit::Router::new();
|
||||||
|
router.insert("/api/greet/:name", 0u32).unwrap();
|
||||||
|
router.insert("/api/ping", 1u32).unwrap();
|
||||||
|
router
|
||||||
|
}
|
||||||
|
async fn route_request(
|
||||||
|
request: http::Request<hyper::body::Incoming>,
|
||||||
|
server_state: std::sync::Arc<ServerState>,
|
||||||
|
) -> pavex::response::Response {
|
||||||
|
let (request_head, request_body) = request.into_parts();
|
||||||
|
#[allow(unused)]
|
||||||
|
let request_body = pavex::request::body::RawIncomingBody::from(request_body);
|
||||||
|
let request_head: pavex::request::RequestHead = request_head.into();
|
||||||
|
let matched_route = match server_state.router.at(&request_head.uri.path()) {
|
||||||
|
Ok(m) => m,
|
||||||
|
Err(_) => {
|
||||||
|
let allowed_methods: pavex::router::AllowedMethods = pavex::router::MethodAllowList::from_iter(
|
||||||
|
vec![],
|
||||||
|
)
|
||||||
|
.into();
|
||||||
|
let matched_route_template = pavex::request::route::MatchedRouteTemplate::new(
|
||||||
|
"*",
|
||||||
|
);
|
||||||
|
return route_2::middleware_0(
|
||||||
|
matched_route_template,
|
||||||
|
&allowed_methods,
|
||||||
|
&request_head,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let route_id = matched_route.value;
|
||||||
|
#[allow(unused)]
|
||||||
|
let url_params: pavex::request::route::RawRouteParams<'_, '_> = matched_route
|
||||||
|
.params
|
||||||
|
.into();
|
||||||
|
match route_id {
|
||||||
|
0u32 => {
|
||||||
|
let matched_route_template = pavex::request::route::MatchedRouteTemplate::new(
|
||||||
|
"/api/greet/:name",
|
||||||
|
);
|
||||||
|
match &request_head.method {
|
||||||
|
&pavex::http::Method::GET => {
|
||||||
|
route_1::middleware_0(
|
||||||
|
matched_route_template,
|
||||||
|
url_params,
|
||||||
|
&request_head,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
let allowed_methods: pavex::router::AllowedMethods = pavex::router::MethodAllowList::from_iter([
|
||||||
|
pavex::http::Method::GET,
|
||||||
|
])
|
||||||
|
.into();
|
||||||
|
route_2::middleware_0(
|
||||||
|
matched_route_template,
|
||||||
|
&allowed_methods,
|
||||||
|
&request_head,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
1u32 => {
|
||||||
|
let matched_route_template = pavex::request::route::MatchedRouteTemplate::new(
|
||||||
|
"/api/ping",
|
||||||
|
);
|
||||||
|
match &request_head.method {
|
||||||
|
&pavex::http::Method::GET => {
|
||||||
|
route_0::middleware_0(matched_route_template, &request_head).await
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
let allowed_methods: pavex::router::AllowedMethods = pavex::router::MethodAllowList::from_iter([
|
||||||
|
pavex::http::Method::GET,
|
||||||
|
])
|
||||||
|
.into();
|
||||||
|
route_2::middleware_0(
|
||||||
|
matched_route_template,
|
||||||
|
&allowed_methods,
|
||||||
|
&request_head,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
i => unreachable!("Unknown route id: {}", i),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub mod route_0 {
|
||||||
|
pub async fn middleware_0(
|
||||||
|
v0: pavex::request::route::MatchedRouteTemplate,
|
||||||
|
v1: &pavex::request::RequestHead,
|
||||||
|
) -> pavex::response::Response {
|
||||||
|
let v2 = todo_app_sqlite_pavex::telemetry::RootSpan::new(v1, v0);
|
||||||
|
let v3 = crate::route_0::Next0 {
|
||||||
|
next: handler,
|
||||||
|
};
|
||||||
|
let v4 = pavex::middleware::Next::new(v3);
|
||||||
|
todo_app_sqlite_pavex::telemetry::logger(v4, v2).await
|
||||||
|
}
|
||||||
|
pub async fn handler() -> pavex::response::Response {
|
||||||
|
let v0 = todo_app_sqlite_pavex::routes::status::ping();
|
||||||
|
<http::StatusCode as pavex::response::IntoResponse>::into_response(v0)
|
||||||
|
}
|
||||||
|
pub struct Next0<T>
|
||||||
|
where
|
||||||
|
T: std::future::Future<Output = pavex::response::Response>,
|
||||||
|
{
|
||||||
|
next: fn() -> T,
|
||||||
|
}
|
||||||
|
impl<T> std::future::IntoFuture for Next0<T>
|
||||||
|
where
|
||||||
|
T: std::future::Future<Output = pavex::response::Response>,
|
||||||
|
{
|
||||||
|
type Output = pavex::response::Response;
|
||||||
|
type IntoFuture = T;
|
||||||
|
fn into_future(self) -> Self::IntoFuture {
|
||||||
|
(self.next)()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub mod route_1 {
|
||||||
|
pub async fn middleware_0(
|
||||||
|
v0: pavex::request::route::MatchedRouteTemplate,
|
||||||
|
v1: pavex::request::route::RawRouteParams<'_, '_>,
|
||||||
|
v2: &pavex::request::RequestHead,
|
||||||
|
) -> pavex::response::Response {
|
||||||
|
let v3 = todo_app_sqlite_pavex::telemetry::RootSpan::new(v2, v0);
|
||||||
|
let v4 = crate::route_1::Next0 {
|
||||||
|
s_0: v1,
|
||||||
|
s_1: v2,
|
||||||
|
next: handler,
|
||||||
|
};
|
||||||
|
let v5 = pavex::middleware::Next::new(v4);
|
||||||
|
todo_app_sqlite_pavex::telemetry::logger(v5, v3).await
|
||||||
|
}
|
||||||
|
pub async fn handler(
|
||||||
|
v0: pavex::request::route::RawRouteParams<'_, '_>,
|
||||||
|
v1: &pavex::request::RequestHead,
|
||||||
|
) -> pavex::response::Response {
|
||||||
|
let v2 = todo_app_sqlite_pavex::user_agent::UserAgent::extract(v1);
|
||||||
|
let v3 = match v2 {
|
||||||
|
Ok(ok) => ok,
|
||||||
|
Err(v3) => {
|
||||||
|
return {
|
||||||
|
let v4 = todo_app_sqlite_pavex::user_agent::invalid_user_agent(&v3);
|
||||||
|
<pavex::response::Response as pavex::response::IntoResponse>::into_response(
|
||||||
|
v4,
|
||||||
|
)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let v4 = pavex::request::route::RouteParams::extract(v0);
|
||||||
|
let v5 = match v4 {
|
||||||
|
Ok(ok) => ok,
|
||||||
|
Err(v5) => {
|
||||||
|
return {
|
||||||
|
let v6 = pavex::request::route::errors::ExtractRouteParamsError::into_response(
|
||||||
|
&v5,
|
||||||
|
);
|
||||||
|
<pavex::response::Response<
|
||||||
|
http_body_util::Full<bytes::Bytes>,
|
||||||
|
> as pavex::response::IntoResponse>::into_response(v6)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let v6 = todo_app_sqlite_pavex::routes::greet::greet(v5, v3);
|
||||||
|
<pavex::response::Response as pavex::response::IntoResponse>::into_response(v6)
|
||||||
|
}
|
||||||
|
pub struct Next0<'a, 'b, 'c, T>
|
||||||
|
where
|
||||||
|
T: std::future::Future<Output = pavex::response::Response>,
|
||||||
|
{
|
||||||
|
s_0: pavex::request::route::RawRouteParams<'a, 'b>,
|
||||||
|
s_1: &'c pavex::request::RequestHead,
|
||||||
|
next: fn(
|
||||||
|
pavex::request::route::RawRouteParams<'a, 'b>,
|
||||||
|
&'c pavex::request::RequestHead,
|
||||||
|
) -> T,
|
||||||
|
}
|
||||||
|
impl<'a, 'b, 'c, T> std::future::IntoFuture for Next0<'a, 'b, 'c, T>
|
||||||
|
where
|
||||||
|
T: std::future::Future<Output = pavex::response::Response>,
|
||||||
|
{
|
||||||
|
type Output = pavex::response::Response;
|
||||||
|
type IntoFuture = T;
|
||||||
|
fn into_future(self) -> Self::IntoFuture {
|
||||||
|
(self.next)(self.s_0, self.s_1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub mod route_2 {
|
||||||
|
pub async fn middleware_0(
|
||||||
|
v0: pavex::request::route::MatchedRouteTemplate,
|
||||||
|
v1: &pavex::router::AllowedMethods,
|
||||||
|
v2: &pavex::request::RequestHead,
|
||||||
|
) -> pavex::response::Response {
|
||||||
|
let v3 = todo_app_sqlite_pavex::telemetry::RootSpan::new(v2, v0);
|
||||||
|
let v4 = crate::route_2::Next0 {
|
||||||
|
s_0: v1,
|
||||||
|
next: handler,
|
||||||
|
};
|
||||||
|
let v5 = pavex::middleware::Next::new(v4);
|
||||||
|
todo_app_sqlite_pavex::telemetry::logger(v5, v3).await
|
||||||
|
}
|
||||||
|
pub async fn handler(
|
||||||
|
v0: &pavex::router::AllowedMethods,
|
||||||
|
) -> pavex::response::Response {
|
||||||
|
let v1 = pavex::router::default_fallback(v0).await;
|
||||||
|
<pavex::response::Response as pavex::response::IntoResponse>::into_response(v1)
|
||||||
|
}
|
||||||
|
pub struct Next0<'a, T>
|
||||||
|
where
|
||||||
|
T: std::future::Future<Output = pavex::response::Response>,
|
||||||
|
{
|
||||||
|
s_0: &'a pavex::router::AllowedMethods,
|
||||||
|
next: fn(&'a pavex::router::AllowedMethods) -> T,
|
||||||
|
}
|
||||||
|
impl<'a, T> std::future::IntoFuture for Next0<'a, T>
|
||||||
|
where
|
||||||
|
T: std::future::Future<Output = pavex::response::Response>,
|
||||||
|
{
|
||||||
|
type Output = pavex::response::Response;
|
||||||
|
type IntoFuture = T;
|
||||||
|
fn into_future(self) -> Self::IntoFuture {
|
||||||
|
(self.next)(self.s_0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
12
flake.lock
12
flake.lock
|
@ -38,11 +38,11 @@
|
||||||
},
|
},
|
||||||
"nixpkgs": {
|
"nixpkgs": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1703637592,
|
"lastModified": 1703961334,
|
||||||
"narHash": "sha256-8MXjxU0RfFfzl57Zy3OfXCITS0qWDNLzlBAdwxGZwfY=",
|
"narHash": "sha256-M1mV/Cq+pgjk0rt6VxoyyD+O8cOUiai8t9Q6Yyq4noY=",
|
||||||
"owner": "NixOS",
|
"owner": "NixOS",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "cfc3698c31b1fb9cdcf10f36c9643460264d0ca8",
|
"rev": "b0d36bd0a420ecee3bc916c91886caca87c894e9",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
@ -81,11 +81,11 @@
|
||||||
"nixpkgs": "nixpkgs_2"
|
"nixpkgs": "nixpkgs_2"
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1703902408,
|
"lastModified": 1704075545,
|
||||||
"narHash": "sha256-qXdWvu+tlgNjeoz8yQMRKSom6QyRROfgpmeOhwbujqw=",
|
"narHash": "sha256-L3zgOuVKhPjKsVLc3yTm2YJ6+BATyZBury7wnhyc8QU=",
|
||||||
"owner": "oxalica",
|
"owner": "oxalica",
|
||||||
"repo": "rust-overlay",
|
"repo": "rust-overlay",
|
||||||
"rev": "319f57cd2c34348c55970a4bf2b35afe82088681",
|
"rev": "a0df72e106322b67e9c6e591fe870380bd0da0d5",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
|
|
@ -50,17 +50,21 @@ pub fn server_impl(args: TokenStream, s: TokenStream) -> TokenStream {
|
||||||
if args.prefix.is_none() {
|
if args.prefix.is_none() {
|
||||||
args.prefix = Some(Literal::string("/api"));
|
args.prefix = Some(Literal::string("/api"));
|
||||||
}
|
}
|
||||||
|
let args_prefix = match &args.prefix {
|
||||||
|
Some(s) => s.to_string(),
|
||||||
|
None => "/api".to_string(),
|
||||||
|
};
|
||||||
// default to "Url" if no encoding given
|
// default to "Url" if no encoding given
|
||||||
if args.encoding.is_none() {
|
if args.encoding.is_none() {
|
||||||
args.encoding = Some(Literal::string("Url"));
|
args.encoding = Some(Literal::string("Url"));
|
||||||
}
|
}
|
||||||
|
// Either this match is wrong, or the impl in the macro crate is wrong
|
||||||
match server_fn_macro::server_macro_impl(
|
match server_fn_macro::server_macro_impl(
|
||||||
quote::quote!(#args),
|
quote::quote!(#args),
|
||||||
mapped_body,
|
mapped_body,
|
||||||
syn::parse_quote!(::leptos::leptos_server::ServerFnTraitObj),
|
syn::parse_quote!(::leptos::leptos_server::ServerFnTraitObj),
|
||||||
None,
|
|
||||||
Some(syn::parse_quote!(::leptos::server_fn)),
|
Some(syn::parse_quote!(::leptos::server_fn)),
|
||||||
|
&args_prefix,
|
||||||
) {
|
) {
|
||||||
Err(e) => e.to_compile_error().into(),
|
Err(e) => e.to_compile_error().into(),
|
||||||
Ok(s) => s.to_token_stream().into(),
|
Ok(s) => s.to_token_stream().into(),
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
use crate::{ServerFn, ServerFnError};
|
//use crate::{ServerFn, ServerFnError};
|
||||||
use leptos_reactive::{
|
use leptos_reactive::{
|
||||||
batch, create_rw_signal, is_suppressing_resource_load, signal_prelude::*,
|
batch, create_rw_signal, is_suppressing_resource_load, signal_prelude::*,
|
||||||
spawn_local, store_value, ReadSignal, RwSignal, StoredValue,
|
spawn_local, store_value, ReadSignal, RwSignal, StoredValue,
|
||||||
};
|
};
|
||||||
|
use server_fn::{ServerFn, ServerFnError};
|
||||||
use std::{cell::Cell, future::Future, pin::Pin, rc::Rc};
|
use std::{cell::Cell, future::Future, pin::Pin, rc::Rc};
|
||||||
|
|
||||||
/// An action synchronizes an imperative `async` call to the synchronous reactive system.
|
/// An action synchronizes an imperative `async` call to the synchronous reactive system.
|
||||||
|
|
|
@ -1,120 +1,120 @@
|
||||||
#![deny(missing_docs)]
|
//#![deny(missing_docs)]
|
||||||
#![forbid(unsafe_code)]
|
#![forbid(unsafe_code)]
|
||||||
|
|
||||||
//! # Leptos Server Functions
|
////! # Leptos Server Functions
|
||||||
//!
|
////!
|
||||||
//! This package is based on a simple idea: sometimes it’s useful to write functions
|
////! This package is based on a simple idea: sometimes it’s useful to write functions
|
||||||
//! that will only run on the server, and call them from the client.
|
////! that will only run on the server, and call them from the client.
|
||||||
//!
|
////!
|
||||||
//! If you’re creating anything beyond a toy app, you’ll need to do this all the time:
|
////! If you’re creating anything beyond a toy app, you’ll need to do this all the time:
|
||||||
//! reading from or writing to a database that only runs on the server, running expensive
|
////! reading from or writing to a database that only runs on the server, running expensive
|
||||||
//! computations using libraries you don’t want to ship down to the client, accessing
|
////! computations using libraries you don’t want to ship down to the client, accessing
|
||||||
//! APIs that need to be called from the server rather than the client for CORS reasons
|
////! APIs that need to be called from the server rather than the client for CORS reasons
|
||||||
//! or because you need a secret API key that’s stored on the server and definitely
|
////! or because you need a secret API key that’s stored on the server and definitely
|
||||||
//! shouldn’t be shipped down to a user’s browser.
|
////! shouldn’t be shipped down to a user’s browser.
|
||||||
//!
|
////!
|
||||||
//! Traditionally, this is done by separating your server and client code, and by setting
|
////! Traditionally, this is done by separating your server and client code, and by setting
|
||||||
//! up something like a REST API or GraphQL API to allow your client to fetch and mutate
|
////! up something like a REST API or GraphQL API to allow your client to fetch and mutate
|
||||||
//! data on the server. This is fine, but it requires you to write and maintain your code
|
////! data on the server. This is fine, but it requires you to write and maintain your code
|
||||||
//! in multiple separate places (client-side code for fetching, server-side functions to run),
|
////! in multiple separate places (client-side code for fetching, server-side functions to run),
|
||||||
//! as well as creating a third thing to manage, which is the API contract between the two.
|
////! as well as creating a third thing to manage, which is the API contract between the two.
|
||||||
//!
|
////!
|
||||||
//! This package provides two simple primitives that allow you instead to write co-located,
|
////! This package provides two simple primitives that allow you instead to write co-located,
|
||||||
//! isomorphic server functions. (*Co-located* means you can write them in your app code so
|
////! isomorphic server functions. (*Co-located* means you can write them in your app code so
|
||||||
//! that they are “located alongside” the client code that calls them, rather than separating
|
////! that they are “located alongside” the client code that calls them, rather than separating
|
||||||
//! the client and server sides. *Isomorphic* means you can call them from the client as if
|
////! the client and server sides. *Isomorphic* means you can call them from the client as if
|
||||||
//! you were simply calling a function; the function call has the “same shape” on the client
|
////! you were simply calling a function; the function call has the “same shape” on the client
|
||||||
//! as it does on the server.)
|
////! as it does on the server.)
|
||||||
//!
|
////!
|
||||||
//! ### `#[server]`
|
////! ### `#[server]`
|
||||||
//!
|
////!
|
||||||
//! The [`#[server]`](https://docs.rs/leptos/latest/leptos/attr.server.html) macro allows you to annotate a function to
|
////! The [`#[server]`](https://docs.rs/leptos/latest/leptos/attr.server.html) macro allows you to annotate a function to
|
||||||
//! indicate that it should only run on the server (i.e., when you have an `ssr` feature in your
|
////! indicate that it should only run on the server (i.e., when you have an `ssr` feature in your
|
||||||
//! crate that is enabled).
|
////! crate that is enabled).
|
||||||
//!
|
////!
|
||||||
//! ```rust,ignore
|
////! ```rust,ignore
|
||||||
//! use leptos::*;
|
////! use leptos::*;
|
||||||
//! #[server(ReadFromDB)]
|
////! #[server(ReadFromDB)]
|
||||||
//! async fn read_posts(how_many: usize, query: String) -> Result<Vec<Posts>, ServerFnError> {
|
////! async fn read_posts(how_many: usize, query: String) -> Result<Vec<Posts>, ServerFnError> {
|
||||||
//! // do some server-only work here to access the database
|
////! // do some server-only work here to access the database
|
||||||
//! let posts = todo!();;
|
////! let posts = todo!();;
|
||||||
//! Ok(posts)
|
////! Ok(posts)
|
||||||
//! }
|
////! }
|
||||||
//!
|
////!
|
||||||
//! // call the function
|
////! // call the function
|
||||||
//! spawn_local(async {
|
////! spawn_local(async {
|
||||||
//! let posts = read_posts(3, "my search".to_string()).await;
|
////! let posts = read_posts(3, "my search".to_string()).await;
|
||||||
//! log::debug!("posts = {posts:#?}");
|
////! log::debug!("posts = {posts:#?}");
|
||||||
//! });
|
////! });
|
||||||
//! ```
|
////! ```
|
||||||
//!
|
////!
|
||||||
//! If you call this function from the client, it will serialize the function arguments and `POST`
|
////! If you call this function from the client, it will serialize the function arguments and `POST`
|
||||||
//! them to the server as if they were the inputs in `<form method="POST">`.
|
////! them to the server as if they were the inputs in `<form method="POST">`.
|
||||||
//!
|
////!
|
||||||
//! Here’s what you need to remember:
|
////! Here’s what you need to remember:
|
||||||
//! - **Server functions must be `async`.** Even if the work being done inside the function body
|
////! - **Server functions must be `async`.** Even if the work being done inside the function body
|
||||||
//! can run synchronously on the server, from the client’s perspective it involves an asynchronous
|
////! can run synchronously on the server, from the client’s perspective it involves an asynchronous
|
||||||
//! function call.
|
////! function call.
|
||||||
//! - **Server functions must return `Result<T, ServerFnError>`.** Even if the work being done
|
////! - **Server functions must return `Result<T, ServerFnError>`.** Even if the work being done
|
||||||
//! inside the function body can’t fail, the processes of serialization/deserialization and the
|
////! inside the function body can’t fail, the processes of serialization/deserialization and the
|
||||||
//! network call are fallible.
|
////! network call are fallible.
|
||||||
//! - **Return types must be [Serializable](leptos_reactive::Serializable).**
|
////! - **Return types must be [Serializable](leptos_reactive::Serializable).**
|
||||||
//! This should be fairly obvious: we have to serialize arguments to send them to the server, and we
|
////! This should be fairly obvious: we have to serialize arguments to send them to the server, and we
|
||||||
//! need to deserialize the result to return it to the client.
|
////! need to deserialize the result to return it to the client.
|
||||||
//! - **Arguments must be implement [serde::Serialize].** They are serialized as an `application/x-www-form-urlencoded`
|
////! - **Arguments must be implement [serde::Serialize].** They are serialized as an `application/x-www-form-urlencoded`
|
||||||
//! form data using [`serde_qs`](https://docs.rs/serde_qs/latest/serde_qs/) or as `application/cbor`
|
////! form data using [`serde_qs`](https://docs.rs/serde_qs/latest/serde_qs/) or as `application/cbor`
|
||||||
//! using [`cbor`](https://docs.rs/cbor/latest/cbor/). **Note**: You should explicitly include `serde` with the
|
////! using [`cbor`](https://docs.rs/cbor/latest/cbor/). **Note**: You should explicitly include `serde` with the
|
||||||
//! `derive` feature enabled in your `Cargo.toml`. You can do this by running `cargo add serde --features=derive`.
|
////! `derive` feature enabled in your `Cargo.toml`. You can do this by running `cargo add serde --features=derive`.
|
||||||
//! - Context comes from the server. [`use_context`](leptos_reactive::use_context) can be used to access specific
|
////! - Context comes from the server. [`use_context`](leptos_reactive::use_context) can be used to access specific
|
||||||
//! server-related data, as documented in the server integrations. This allows accessing things like HTTP request
|
////! server-related data, as documented in the server integrations. This allows accessing things like HTTP request
|
||||||
//! headers as needed. However, server functions *not* have access to reactive state that exists in the client.
|
////! headers as needed. However, server functions *not* have access to reactive state that exists in the client.
|
||||||
//!
|
////!
|
||||||
//! ## Server Function Encodings
|
////! ## Server Function Encodings
|
||||||
//!
|
////!
|
||||||
//! By default, the server function call is a `POST` request that serializes the arguments as URL-encoded form data in the body
|
////! By default, the server function call is a `POST` request that serializes the arguments as URL-encoded form data in the body
|
||||||
//! of the request. But there are a few other methods supported. Optionally, we can provide another argument to the `#[server]`
|
////! of the request. But there are a few other methods supported. Optionally, we can provide another argument to the `#[server]`
|
||||||
//! macro to specify an alternate encoding:
|
////! macro to specify an alternate encoding:
|
||||||
//!
|
////!
|
||||||
//! ```rust,ignore
|
////! ```rust,ignore
|
||||||
//! #[server(AddTodo, "/api", "Url")]
|
////! #[server(AddTodo, "/api", "Url")]
|
||||||
//! #[server(AddTodo, "/api", "GetJson")]
|
////! #[server(AddTodo, "/api", "GetJson")]
|
||||||
//! #[server(AddTodo, "/api", "Cbor")]
|
////! #[server(AddTodo, "/api", "Cbor")]
|
||||||
//! #[server(AddTodo, "/api", "GetCbor")]
|
////! #[server(AddTodo, "/api", "GetCbor")]
|
||||||
//! ```
|
////! ```
|
||||||
//!
|
////!
|
||||||
//! The four options use different combinations of HTTP verbs and encoding methods:
|
////! The four options use different combinations of HTTP verbs and encoding methods:
|
||||||
//!
|
////!
|
||||||
//! | Name | Method | Request | Response |
|
////! | Name | Method | Request | Response |
|
||||||
//! | ----------------- | ------ | ----------- | -------- |
|
////! | ----------------- | ------ | ----------- | -------- |
|
||||||
//! | **Url** (default) | POST | URL encoded | JSON |
|
////! | **Url** (default) | POST | URL encoded | JSON |
|
||||||
//! | **GetJson** | GET | URL encoded | JSON |
|
////! | **GetJson** | GET | URL encoded | JSON |
|
||||||
//! | **Cbor** | POST | CBOR | CBOR |
|
////! | **Cbor** | POST | CBOR | CBOR |
|
||||||
//! | **GetCbor** | GET | URL encoded | CBOR |
|
////! | **GetCbor** | GET | URL encoded | CBOR |
|
||||||
//!
|
////!
|
||||||
//! In other words, you have two choices:
|
////! In other words, you have two choices:
|
||||||
//!
|
////!
|
||||||
//! - `GET` or `POST`? This has implications for things like browser or CDN caching; while `POST` requests should not be cached,
|
////! - `GET` or `POST`? This has implications for things like browser or CDN caching; while `POST` requests should not be cached,
|
||||||
//! `GET` requests can be.
|
////! `GET` requests can be.
|
||||||
//! - Plain text (arguments sent with URL/form encoding, results sent as JSON) or a binary format (CBOR, encoded as a base64
|
////! - Plain text (arguments sent with URL/form encoding, results sent as JSON) or a binary format (CBOR, encoded as a base64
|
||||||
//! string)?
|
////! string)?
|
||||||
//!
|
////!
|
||||||
//! ## Why not `PUT` or `DELETE`? Why URL/form encoding, and not JSON?**
|
////! ## Why not `PUT` or `DELETE`? Why URL/form encoding, and not JSON?**
|
||||||
//!
|
////!
|
||||||
//! These are reasonable questions. Much of the web is built on REST API patterns that encourage the use of semantic HTTP
|
////! These are reasonable questions. Much of the web is built on REST API patterns that encourage the use of semantic HTTP
|
||||||
//! methods like `DELETE` to delete an item from a database, and many devs are accustomed to sending data to APIs in the
|
////! methods like `DELETE` to delete an item from a database, and many devs are accustomed to sending data to APIs in the
|
||||||
//! JSON format.
|
////! JSON format.
|
||||||
//!
|
////!
|
||||||
//! The reason we use `POST` or `GET` with URL-encoded data by default is the `<form>` support. For better or for worse,
|
////! The reason we use `POST` or `GET` with URL-encoded data by default is the `<form>` support. For better or for worse,
|
||||||
//! HTML forms don’t support `PUT` or `DELETE`, and they don’t support sending JSON. This means that if you use anything
|
////! HTML forms don’t support `PUT` or `DELETE`, and they don’t support sending JSON. This means that if you use anything
|
||||||
//! but a `GET` or `POST` request with URL-encoded data, it can only work once WASM has loaded.
|
////! but a `GET` or `POST` request with URL-encoded data, it can only work once WASM has loaded.
|
||||||
//!
|
////!
|
||||||
//! The CBOR encoding is suported for historical reasons; an earlier version of server functions used a URL encoding that
|
////! The CBOR encoding is suported for historical reasons; an earlier version of server functions used a URL encoding that
|
||||||
//! didn’t support nested objects like structs or vectors as server function arguments, which CBOR did. But note that the
|
////! didn’t support nested objects like structs or vectors as server function arguments, which CBOR did. But note that the
|
||||||
//! CBOR forms encounter the same issue as `PUT`, `DELETE`, or JSON: they do not degrade gracefully if the WASM version of
|
////! CBOR forms encounter the same issue as `PUT`, `DELETE`, or JSON: they do not degrade gracefully if the WASM version of
|
||||||
//! your app is not available.
|
////! your app is not available.
|
||||||
|
|
||||||
pub use server_fn::{
|
pub use server_fn::{
|
||||||
error::ServerFnErrorErr, Encoding, Payload, ServerFnError,
|
error::ServerFnErrorErr, ServerFnError,
|
||||||
};
|
};
|
||||||
|
|
||||||
mod action;
|
mod action;
|
||||||
|
@ -123,250 +123,250 @@ pub use action::*;
|
||||||
pub use multi_action::*;
|
pub use multi_action::*;
|
||||||
extern crate tracing;
|
extern crate tracing;
|
||||||
|
|
||||||
#[cfg(any(feature = "ssr", doc))]
|
//#[cfg(any(feature = "ssr", doc))]
|
||||||
use std::{
|
//use std::{
|
||||||
collections::HashMap,
|
// collections::HashMap,
|
||||||
sync::{Arc, RwLock},
|
// sync::{Arc, RwLock},
|
||||||
};
|
//};
|
||||||
|
//
|
||||||
#[cfg(any(feature = "ssr", doc))]
|
//#[cfg(any(feature = "ssr", doc))]
|
||||||
/// A concrete type for a server function.
|
///// A concrete type for a server function.
|
||||||
#[derive(Clone)]
|
//#[derive(Clone)]
|
||||||
pub struct ServerFnTraitObj(pub server_fn::ServerFnTraitObj<()>);
|
//pub struct ServerFnTraitObj(pub server_fn::ServerFnTraitObj<()>);
|
||||||
|
//
|
||||||
#[cfg(any(feature = "ssr", doc))]
|
//#[cfg(any(feature = "ssr", doc))]
|
||||||
impl std::ops::Deref for ServerFnTraitObj {
|
//impl std::ops::Deref for ServerFnTraitObj {
|
||||||
type Target = server_fn::ServerFnTraitObj<()>;
|
// type Target = server_fn::ServerFnTraitObj<()>;
|
||||||
|
//
|
||||||
fn deref(&self) -> &Self::Target {
|
// fn deref(&self) -> &Self::Target {
|
||||||
&self.0
|
// &self.0
|
||||||
}
|
// }
|
||||||
}
|
//}
|
||||||
|
//
|
||||||
#[cfg(any(feature = "ssr", doc))]
|
//#[cfg(any(feature = "ssr", doc))]
|
||||||
impl std::ops::DerefMut for ServerFnTraitObj {
|
//impl std::ops::DerefMut for ServerFnTraitObj {
|
||||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
// fn deref_mut(&mut self) -> &mut Self::Target {
|
||||||
&mut self.0
|
// &mut self.0
|
||||||
}
|
// }
|
||||||
}
|
//}
|
||||||
|
//
|
||||||
#[cfg(any(feature = "ssr", doc))]
|
//#[cfg(any(feature = "ssr", doc))]
|
||||||
impl ServerFnTraitObj {
|
//impl ServerFnTraitObj {
|
||||||
/// Create a new `ServerFnTraitObj` from a `server_fn::ServerFnTraitObj`.
|
// /// Create a new `ServerFnTraitObj` from a `server_fn::ServerFnTraitObj`.
|
||||||
pub const fn from_generic_server_fn(
|
// pub const fn from_generic_server_fn(
|
||||||
server_fn: server_fn::ServerFnTraitObj<()>,
|
// server_fn: server_fn::ServerFnTraitObj<()>,
|
||||||
) -> Self {
|
// ) -> Self {
|
||||||
Self(server_fn)
|
// Self(server_fn)
|
||||||
}
|
// }
|
||||||
}
|
//}
|
||||||
|
//
|
||||||
#[cfg(feature = "ssr")]
|
//#[cfg(feature = "ssr")]
|
||||||
inventory::collect!(ServerFnTraitObj);
|
//inventory::collect!(ServerFnTraitObj);
|
||||||
|
//
|
||||||
#[allow(unused)]
|
//#[allow(unused)]
|
||||||
type ServerFunction = server_fn::ServerFnTraitObj<()>;
|
//type ServerFunction = server_fn::ServerFnTraitObj<()>;
|
||||||
|
//
|
||||||
#[cfg(any(feature = "ssr", doc))]
|
//#[cfg(any(feature = "ssr", doc))]
|
||||||
lazy_static::lazy_static! {
|
//lazy_static::lazy_static! {
|
||||||
static ref REGISTERED_SERVER_FUNCTIONS: Arc<RwLock<HashMap<&'static str, ServerFnTraitObj>>> = {
|
// static ref REGISTERED_SERVER_FUNCTIONS: Arc<RwLock<HashMap<&'static str, ServerFnTraitObj>>> = {
|
||||||
let mut map = HashMap::new();
|
// let mut map = HashMap::new();
|
||||||
for server_fn in inventory::iter::<ServerFnTraitObj> {
|
// for server_fn in inventory::iter::<ServerFnTraitObj> {
|
||||||
map.insert(server_fn.0.url(), server_fn.clone());
|
// map.insert(server_fn.0.url(), server_fn.clone());
|
||||||
}
|
// }
|
||||||
Arc::new(RwLock::new(map))
|
// Arc::new(RwLock::new(map))
|
||||||
};
|
// };
|
||||||
}
|
//}
|
||||||
|
//
|
||||||
#[cfg(any(feature = "ssr", doc))]
|
//#[cfg(any(feature = "ssr", doc))]
|
||||||
/// The registry of all Leptos server functions.
|
///// The registry of all Leptos server functions.
|
||||||
pub struct LeptosServerFnRegistry;
|
//pub struct LeptosServerFnRegistry;
|
||||||
|
//
|
||||||
#[cfg(any(feature = "ssr", doc))]
|
//#[cfg(any(feature = "ssr", doc))]
|
||||||
impl server_fn::ServerFunctionRegistry<()> for LeptosServerFnRegistry {
|
//impl server_fn::ServerFunctionRegistry<()> for LeptosServerFnRegistry {
|
||||||
type Error = ServerRegistrationFnError;
|
// type Error = ServerRegistrationFnError;
|
||||||
|
//
|
||||||
/// Server functions are automatically registered on most platforms, (including Linux, macOS,
|
// /// Server functions are automatically registered on most platforms, (including Linux, macOS,
|
||||||
/// iOS, FreeBSD, Android, and Windows). If you are on another platform, like a WASM server runtime,
|
// /// iOS, FreeBSD, Android, and Windows). If you are on another platform, like a WASM server runtime,
|
||||||
/// you should register server functions by calling this `T::register_explicit()`.
|
// /// you should register server functions by calling this `T::register_explicit()`.
|
||||||
fn register_explicit(
|
// fn register_explicit(
|
||||||
prefix: &'static str,
|
// prefix: &'static str,
|
||||||
url: &'static str,
|
// url: &'static str,
|
||||||
server_function: server_fn::SerializedFnTraitObj<()>,
|
// server_function: server_fn::SerializedFnTraitObj<()>,
|
||||||
encoding: Encoding,
|
// encoding: Encoding,
|
||||||
) -> Result<(), Self::Error> {
|
// ) -> Result<(), Self::Error> {
|
||||||
// store it in the hashmap
|
// // store it in the hashmap
|
||||||
let mut func_write = REGISTERED_SERVER_FUNCTIONS
|
// let mut func_write = REGISTERED_SERVER_FUNCTIONS
|
||||||
.write()
|
// .write()
|
||||||
.map_err(|e| ServerRegistrationFnError::Poisoned(e.to_string()))?;
|
// .map_err(|e| ServerRegistrationFnError::Poisoned(e.to_string()))?;
|
||||||
let prev = func_write.insert(
|
// let prev = func_write.insert(
|
||||||
url,
|
// url,
|
||||||
ServerFnTraitObj(server_fn::ServerFnTraitObj::new(
|
// ServerFnTraitObj(server_fn::ServerFnTraitObj::new(
|
||||||
prefix,
|
// prefix,
|
||||||
url,
|
// url,
|
||||||
encoding,
|
// encoding,
|
||||||
server_function,
|
// server_function,
|
||||||
)),
|
// )),
|
||||||
);
|
// );
|
||||||
|
//
|
||||||
// if there was already a server function with this key,
|
// // if there was already a server function with this key,
|
||||||
// return Err
|
// // return Err
|
||||||
match prev {
|
// match prev {
|
||||||
Some(_) => {
|
// Some(_) => {
|
||||||
Err(ServerRegistrationFnError::AlreadyRegistered(format!(
|
// Err(ServerRegistrationFnError::AlreadyRegistered(format!(
|
||||||
"There was already a server function registered at {:?}. \
|
// "There was already a server function registered at {:?}. \
|
||||||
This can happen if you use the same server function name \
|
// This can happen if you use the same server function name \
|
||||||
in two different modules
|
// in two different modules
|
||||||
on `stable` or in `release` mode.",
|
// on `stable` or in `release` mode.",
|
||||||
url
|
// url
|
||||||
)))
|
// )))
|
||||||
}
|
// }
|
||||||
None => Ok(()),
|
// None => Ok(()),
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
/// Returns the server function registered at the given URL, or `None` if no function is registered at that URL.
|
// /// Returns the server function registered at the given URL, or `None` if no function is registered at that URL.
|
||||||
fn get(url: &str) -> Option<server_fn::ServerFnTraitObj<()>> {
|
// fn get(url: &str) -> Option<server_fn::ServerFnTraitObj<()>> {
|
||||||
REGISTERED_SERVER_FUNCTIONS
|
// REGISTERED_SERVER_FUNCTIONS
|
||||||
.read()
|
// .read()
|
||||||
.ok()
|
// .ok()
|
||||||
.and_then(|fns| fns.get(url).map(|sf| sf.0.clone()))
|
// .and_then(|fns| fns.get(url).map(|sf| sf.0.clone()))
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
/// Returns the server function trait obj registered at the given URL, or `None` if no function is registered at that URL.
|
// /// Returns the server function trait obj registered at the given URL, or `None` if no function is registered at that URL.
|
||||||
fn get_trait_obj(url: &str) -> Option<server_fn::ServerFnTraitObj<()>> {
|
// fn get_trait_obj(url: &str) -> Option<server_fn::ServerFnTraitObj<()>> {
|
||||||
REGISTERED_SERVER_FUNCTIONS
|
// REGISTERED_SERVER_FUNCTIONS
|
||||||
.read()
|
// .read()
|
||||||
.ok()
|
// .ok()
|
||||||
.and_then(|fns| fns.get(url).map(|sf| sf.0.clone()))
|
// .and_then(|fns| fns.get(url).map(|sf| sf.0.clone()))
|
||||||
}
|
// }
|
||||||
/// Return the
|
// /// Return the
|
||||||
fn get_encoding(url: &str) -> Option<Encoding> {
|
// fn get_encoding(url: &str) -> Option<Encoding> {
|
||||||
REGISTERED_SERVER_FUNCTIONS
|
// REGISTERED_SERVER_FUNCTIONS
|
||||||
.read()
|
// .read()
|
||||||
.ok()
|
// .ok()
|
||||||
.and_then(|fns| fns.get(url).map(|sf| sf.encoding()))
|
// .and_then(|fns| fns.get(url).map(|sf| sf.encoding()))
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
/// Returns a list of all registered server functions.
|
// /// Returns a list of all registered server functions.
|
||||||
fn paths_registered() -> Vec<&'static str> {
|
// fn paths_registered() -> Vec<&'static str> {
|
||||||
REGISTERED_SERVER_FUNCTIONS
|
// REGISTERED_SERVER_FUNCTIONS
|
||||||
.read()
|
// .read()
|
||||||
.ok()
|
// .ok()
|
||||||
.map(|fns| fns.keys().cloned().collect())
|
// .map(|fns| fns.keys().cloned().collect())
|
||||||
.unwrap_or_default()
|
// .unwrap_or_default()
|
||||||
}
|
// }
|
||||||
}
|
//}
|
||||||
|
//
|
||||||
#[cfg(any(feature = "ssr", doc))]
|
//#[cfg(any(feature = "ssr", doc))]
|
||||||
/// Errors that can occur when registering a server function.
|
///// Errors that can occur when registering a server function.
|
||||||
#[derive(
|
//#[derive(
|
||||||
thiserror::Error, Debug, Clone, serde::Serialize, serde::Deserialize,
|
// thiserror::Error, Debug, Clone, serde::Serialize, serde::Deserialize,
|
||||||
)]
|
//)]
|
||||||
pub enum ServerRegistrationFnError {
|
//pub enum ServerRegistrationFnError {
|
||||||
/// The server function is already registered.
|
// /// The server function is already registered.
|
||||||
#[error("The server function {0} is already registered")]
|
// #[error("The server function {0} is already registered")]
|
||||||
AlreadyRegistered(String),
|
// AlreadyRegistered(String),
|
||||||
/// The server function registry is poisoned.
|
// /// The server function registry is poisoned.
|
||||||
#[error("The server function registry is poisoned: {0}")]
|
// #[error("The server function registry is poisoned: {0}")]
|
||||||
Poisoned(String),
|
// Poisoned(String),
|
||||||
}
|
//}
|
||||||
|
//
|
||||||
/// Get a ServerFunction struct containing info about the server fn
|
///// Get a ServerFunction struct containing info about the server fn
|
||||||
#[cfg(any(feature = "ssr", doc))]
|
//#[cfg(any(feature = "ssr", doc))]
|
||||||
pub fn server_fn_by_path(path: &str) -> Option<ServerFnTraitObj> {
|
//pub fn server_fn_by_path(path: &str) -> Option<ServerFnTraitObj> {
|
||||||
REGISTERED_SERVER_FUNCTIONS
|
// REGISTERED_SERVER_FUNCTIONS
|
||||||
.read()
|
// .read()
|
||||||
.expect("Server function registry is poisoned")
|
// .expect("Server function registry is poisoned")
|
||||||
.get(path)
|
// .get(path)
|
||||||
.cloned()
|
// .cloned()
|
||||||
}
|
//}
|
||||||
|
//
|
||||||
/// Attempts to find a server function registered at the given path.
|
///// Attempts to find a server function registered at the given path.
|
||||||
///
|
/////
|
||||||
/// This can be used by a server to handle the requests, as in the following example (using `actix-web`)
|
///// This can be used by a server to handle the requests, as in the following example (using `actix-web`)
|
||||||
///
|
/////
|
||||||
/// ```rust, ignore
|
///// ```rust, ignore
|
||||||
/// #[post("{tail:.*}")]
|
///// #[post("{tail:.*}")]
|
||||||
/// async fn handle_server_fns(
|
///// async fn handle_server_fns(
|
||||||
/// req: HttpRequest,
|
///// req: HttpRequest,
|
||||||
/// params: web::Path<String>,
|
///// params: web::Path<String>,
|
||||||
/// body: web::Bytes,
|
///// body: web::Bytes,
|
||||||
/// ) -> impl Responder {
|
///// ) -> impl Responder {
|
||||||
/// let path = params.into_inner();
|
///// let path = params.into_inner();
|
||||||
/// let accept_header = req
|
///// let accept_header = req
|
||||||
/// .headers()
|
///// .headers()
|
||||||
/// .get("Accept")
|
///// .get("Accept")
|
||||||
/// .and_then(|value| value.to_str().ok());
|
///// .and_then(|value| value.to_str().ok());
|
||||||
/// if let Some(server_fn) = server_fn_by_path(path.as_str()) {
|
///// if let Some(server_fn) = server_fn_by_path(path.as_str()) {
|
||||||
/// let query = req.query_string().as_bytes();
|
///// let query = req.query_string().as_bytes();
|
||||||
/// let data = match &server_fn.encoding {
|
///// let data = match &server_fn.encoding {
|
||||||
/// Encoding::Url | Encoding::Cbor => &body,
|
///// Encoding::Url | Encoding::Cbor => &body,
|
||||||
/// Encoding::GetJSON | Encoding::GetCBOR => query,
|
///// Encoding::GetJSON | Encoding::GetCBOR => query,
|
||||||
/// };
|
///// };
|
||||||
/// match (server_fn.trait_obj)(data).await {
|
///// match (server_fn.trait_obj)(data).await {
|
||||||
/// Ok(serialized) => {
|
///// Ok(serialized) => {
|
||||||
/// // if this is Accept: application/json then send a serialized JSON response
|
///// // if this is Accept: application/json then send a serialized JSON response
|
||||||
/// if let Some("application/json") = accept_header {
|
///// if let Some("application/json") = accept_header {
|
||||||
/// HttpResponse::Ok().body(serialized)
|
///// HttpResponse::Ok().body(serialized)
|
||||||
/// }
|
///// }
|
||||||
/// // otherwise, it's probably a <form> submit or something: redirect back to the referrer
|
///// // otherwise, it's probably a <form> submit or something: redirect back to the referrer
|
||||||
/// else {
|
///// else {
|
||||||
/// HttpResponse::SeeOther()
|
///// HttpResponse::SeeOther()
|
||||||
/// .insert_header(("Location", "/"))
|
///// .insert_header(("Location", "/"))
|
||||||
/// .content_type("application/json")
|
///// .content_type("application/json")
|
||||||
/// .body(serialized)
|
///// .body(serialized)
|
||||||
/// }
|
///// }
|
||||||
/// }
|
///// }
|
||||||
/// Err(e) => {
|
///// Err(e) => {
|
||||||
/// eprintln!("server function error: {e:#?}");
|
///// eprintln!("server function error: {e:#?}");
|
||||||
/// HttpResponse::InternalServerError().body(e.to_string())
|
///// HttpResponse::InternalServerError().body(e.to_string())
|
||||||
/// }
|
///// }
|
||||||
/// }
|
///// }
|
||||||
/// } else {
|
///// } else {
|
||||||
/// HttpResponse::BadRequest().body(format!("Could not find a server function at that route."))
|
///// HttpResponse::BadRequest().body(format!("Could not find a server function at that route."))
|
||||||
/// }
|
///// }
|
||||||
/// }
|
///// }
|
||||||
/// ```
|
///// ```
|
||||||
#[cfg(any(feature = "ssr", doc))]
|
//#[cfg(any(feature = "ssr", doc))]
|
||||||
pub fn server_fn_trait_obj_by_path(path: &str) -> Option<ServerFnTraitObj> {
|
//pub fn server_fn_trait_obj_by_path(path: &str) -> Option<ServerFnTraitObj> {
|
||||||
server_fn::server_fn_trait_obj_by_path::<(), LeptosServerFnRegistry>(path)
|
// server_fn::server_fn_trait_obj_by_path::<(), LeptosServerFnRegistry>(path)
|
||||||
.map(ServerFnTraitObj::from_generic_server_fn)
|
// .map(ServerFnTraitObj::from_generic_server_fn)
|
||||||
}
|
//}
|
||||||
|
//
|
||||||
/// Get the Encoding of a server fn if one is registered at that path. Otherwise, return None
|
///// Get the Encoding of a server fn if one is registered at that path. Otherwise, return None
|
||||||
#[cfg(any(feature = "ssr", doc))]
|
//#[cfg(any(feature = "ssr", doc))]
|
||||||
pub fn server_fn_encoding_by_path(path: &str) -> Option<Encoding> {
|
//pub fn server_fn_encoding_by_path(path: &str) -> Option<Encoding> {
|
||||||
server_fn::server_fn_encoding_by_path::<(), LeptosServerFnRegistry>(path)
|
// server_fn::server_fn_encoding_by_path::<(), LeptosServerFnRegistry>(path)
|
||||||
}
|
//}
|
||||||
|
//
|
||||||
/// Returns the set of currently-registered server function paths, for debugging purposes.
|
///// Returns the set of currently-registered server function paths, for debugging purposes.
|
||||||
#[cfg(any(feature = "ssr", doc))]
|
//#[cfg(any(feature = "ssr", doc))]
|
||||||
pub fn server_fns_by_path() -> Vec<&'static str> {
|
//pub fn server_fns_by_path() -> Vec<&'static str> {
|
||||||
server_fn::server_fns_by_path::<(), LeptosServerFnRegistry>()
|
// server_fn::server_fns_by_path::<(), LeptosServerFnRegistry>()
|
||||||
}
|
//}
|
||||||
|
//
|
||||||
/// Defines a "server function." A server function can be called from the server or the client,
|
///// Defines a "server function." A server function can be called from the server or the client,
|
||||||
/// but the body of its code will only be run on the server, i.e., if a crate feature `ssr` is enabled.
|
///// but the body of its code will only be run on the server, i.e., if a crate feature `ssr` is enabled.
|
||||||
///
|
/////
|
||||||
/// (This follows the same convention as the Leptos framework's distinction between `ssr` for server-side rendering,
|
///// (This follows the same convention as the Leptos framework's distinction between `ssr` for server-side rendering,
|
||||||
/// and `csr` and `hydrate` for client-side rendering and hydration, respectively.)
|
///// and `csr` and `hydrate` for client-side rendering and hydration, respectively.)
|
||||||
///
|
/////
|
||||||
/// Server functions are created using the `server` macro.
|
///// Server functions are created using the `server` macro.
|
||||||
///
|
/////
|
||||||
/// The function should be registered by calling `ServerFn::register()`. The set of server functions
|
///// The function should be registered by calling `ServerFn::register()`. The set of server functions
|
||||||
/// can be queried on the server for routing purposes by calling [server_fn_by_path].
|
///// can be queried on the server for routing purposes by calling [server_fn_by_path].
|
||||||
///
|
/////
|
||||||
/// Technically, the trait is implemented on a type that describes the server function's arguments.
|
///// Technically, the trait is implemented on a type that describes the server function's arguments.
|
||||||
pub trait ServerFn: server_fn::ServerFn<()> {
|
//pub trait ServerFn: server_fn::ServerFn<()> {
|
||||||
#[cfg(any(feature = "ssr", doc))]
|
// #[cfg(any(feature = "ssr", doc))]
|
||||||
/// Explicitly registers the server function on platforms that require it,
|
// /// Explicitly registers the server function on platforms that require it,
|
||||||
/// allowing the server to query it by URL.
|
// /// allowing the server to query it by URL.
|
||||||
///
|
// ///
|
||||||
/// Explicit server function registration is no longer required on most platforms
|
// /// Explicit server function registration is no longer required on most platforms
|
||||||
/// (including Linux, macOS, iOS, FreeBSD, Android, and Windows)
|
// /// (including Linux, macOS, iOS, FreeBSD, Android, and Windows)
|
||||||
fn register_explicit() -> Result<(), ServerFnError> {
|
// fn register_explicit() -> Result<(), ServerFnError> {
|
||||||
Self::register_in_explicit::<LeptosServerFnRegistry>()
|
// Self::register_in_explicit::<LeptosServerFnRegistry>()
|
||||||
}
|
// }
|
||||||
}
|
//}
|
||||||
|
//
|
||||||
impl<T> ServerFn for T where T: server_fn::ServerFn<()> {}
|
//impl<T> ServerFn for T where T: server_fn::ServerFn<()> {}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
use crate::{ServerFn, ServerFnError};
|
use server_fn::{ServerFn, ServerFnError};
|
||||||
use leptos_reactive::{
|
use leptos_reactive::{
|
||||||
create_rw_signal, is_suppressing_resource_load, signal_prelude::*,
|
create_rw_signal, is_suppressing_resource_load, signal_prelude::*,
|
||||||
spawn_local, store_value, untrack, ReadSignal, RwSignal, StoredValue,
|
spawn_local, store_value, untrack, ReadSignal, RwSignal, StoredValue,
|
||||||
|
|
|
@ -32,7 +32,7 @@ multer = { version = "3", optional = true }
|
||||||
|
|
||||||
## output encodings
|
## output encodings
|
||||||
# serde
|
# serde
|
||||||
serde_json = { version = "1", optional = true }
|
serde_json = "1"
|
||||||
futures = "0.3"
|
futures = "0.3"
|
||||||
http = { version = "1", optional = true }
|
http = { version = "1", optional = true }
|
||||||
ciborium = { version = "0.2", optional = true }
|
ciborium = { version = "0.2", optional = true }
|
||||||
|
@ -81,7 +81,7 @@ browser = [
|
||||||
"dep:wasm-streams",
|
"dep:wasm-streams",
|
||||||
"dep:wasm-bindgen-futures",
|
"dep:wasm-bindgen-futures",
|
||||||
]
|
]
|
||||||
json = ["dep:serde_json"]
|
#json = ["dep:serde_json"]
|
||||||
multipart = ["dep:multer"]
|
multipart = ["dep:multer"]
|
||||||
url = ["dep:serde_qs"]
|
url = ["dep:serde_qs"]
|
||||||
cbor = ["dep:ciborium"]
|
cbor = ["dep:ciborium"]
|
||||||
|
|
|
@ -1,8 +1,55 @@
|
||||||
use core::fmt::{self, Display};
|
use core::fmt::Display;
|
||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::{error, fmt, ops, sync::Arc};
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
|
/// This is a result type into which any error can be converted,
|
||||||
|
/// and which can be used directly in your `view`.
|
||||||
|
///
|
||||||
|
/// All errors will be stored as [`struct@Error`].
|
||||||
|
pub type Result<T, E = Error> = core::result::Result<T, E>;
|
||||||
|
|
||||||
|
/// A generic wrapper for any error.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
#[repr(transparent)]
|
||||||
|
pub struct Error(Arc<dyn error::Error + Send + Sync>);
|
||||||
|
|
||||||
|
impl Error {
|
||||||
|
/// Converts the wrapper into the inner reference-counted error.
|
||||||
|
pub fn into_inner(self) -> Arc<dyn error::Error + Send + Sync> {
|
||||||
|
Arc::clone(&self.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ops::Deref for Error {
|
||||||
|
type Target = Arc<dyn error::Error + Send + Sync>;
|
||||||
|
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for Error {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
write!(f, "{}", self.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> From<T> for Error
|
||||||
|
where
|
||||||
|
T: std::error::Error + Send + Sync + 'static,
|
||||||
|
{
|
||||||
|
fn from(value: T) -> Self {
|
||||||
|
Error(Arc::new(value))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<ServerFnError> for Error {
|
||||||
|
fn from(e: ServerFnError) -> Self {
|
||||||
|
Error(Arc::new(ServerFnErrorErr::from(e)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// An empty value indicating that there is no custom error type associated
|
/// An empty value indicating that there is no custom error type associated
|
||||||
/// with this server function.
|
/// with this server function.
|
||||||
#[derive(Debug, Deserialize, Serialize)]
|
#[derive(Debug, Deserialize, Serialize)]
|
||||||
|
@ -83,7 +130,12 @@ impl<E: Display + Clone> ViaError<E> for &WrapError<E> {
|
||||||
impl<E> ViaError<E> for WrapError<E> {
|
impl<E> ViaError<E> for WrapError<E> {
|
||||||
#[track_caller]
|
#[track_caller]
|
||||||
fn to_server_error(&self) -> ServerFnError<E> {
|
fn to_server_error(&self) -> ServerFnError<E> {
|
||||||
panic!("At {}, you call `to_server_error()` or use `server_fn_error!` with a value that does not implement `Clone` and either `Error` or `Display`.", std::panic::Location::caller());
|
panic!(
|
||||||
|
"At {}, you call `to_server_error()` or use `server_fn_error!` \
|
||||||
|
with a value that does not implement `Clone` and either `Error` \
|
||||||
|
or `Display`.",
|
||||||
|
std::panic::Location::caller()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -129,19 +181,24 @@ where
|
||||||
f,
|
f,
|
||||||
"{}",
|
"{}",
|
||||||
match self {
|
match self {
|
||||||
ServerFnError::Registration(s) =>
|
ServerFnError::Registration(s) => format!(
|
||||||
format!("error while trying to register the server function: {s}"),
|
"error while trying to register the server function: {s}"
|
||||||
ServerFnError::Request(s) =>
|
),
|
||||||
format!("error reaching server to call server function: {s}"),
|
ServerFnError::Request(s) => format!(
|
||||||
ServerFnError::ServerError(s) => format!("error running server function: {s}"),
|
"error reaching server to call server function: {s}"
|
||||||
|
),
|
||||||
|
ServerFnError::ServerError(s) =>
|
||||||
|
format!("error running server function: {s}"),
|
||||||
ServerFnError::Deserialization(s) =>
|
ServerFnError::Deserialization(s) =>
|
||||||
format!("error deserializing server function results: {s}"),
|
format!("error deserializing server function results: {s}"),
|
||||||
ServerFnError::Serialization(s) =>
|
ServerFnError::Serialization(s) =>
|
||||||
format!("error serializing server function arguments: {s}"),
|
format!("error serializing server function arguments: {s}"),
|
||||||
ServerFnError::Args(s) =>
|
ServerFnError::Args(s) => format!(
|
||||||
format!("error deserializing server function arguments: {s}"),
|
"error deserializing server function arguments: {s}"
|
||||||
|
),
|
||||||
ServerFnError::MissingArg(s) => format!("missing argument {s}"),
|
ServerFnError::MissingArg(s) => format!("missing argument {s}"),
|
||||||
ServerFnError::Response(s) => format!("error generating HTTP response: {s}"),
|
ServerFnError::Response(s) =>
|
||||||
|
format!("error generating HTTP response: {s}"),
|
||||||
ServerFnError::WrappedServerError(e) => format!("{}", e),
|
ServerFnError::WrappedServerError(e) => format!("{}", e),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
@ -202,14 +259,26 @@ pub enum ServerFnErrorErr<E = NoCustomError> {
|
||||||
impl<CustErr> From<ServerFnError<CustErr>> for ServerFnErrorErr<CustErr> {
|
impl<CustErr> From<ServerFnError<CustErr>> for ServerFnErrorErr<CustErr> {
|
||||||
fn from(value: ServerFnError<CustErr>) -> Self {
|
fn from(value: ServerFnError<CustErr>) -> Self {
|
||||||
match value {
|
match value {
|
||||||
ServerFnError::Registration(value) => ServerFnErrorErr::Registration(value),
|
ServerFnError::Registration(value) => {
|
||||||
|
ServerFnErrorErr::Registration(value)
|
||||||
|
}
|
||||||
ServerFnError::Request(value) => ServerFnErrorErr::Request(value),
|
ServerFnError::Request(value) => ServerFnErrorErr::Request(value),
|
||||||
ServerFnError::ServerError(value) => ServerFnErrorErr::ServerError(value),
|
ServerFnError::ServerError(value) => {
|
||||||
ServerFnError::Deserialization(value) => ServerFnErrorErr::Deserialization(value),
|
ServerFnErrorErr::ServerError(value)
|
||||||
ServerFnError::Serialization(value) => ServerFnErrorErr::Serialization(value),
|
}
|
||||||
|
ServerFnError::Deserialization(value) => {
|
||||||
|
ServerFnErrorErr::Deserialization(value)
|
||||||
|
}
|
||||||
|
ServerFnError::Serialization(value) => {
|
||||||
|
ServerFnErrorErr::Serialization(value)
|
||||||
|
}
|
||||||
ServerFnError::Args(value) => ServerFnErrorErr::Args(value),
|
ServerFnError::Args(value) => ServerFnErrorErr::Args(value),
|
||||||
ServerFnError::MissingArg(value) => ServerFnErrorErr::MissingArg(value),
|
ServerFnError::MissingArg(value) => {
|
||||||
ServerFnError::WrappedServerError(value) => ServerFnErrorErr::WrappedServerError(value),
|
ServerFnErrorErr::MissingArg(value)
|
||||||
|
}
|
||||||
|
ServerFnError::WrappedServerError(value) => {
|
||||||
|
ServerFnErrorErr::WrappedServerError(value)
|
||||||
|
}
|
||||||
ServerFnError::Response(value) => ServerFnErrorErr::Response(value),
|
ServerFnError::Response(value) => ServerFnErrorErr::Response(value),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue