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": {
|
||||
"locked": {
|
||||
"lastModified": 1703637592,
|
||||
"narHash": "sha256-8MXjxU0RfFfzl57Zy3OfXCITS0qWDNLzlBAdwxGZwfY=",
|
||||
"lastModified": 1703961334,
|
||||
"narHash": "sha256-M1mV/Cq+pgjk0rt6VxoyyD+O8cOUiai8t9Q6Yyq4noY=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "cfc3698c31b1fb9cdcf10f36c9643460264d0ca8",
|
||||
"rev": "b0d36bd0a420ecee3bc916c91886caca87c894e9",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
@ -81,11 +81,11 @@
|
|||
"nixpkgs": "nixpkgs_2"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1703902408,
|
||||
"narHash": "sha256-qXdWvu+tlgNjeoz8yQMRKSom6QyRROfgpmeOhwbujqw=",
|
||||
"lastModified": 1704075545,
|
||||
"narHash": "sha256-L3zgOuVKhPjKsVLc3yTm2YJ6+BATyZBury7wnhyc8QU=",
|
||||
"owner": "oxalica",
|
||||
"repo": "rust-overlay",
|
||||
"rev": "319f57cd2c34348c55970a4bf2b35afe82088681",
|
||||
"rev": "a0df72e106322b67e9c6e591fe870380bd0da0d5",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
|
|
@ -50,17 +50,21 @@ pub fn server_impl(args: TokenStream, s: TokenStream) -> TokenStream {
|
|||
if args.prefix.is_none() {
|
||||
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
|
||||
if args.encoding.is_none() {
|
||||
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(
|
||||
quote::quote!(#args),
|
||||
mapped_body,
|
||||
syn::parse_quote!(::leptos::leptos_server::ServerFnTraitObj),
|
||||
None,
|
||||
Some(syn::parse_quote!(::leptos::server_fn)),
|
||||
&args_prefix,
|
||||
) {
|
||||
Err(e) => e.to_compile_error().into(),
|
||||
Ok(s) => s.to_token_stream().into(),
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
use crate::{ServerFn, ServerFnError};
|
||||
//use crate::{ServerFn, ServerFnError};
|
||||
use leptos_reactive::{
|
||||
batch, create_rw_signal, is_suppressing_resource_load, signal_prelude::*,
|
||||
spawn_local, store_value, ReadSignal, RwSignal, StoredValue,
|
||||
};
|
||||
use server_fn::{ServerFn, ServerFnError};
|
||||
use std::{cell::Cell, future::Future, pin::Pin, rc::Rc};
|
||||
|
||||
/// 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)]
|
||||
|
||||
//! # Leptos Server 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.
|
||||
//!
|
||||
//! 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
|
||||
//! 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
|
||||
//! 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.
|
||||
//!
|
||||
//! 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
|
||||
//! 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),
|
||||
//! 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,
|
||||
//! 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
|
||||
//! 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
|
||||
//! as it does on the server.)
|
||||
//!
|
||||
//! ### `#[server]`
|
||||
//!
|
||||
//! 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
|
||||
//! crate that is enabled).
|
||||
//!
|
||||
//! ```rust,ignore
|
||||
//! use leptos::*;
|
||||
//! #[server(ReadFromDB)]
|
||||
//! async fn read_posts(how_many: usize, query: String) -> Result<Vec<Posts>, ServerFnError> {
|
||||
//! // do some server-only work here to access the database
|
||||
//! let posts = todo!();;
|
||||
//! Ok(posts)
|
||||
//! }
|
||||
//!
|
||||
//! // call the function
|
||||
//! spawn_local(async {
|
||||
//! let posts = read_posts(3, "my search".to_string()).await;
|
||||
//! log::debug!("posts = {posts:#?}");
|
||||
//! });
|
||||
//! ```
|
||||
//!
|
||||
//! 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">`.
|
||||
//!
|
||||
//! Here’s what you need to remember:
|
||||
//! - **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
|
||||
//! function call.
|
||||
//! - **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
|
||||
//! network call are fallible.
|
||||
//! - **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
|
||||
//! 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`
|
||||
//! 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
|
||||
//! `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
|
||||
//! 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.
|
||||
//!
|
||||
//! ## 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
|
||||
//! 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:
|
||||
//!
|
||||
//! ```rust,ignore
|
||||
//! #[server(AddTodo, "/api", "Url")]
|
||||
//! #[server(AddTodo, "/api", "GetJson")]
|
||||
//! #[server(AddTodo, "/api", "Cbor")]
|
||||
//! #[server(AddTodo, "/api", "GetCbor")]
|
||||
//! ```
|
||||
//!
|
||||
//! The four options use different combinations of HTTP verbs and encoding methods:
|
||||
//!
|
||||
//! | Name | Method | Request | Response |
|
||||
//! | ----------------- | ------ | ----------- | -------- |
|
||||
//! | **Url** (default) | POST | URL encoded | JSON |
|
||||
//! | **GetJson** | GET | URL encoded | JSON |
|
||||
//! | **Cbor** | POST | CBOR | CBOR |
|
||||
//! | **GetCbor** | GET | URL encoded | CBOR |
|
||||
//!
|
||||
//! 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` requests can be.
|
||||
//! - Plain text (arguments sent with URL/form encoding, results sent as JSON) or a binary format (CBOR, encoded as a base64
|
||||
//! string)?
|
||||
//!
|
||||
//! ## 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
|
||||
//! methods like `DELETE` to delete an item from a database, and many devs are accustomed to sending data to APIs in the
|
||||
//! JSON format.
|
||||
//!
|
||||
//! 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
|
||||
//! 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
|
||||
//! 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
|
||||
//! your app is not available.
|
||||
////! # Leptos Server 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.
|
||||
////!
|
||||
////! 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
|
||||
////! 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
|
||||
////! 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.
|
||||
////!
|
||||
////! 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
|
||||
////! 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),
|
||||
////! 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,
|
||||
////! 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
|
||||
////! 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
|
||||
////! as it does on the server.)
|
||||
////!
|
||||
////! ### `#[server]`
|
||||
////!
|
||||
////! 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
|
||||
////! crate that is enabled).
|
||||
////!
|
||||
////! ```rust,ignore
|
||||
////! use leptos::*;
|
||||
////! #[server(ReadFromDB)]
|
||||
////! async fn read_posts(how_many: usize, query: String) -> Result<Vec<Posts>, ServerFnError> {
|
||||
////! // do some server-only work here to access the database
|
||||
////! let posts = todo!();;
|
||||
////! Ok(posts)
|
||||
////! }
|
||||
////!
|
||||
////! // call the function
|
||||
////! spawn_local(async {
|
||||
////! let posts = read_posts(3, "my search".to_string()).await;
|
||||
////! log::debug!("posts = {posts:#?}");
|
||||
////! });
|
||||
////! ```
|
||||
////!
|
||||
////! 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">`.
|
||||
////!
|
||||
////! Here’s what you need to remember:
|
||||
////! - **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
|
||||
////! function call.
|
||||
////! - **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
|
||||
////! network call are fallible.
|
||||
////! - **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
|
||||
////! 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`
|
||||
////! 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
|
||||
////! `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
|
||||
////! 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.
|
||||
////!
|
||||
////! ## 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
|
||||
////! 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:
|
||||
////!
|
||||
////! ```rust,ignore
|
||||
////! #[server(AddTodo, "/api", "Url")]
|
||||
////! #[server(AddTodo, "/api", "GetJson")]
|
||||
////! #[server(AddTodo, "/api", "Cbor")]
|
||||
////! #[server(AddTodo, "/api", "GetCbor")]
|
||||
////! ```
|
||||
////!
|
||||
////! The four options use different combinations of HTTP verbs and encoding methods:
|
||||
////!
|
||||
////! | Name | Method | Request | Response |
|
||||
////! | ----------------- | ------ | ----------- | -------- |
|
||||
////! | **Url** (default) | POST | URL encoded | JSON |
|
||||
////! | **GetJson** | GET | URL encoded | JSON |
|
||||
////! | **Cbor** | POST | CBOR | CBOR |
|
||||
////! | **GetCbor** | GET | URL encoded | CBOR |
|
||||
////!
|
||||
////! 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` requests can be.
|
||||
////! - Plain text (arguments sent with URL/form encoding, results sent as JSON) or a binary format (CBOR, encoded as a base64
|
||||
////! string)?
|
||||
////!
|
||||
////! ## 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
|
||||
////! methods like `DELETE` to delete an item from a database, and many devs are accustomed to sending data to APIs in the
|
||||
////! JSON format.
|
||||
////!
|
||||
////! 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
|
||||
////! 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
|
||||
////! 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
|
||||
////! your app is not available.
|
||||
|
||||
pub use server_fn::{
|
||||
error::ServerFnErrorErr, Encoding, Payload, ServerFnError,
|
||||
error::ServerFnErrorErr, ServerFnError,
|
||||
};
|
||||
|
||||
mod action;
|
||||
|
@ -123,250 +123,250 @@ pub use action::*;
|
|||
pub use multi_action::*;
|
||||
extern crate tracing;
|
||||
|
||||
#[cfg(any(feature = "ssr", doc))]
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
sync::{Arc, RwLock},
|
||||
};
|
||||
|
||||
#[cfg(any(feature = "ssr", doc))]
|
||||
/// A concrete type for a server function.
|
||||
#[derive(Clone)]
|
||||
pub struct ServerFnTraitObj(pub server_fn::ServerFnTraitObj<()>);
|
||||
|
||||
#[cfg(any(feature = "ssr", doc))]
|
||||
impl std::ops::Deref for ServerFnTraitObj {
|
||||
type Target = server_fn::ServerFnTraitObj<()>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "ssr", doc))]
|
||||
impl std::ops::DerefMut for ServerFnTraitObj {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.0
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "ssr", doc))]
|
||||
impl ServerFnTraitObj {
|
||||
/// Create a new `ServerFnTraitObj` from a `server_fn::ServerFnTraitObj`.
|
||||
pub const fn from_generic_server_fn(
|
||||
server_fn: server_fn::ServerFnTraitObj<()>,
|
||||
) -> Self {
|
||||
Self(server_fn)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
inventory::collect!(ServerFnTraitObj);
|
||||
|
||||
#[allow(unused)]
|
||||
type ServerFunction = server_fn::ServerFnTraitObj<()>;
|
||||
|
||||
#[cfg(any(feature = "ssr", doc))]
|
||||
lazy_static::lazy_static! {
|
||||
static ref REGISTERED_SERVER_FUNCTIONS: Arc<RwLock<HashMap<&'static str, ServerFnTraitObj>>> = {
|
||||
let mut map = HashMap::new();
|
||||
for server_fn in inventory::iter::<ServerFnTraitObj> {
|
||||
map.insert(server_fn.0.url(), server_fn.clone());
|
||||
}
|
||||
Arc::new(RwLock::new(map))
|
||||
};
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "ssr", doc))]
|
||||
/// The registry of all Leptos server functions.
|
||||
pub struct LeptosServerFnRegistry;
|
||||
|
||||
#[cfg(any(feature = "ssr", doc))]
|
||||
impl server_fn::ServerFunctionRegistry<()> for LeptosServerFnRegistry {
|
||||
type Error = ServerRegistrationFnError;
|
||||
|
||||
/// 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,
|
||||
/// you should register server functions by calling this `T::register_explicit()`.
|
||||
fn register_explicit(
|
||||
prefix: &'static str,
|
||||
url: &'static str,
|
||||
server_function: server_fn::SerializedFnTraitObj<()>,
|
||||
encoding: Encoding,
|
||||
) -> Result<(), Self::Error> {
|
||||
// store it in the hashmap
|
||||
let mut func_write = REGISTERED_SERVER_FUNCTIONS
|
||||
.write()
|
||||
.map_err(|e| ServerRegistrationFnError::Poisoned(e.to_string()))?;
|
||||
let prev = func_write.insert(
|
||||
url,
|
||||
ServerFnTraitObj(server_fn::ServerFnTraitObj::new(
|
||||
prefix,
|
||||
url,
|
||||
encoding,
|
||||
server_function,
|
||||
)),
|
||||
);
|
||||
|
||||
// if there was already a server function with this key,
|
||||
// return Err
|
||||
match prev {
|
||||
Some(_) => {
|
||||
Err(ServerRegistrationFnError::AlreadyRegistered(format!(
|
||||
"There was already a server function registered at {:?}. \
|
||||
This can happen if you use the same server function name \
|
||||
in two different modules
|
||||
on `stable` or in `release` mode.",
|
||||
url
|
||||
)))
|
||||
}
|
||||
None => Ok(()),
|
||||
}
|
||||
}
|
||||
|
||||
/// 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<()>> {
|
||||
REGISTERED_SERVER_FUNCTIONS
|
||||
.read()
|
||||
.ok()
|
||||
.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.
|
||||
fn get_trait_obj(url: &str) -> Option<server_fn::ServerFnTraitObj<()>> {
|
||||
REGISTERED_SERVER_FUNCTIONS
|
||||
.read()
|
||||
.ok()
|
||||
.and_then(|fns| fns.get(url).map(|sf| sf.0.clone()))
|
||||
}
|
||||
/// Return the
|
||||
fn get_encoding(url: &str) -> Option<Encoding> {
|
||||
REGISTERED_SERVER_FUNCTIONS
|
||||
.read()
|
||||
.ok()
|
||||
.and_then(|fns| fns.get(url).map(|sf| sf.encoding()))
|
||||
}
|
||||
|
||||
/// Returns a list of all registered server functions.
|
||||
fn paths_registered() -> Vec<&'static str> {
|
||||
REGISTERED_SERVER_FUNCTIONS
|
||||
.read()
|
||||
.ok()
|
||||
.map(|fns| fns.keys().cloned().collect())
|
||||
.unwrap_or_default()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "ssr", doc))]
|
||||
/// Errors that can occur when registering a server function.
|
||||
#[derive(
|
||||
thiserror::Error, Debug, Clone, serde::Serialize, serde::Deserialize,
|
||||
)]
|
||||
pub enum ServerRegistrationFnError {
|
||||
/// The server function is already registered.
|
||||
#[error("The server function {0} is already registered")]
|
||||
AlreadyRegistered(String),
|
||||
/// The server function registry is poisoned.
|
||||
#[error("The server function registry is poisoned: {0}")]
|
||||
Poisoned(String),
|
||||
}
|
||||
|
||||
/// Get a ServerFunction struct containing info about the server fn
|
||||
#[cfg(any(feature = "ssr", doc))]
|
||||
pub fn server_fn_by_path(path: &str) -> Option<ServerFnTraitObj> {
|
||||
REGISTERED_SERVER_FUNCTIONS
|
||||
.read()
|
||||
.expect("Server function registry is poisoned")
|
||||
.get(path)
|
||||
.cloned()
|
||||
}
|
||||
|
||||
/// 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`)
|
||||
///
|
||||
/// ```rust, ignore
|
||||
/// #[post("{tail:.*}")]
|
||||
/// async fn handle_server_fns(
|
||||
/// req: HttpRequest,
|
||||
/// params: web::Path<String>,
|
||||
/// body: web::Bytes,
|
||||
/// ) -> impl Responder {
|
||||
/// let path = params.into_inner();
|
||||
/// let accept_header = req
|
||||
/// .headers()
|
||||
/// .get("Accept")
|
||||
/// .and_then(|value| value.to_str().ok());
|
||||
/// if let Some(server_fn) = server_fn_by_path(path.as_str()) {
|
||||
/// let query = req.query_string().as_bytes();
|
||||
/// let data = match &server_fn.encoding {
|
||||
/// Encoding::Url | Encoding::Cbor => &body,
|
||||
/// Encoding::GetJSON | Encoding::GetCBOR => query,
|
||||
/// };
|
||||
/// match (server_fn.trait_obj)(data).await {
|
||||
/// Ok(serialized) => {
|
||||
/// // if this is Accept: application/json then send a serialized JSON response
|
||||
/// if let Some("application/json") = accept_header {
|
||||
/// HttpResponse::Ok().body(serialized)
|
||||
/// }
|
||||
/// // otherwise, it's probably a <form> submit or something: redirect back to the referrer
|
||||
/// else {
|
||||
/// HttpResponse::SeeOther()
|
||||
/// .insert_header(("Location", "/"))
|
||||
/// .content_type("application/json")
|
||||
/// .body(serialized)
|
||||
/// }
|
||||
/// }
|
||||
/// Err(e) => {
|
||||
/// eprintln!("server function error: {e:#?}");
|
||||
/// HttpResponse::InternalServerError().body(e.to_string())
|
||||
/// }
|
||||
/// }
|
||||
/// } else {
|
||||
/// HttpResponse::BadRequest().body(format!("Could not find a server function at that route."))
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
#[cfg(any(feature = "ssr", doc))]
|
||||
pub fn server_fn_trait_obj_by_path(path: &str) -> Option<ServerFnTraitObj> {
|
||||
server_fn::server_fn_trait_obj_by_path::<(), LeptosServerFnRegistry>(path)
|
||||
.map(ServerFnTraitObj::from_generic_server_fn)
|
||||
}
|
||||
|
||||
/// Get the Encoding of a server fn if one is registered at that path. Otherwise, return None
|
||||
#[cfg(any(feature = "ssr", doc))]
|
||||
pub fn server_fn_encoding_by_path(path: &str) -> Option<Encoding> {
|
||||
server_fn::server_fn_encoding_by_path::<(), LeptosServerFnRegistry>(path)
|
||||
}
|
||||
|
||||
/// Returns the set of currently-registered server function paths, for debugging purposes.
|
||||
#[cfg(any(feature = "ssr", doc))]
|
||||
pub fn server_fns_by_path() -> Vec<&'static str> {
|
||||
server_fn::server_fns_by_path::<(), LeptosServerFnRegistry>()
|
||||
}
|
||||
|
||||
/// 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.
|
||||
///
|
||||
/// (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.)
|
||||
///
|
||||
/// Server functions are created using the `server` macro.
|
||||
///
|
||||
/// 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].
|
||||
///
|
||||
/// Technically, the trait is implemented on a type that describes the server function's arguments.
|
||||
pub trait ServerFn: server_fn::ServerFn<()> {
|
||||
#[cfg(any(feature = "ssr", doc))]
|
||||
/// Explicitly registers the server function on platforms that require it,
|
||||
/// allowing the server to query it by URL.
|
||||
///
|
||||
/// Explicit server function registration is no longer required on most platforms
|
||||
/// (including Linux, macOS, iOS, FreeBSD, Android, and Windows)
|
||||
fn register_explicit() -> Result<(), ServerFnError> {
|
||||
Self::register_in_explicit::<LeptosServerFnRegistry>()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> ServerFn for T where T: server_fn::ServerFn<()> {}
|
||||
//#[cfg(any(feature = "ssr", doc))]
|
||||
//use std::{
|
||||
// collections::HashMap,
|
||||
// sync::{Arc, RwLock},
|
||||
//};
|
||||
//
|
||||
//#[cfg(any(feature = "ssr", doc))]
|
||||
///// A concrete type for a server function.
|
||||
//#[derive(Clone)]
|
||||
//pub struct ServerFnTraitObj(pub server_fn::ServerFnTraitObj<()>);
|
||||
//
|
||||
//#[cfg(any(feature = "ssr", doc))]
|
||||
//impl std::ops::Deref for ServerFnTraitObj {
|
||||
// type Target = server_fn::ServerFnTraitObj<()>;
|
||||
//
|
||||
// fn deref(&self) -> &Self::Target {
|
||||
// &self.0
|
||||
// }
|
||||
//}
|
||||
//
|
||||
//#[cfg(any(feature = "ssr", doc))]
|
||||
//impl std::ops::DerefMut for ServerFnTraitObj {
|
||||
// fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
// &mut self.0
|
||||
// }
|
||||
//}
|
||||
//
|
||||
//#[cfg(any(feature = "ssr", doc))]
|
||||
//impl ServerFnTraitObj {
|
||||
// /// Create a new `ServerFnTraitObj` from a `server_fn::ServerFnTraitObj`.
|
||||
// pub const fn from_generic_server_fn(
|
||||
// server_fn: server_fn::ServerFnTraitObj<()>,
|
||||
// ) -> Self {
|
||||
// Self(server_fn)
|
||||
// }
|
||||
//}
|
||||
//
|
||||
//#[cfg(feature = "ssr")]
|
||||
//inventory::collect!(ServerFnTraitObj);
|
||||
//
|
||||
//#[allow(unused)]
|
||||
//type ServerFunction = server_fn::ServerFnTraitObj<()>;
|
||||
//
|
||||
//#[cfg(any(feature = "ssr", doc))]
|
||||
//lazy_static::lazy_static! {
|
||||
// static ref REGISTERED_SERVER_FUNCTIONS: Arc<RwLock<HashMap<&'static str, ServerFnTraitObj>>> = {
|
||||
// let mut map = HashMap::new();
|
||||
// for server_fn in inventory::iter::<ServerFnTraitObj> {
|
||||
// map.insert(server_fn.0.url(), server_fn.clone());
|
||||
// }
|
||||
// Arc::new(RwLock::new(map))
|
||||
// };
|
||||
//}
|
||||
//
|
||||
//#[cfg(any(feature = "ssr", doc))]
|
||||
///// The registry of all Leptos server functions.
|
||||
//pub struct LeptosServerFnRegistry;
|
||||
//
|
||||
//#[cfg(any(feature = "ssr", doc))]
|
||||
//impl server_fn::ServerFunctionRegistry<()> for LeptosServerFnRegistry {
|
||||
// type Error = ServerRegistrationFnError;
|
||||
//
|
||||
// /// 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,
|
||||
// /// you should register server functions by calling this `T::register_explicit()`.
|
||||
// fn register_explicit(
|
||||
// prefix: &'static str,
|
||||
// url: &'static str,
|
||||
// server_function: server_fn::SerializedFnTraitObj<()>,
|
||||
// encoding: Encoding,
|
||||
// ) -> Result<(), Self::Error> {
|
||||
// // store it in the hashmap
|
||||
// let mut func_write = REGISTERED_SERVER_FUNCTIONS
|
||||
// .write()
|
||||
// .map_err(|e| ServerRegistrationFnError::Poisoned(e.to_string()))?;
|
||||
// let prev = func_write.insert(
|
||||
// url,
|
||||
// ServerFnTraitObj(server_fn::ServerFnTraitObj::new(
|
||||
// prefix,
|
||||
// url,
|
||||
// encoding,
|
||||
// server_function,
|
||||
// )),
|
||||
// );
|
||||
//
|
||||
// // if there was already a server function with this key,
|
||||
// // return Err
|
||||
// match prev {
|
||||
// Some(_) => {
|
||||
// Err(ServerRegistrationFnError::AlreadyRegistered(format!(
|
||||
// "There was already a server function registered at {:?}. \
|
||||
// This can happen if you use the same server function name \
|
||||
// in two different modules
|
||||
// on `stable` or in `release` mode.",
|
||||
// url
|
||||
// )))
|
||||
// }
|
||||
// None => Ok(()),
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// /// 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<()>> {
|
||||
// REGISTERED_SERVER_FUNCTIONS
|
||||
// .read()
|
||||
// .ok()
|
||||
// .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.
|
||||
// fn get_trait_obj(url: &str) -> Option<server_fn::ServerFnTraitObj<()>> {
|
||||
// REGISTERED_SERVER_FUNCTIONS
|
||||
// .read()
|
||||
// .ok()
|
||||
// .and_then(|fns| fns.get(url).map(|sf| sf.0.clone()))
|
||||
// }
|
||||
// /// Return the
|
||||
// fn get_encoding(url: &str) -> Option<Encoding> {
|
||||
// REGISTERED_SERVER_FUNCTIONS
|
||||
// .read()
|
||||
// .ok()
|
||||
// .and_then(|fns| fns.get(url).map(|sf| sf.encoding()))
|
||||
// }
|
||||
//
|
||||
// /// Returns a list of all registered server functions.
|
||||
// fn paths_registered() -> Vec<&'static str> {
|
||||
// REGISTERED_SERVER_FUNCTIONS
|
||||
// .read()
|
||||
// .ok()
|
||||
// .map(|fns| fns.keys().cloned().collect())
|
||||
// .unwrap_or_default()
|
||||
// }
|
||||
//}
|
||||
//
|
||||
//#[cfg(any(feature = "ssr", doc))]
|
||||
///// Errors that can occur when registering a server function.
|
||||
//#[derive(
|
||||
// thiserror::Error, Debug, Clone, serde::Serialize, serde::Deserialize,
|
||||
//)]
|
||||
//pub enum ServerRegistrationFnError {
|
||||
// /// The server function is already registered.
|
||||
// #[error("The server function {0} is already registered")]
|
||||
// AlreadyRegistered(String),
|
||||
// /// The server function registry is poisoned.
|
||||
// #[error("The server function registry is poisoned: {0}")]
|
||||
// Poisoned(String),
|
||||
//}
|
||||
//
|
||||
///// Get a ServerFunction struct containing info about the server fn
|
||||
//#[cfg(any(feature = "ssr", doc))]
|
||||
//pub fn server_fn_by_path(path: &str) -> Option<ServerFnTraitObj> {
|
||||
// REGISTERED_SERVER_FUNCTIONS
|
||||
// .read()
|
||||
// .expect("Server function registry is poisoned")
|
||||
// .get(path)
|
||||
// .cloned()
|
||||
//}
|
||||
//
|
||||
///// 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`)
|
||||
/////
|
||||
///// ```rust, ignore
|
||||
///// #[post("{tail:.*}")]
|
||||
///// async fn handle_server_fns(
|
||||
///// req: HttpRequest,
|
||||
///// params: web::Path<String>,
|
||||
///// body: web::Bytes,
|
||||
///// ) -> impl Responder {
|
||||
///// let path = params.into_inner();
|
||||
///// let accept_header = req
|
||||
///// .headers()
|
||||
///// .get("Accept")
|
||||
///// .and_then(|value| value.to_str().ok());
|
||||
///// if let Some(server_fn) = server_fn_by_path(path.as_str()) {
|
||||
///// let query = req.query_string().as_bytes();
|
||||
///// let data = match &server_fn.encoding {
|
||||
///// Encoding::Url | Encoding::Cbor => &body,
|
||||
///// Encoding::GetJSON | Encoding::GetCBOR => query,
|
||||
///// };
|
||||
///// match (server_fn.trait_obj)(data).await {
|
||||
///// Ok(serialized) => {
|
||||
///// // if this is Accept: application/json then send a serialized JSON response
|
||||
///// if let Some("application/json") = accept_header {
|
||||
///// HttpResponse::Ok().body(serialized)
|
||||
///// }
|
||||
///// // otherwise, it's probably a <form> submit or something: redirect back to the referrer
|
||||
///// else {
|
||||
///// HttpResponse::SeeOther()
|
||||
///// .insert_header(("Location", "/"))
|
||||
///// .content_type("application/json")
|
||||
///// .body(serialized)
|
||||
///// }
|
||||
///// }
|
||||
///// Err(e) => {
|
||||
///// eprintln!("server function error: {e:#?}");
|
||||
///// HttpResponse::InternalServerError().body(e.to_string())
|
||||
///// }
|
||||
///// }
|
||||
///// } else {
|
||||
///// HttpResponse::BadRequest().body(format!("Could not find a server function at that route."))
|
||||
///// }
|
||||
///// }
|
||||
///// ```
|
||||
//#[cfg(any(feature = "ssr", doc))]
|
||||
//pub fn server_fn_trait_obj_by_path(path: &str) -> Option<ServerFnTraitObj> {
|
||||
// server_fn::server_fn_trait_obj_by_path::<(), LeptosServerFnRegistry>(path)
|
||||
// .map(ServerFnTraitObj::from_generic_server_fn)
|
||||
//}
|
||||
//
|
||||
///// Get the Encoding of a server fn if one is registered at that path. Otherwise, return None
|
||||
//#[cfg(any(feature = "ssr", doc))]
|
||||
//pub fn server_fn_encoding_by_path(path: &str) -> Option<Encoding> {
|
||||
// server_fn::server_fn_encoding_by_path::<(), LeptosServerFnRegistry>(path)
|
||||
//}
|
||||
//
|
||||
///// Returns the set of currently-registered server function paths, for debugging purposes.
|
||||
//#[cfg(any(feature = "ssr", doc))]
|
||||
//pub fn server_fns_by_path() -> Vec<&'static str> {
|
||||
// server_fn::server_fns_by_path::<(), LeptosServerFnRegistry>()
|
||||
//}
|
||||
//
|
||||
///// 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.
|
||||
/////
|
||||
///// (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.)
|
||||
/////
|
||||
///// Server functions are created using the `server` macro.
|
||||
/////
|
||||
///// 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].
|
||||
/////
|
||||
///// Technically, the trait is implemented on a type that describes the server function's arguments.
|
||||
//pub trait ServerFn: server_fn::ServerFn<()> {
|
||||
// #[cfg(any(feature = "ssr", doc))]
|
||||
// /// Explicitly registers the server function on platforms that require it,
|
||||
// /// allowing the server to query it by URL.
|
||||
// ///
|
||||
// /// Explicit server function registration is no longer required on most platforms
|
||||
// /// (including Linux, macOS, iOS, FreeBSD, Android, and Windows)
|
||||
// fn register_explicit() -> Result<(), ServerFnError> {
|
||||
// Self::register_in_explicit::<LeptosServerFnRegistry>()
|
||||
// }
|
||||
//}
|
||||
//
|
||||
//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::{
|
||||
create_rw_signal, is_suppressing_resource_load, signal_prelude::*,
|
||||
spawn_local, store_value, untrack, ReadSignal, RwSignal, StoredValue,
|
||||
|
|
|
@ -32,7 +32,7 @@ multer = { version = "3", optional = true }
|
|||
|
||||
## output encodings
|
||||
# serde
|
||||
serde_json = { version = "1", optional = true }
|
||||
serde_json = "1"
|
||||
futures = "0.3"
|
||||
http = { version = "1", optional = true }
|
||||
ciborium = { version = "0.2", optional = true }
|
||||
|
@ -81,7 +81,7 @@ browser = [
|
|||
"dep:wasm-streams",
|
||||
"dep:wasm-bindgen-futures",
|
||||
]
|
||||
json = ["dep:serde_json"]
|
||||
#json = ["dep:serde_json"]
|
||||
multipart = ["dep:multer"]
|
||||
url = ["dep:serde_qs"]
|
||||
cbor = ["dep:ciborium"]
|
||||
|
|
|
@ -1,8 +1,55 @@
|
|||
use core::fmt::{self, Display};
|
||||
|
||||
use core::fmt::Display;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{error, fmt, ops, sync::Arc};
|
||||
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
|
||||
/// with this server function.
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
|
@ -83,7 +130,12 @@ impl<E: Display + Clone> ViaError<E> for &WrapError<E> {
|
|||
impl<E> ViaError<E> for WrapError<E> {
|
||||
#[track_caller]
|
||||
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,
|
||||
"{}",
|
||||
match self {
|
||||
ServerFnError::Registration(s) =>
|
||||
format!("error while trying to register the server function: {s}"),
|
||||
ServerFnError::Request(s) =>
|
||||
format!("error reaching server to call server function: {s}"),
|
||||
ServerFnError::ServerError(s) => format!("error running server function: {s}"),
|
||||
ServerFnError::Registration(s) => format!(
|
||||
"error while trying to register the server function: {s}"
|
||||
),
|
||||
ServerFnError::Request(s) => format!(
|
||||
"error reaching server to call server function: {s}"
|
||||
),
|
||||
ServerFnError::ServerError(s) =>
|
||||
format!("error running server function: {s}"),
|
||||
ServerFnError::Deserialization(s) =>
|
||||
format!("error deserializing server function results: {s}"),
|
||||
ServerFnError::Serialization(s) =>
|
||||
format!("error serializing server function arguments: {s}"),
|
||||
ServerFnError::Args(s) =>
|
||||
format!("error deserializing server function arguments: {s}"),
|
||||
ServerFnError::Args(s) => format!(
|
||||
"error deserializing server function arguments: {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),
|
||||
}
|
||||
)
|
||||
|
@ -202,14 +259,26 @@ pub enum ServerFnErrorErr<E = NoCustomError> {
|
|||
impl<CustErr> From<ServerFnError<CustErr>> for ServerFnErrorErr<CustErr> {
|
||||
fn from(value: ServerFnError<CustErr>) -> Self {
|
||||
match value {
|
||||
ServerFnError::Registration(value) => ServerFnErrorErr::Registration(value),
|
||||
ServerFnError::Registration(value) => {
|
||||
ServerFnErrorErr::Registration(value)
|
||||
}
|
||||
ServerFnError::Request(value) => ServerFnErrorErr::Request(value),
|
||||
ServerFnError::ServerError(value) => ServerFnErrorErr::ServerError(value),
|
||||
ServerFnError::Deserialization(value) => ServerFnErrorErr::Deserialization(value),
|
||||
ServerFnError::Serialization(value) => ServerFnErrorErr::Serialization(value),
|
||||
ServerFnError::ServerError(value) => {
|
||||
ServerFnErrorErr::ServerError(value)
|
||||
}
|
||||
ServerFnError::Deserialization(value) => {
|
||||
ServerFnErrorErr::Deserialization(value)
|
||||
}
|
||||
ServerFnError::Serialization(value) => {
|
||||
ServerFnErrorErr::Serialization(value)
|
||||
}
|
||||
ServerFnError::Args(value) => ServerFnErrorErr::Args(value),
|
||||
ServerFnError::MissingArg(value) => ServerFnErrorErr::MissingArg(value),
|
||||
ServerFnError::WrappedServerError(value) => ServerFnErrorErr::WrappedServerError(value),
|
||||
ServerFnError::MissingArg(value) => {
|
||||
ServerFnErrorErr::MissingArg(value)
|
||||
}
|
||||
ServerFnError::WrappedServerError(value) => {
|
||||
ServerFnErrorErr::WrappedServerError(value)
|
||||
}
|
||||
ServerFnError::Response(value) => ServerFnErrorErr::Response(value),
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue