Made some progress, started work on pavex integration as well

This commit is contained in:
benwis 2024-01-02 15:06:49 -08:00 committed by Greg Johnston
parent 2a5c855595
commit 197edebd51
46 changed files with 2315 additions and 389 deletions

3
examples/pavex_demo/.gitignore vendored Normal file
View file

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

View 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

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

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

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

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

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

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

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

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

View file

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

View 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

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

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

View file

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

View file

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

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

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

View file

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

View file

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

View file

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

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

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

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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

View file

@ -38,11 +38,11 @@
}, },
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1703637592, "lastModified": 1703961334,
"narHash": "sha256-8MXjxU0RfFfzl57Zy3OfXCITS0qWDNLzlBAdwxGZwfY=", "narHash": "sha256-M1mV/Cq+pgjk0rt6VxoyyD+O8cOUiai8t9Q6Yyq4noY=",
"owner": "NixOS", "owner": "NixOS",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "cfc3698c31b1fb9cdcf10f36c9643460264d0ca8", "rev": "b0d36bd0a420ecee3bc916c91886caca87c894e9",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -81,11 +81,11 @@
"nixpkgs": "nixpkgs_2" "nixpkgs": "nixpkgs_2"
}, },
"locked": { "locked": {
"lastModified": 1703902408, "lastModified": 1704075545,
"narHash": "sha256-qXdWvu+tlgNjeoz8yQMRKSom6QyRROfgpmeOhwbujqw=", "narHash": "sha256-L3zgOuVKhPjKsVLc3yTm2YJ6+BATyZBury7wnhyc8QU=",
"owner": "oxalica", "owner": "oxalica",
"repo": "rust-overlay", "repo": "rust-overlay",
"rev": "319f57cd2c34348c55970a4bf2b35afe82088681", "rev": "a0df72e106322b67e9c6e591fe870380bd0da0d5",
"type": "github" "type": "github"
}, },
"original": { "original": {

View file

@ -50,17 +50,21 @@ pub fn server_impl(args: TokenStream, s: TokenStream) -> TokenStream {
if args.prefix.is_none() { if args.prefix.is_none() {
args.prefix = Some(Literal::string("/api")); args.prefix = Some(Literal::string("/api"));
} }
let args_prefix = match &args.prefix {
Some(s) => s.to_string(),
None => "/api".to_string(),
};
// default to "Url" if no encoding given // default to "Url" if no encoding given
if args.encoding.is_none() { if args.encoding.is_none() {
args.encoding = Some(Literal::string("Url")); args.encoding = Some(Literal::string("Url"));
} }
// Either this match is wrong, or the impl in the macro crate is wrong
match server_fn_macro::server_macro_impl( match server_fn_macro::server_macro_impl(
quote::quote!(#args), quote::quote!(#args),
mapped_body, mapped_body,
syn::parse_quote!(::leptos::leptos_server::ServerFnTraitObj), syn::parse_quote!(::leptos::leptos_server::ServerFnTraitObj),
None,
Some(syn::parse_quote!(::leptos::server_fn)), Some(syn::parse_quote!(::leptos::server_fn)),
&args_prefix,
) { ) {
Err(e) => e.to_compile_error().into(), Err(e) => e.to_compile_error().into(),
Ok(s) => s.to_token_stream().into(), Ok(s) => s.to_token_stream().into(),

View file

@ -1,8 +1,9 @@
use crate::{ServerFn, ServerFnError}; //use crate::{ServerFn, ServerFnError};
use leptos_reactive::{ use leptos_reactive::{
batch, create_rw_signal, is_suppressing_resource_load, signal_prelude::*, batch, create_rw_signal, is_suppressing_resource_load, signal_prelude::*,
spawn_local, store_value, ReadSignal, RwSignal, StoredValue, spawn_local, store_value, ReadSignal, RwSignal, StoredValue,
}; };
use server_fn::{ServerFn, ServerFnError};
use std::{cell::Cell, future::Future, pin::Pin, rc::Rc}; use std::{cell::Cell, future::Future, pin::Pin, rc::Rc};
/// An action synchronizes an imperative `async` call to the synchronous reactive system. /// An action synchronizes an imperative `async` call to the synchronous reactive system.

View file

@ -1,120 +1,120 @@
#![deny(missing_docs)] //#![deny(missing_docs)]
#![forbid(unsafe_code)] #![forbid(unsafe_code)]
//! # Leptos Server Functions ////! # Leptos Server Functions
//! ////!
//! This package is based on a simple idea: sometimes its useful to write functions ////! This package is based on a simple idea: sometimes its useful to write functions
//! that will only run on the server, and call them from the client. ////! that will only run on the server, and call them from the client.
//! ////!
//! If youre creating anything beyond a toy app, youll need to do this all the time: ////! If youre creating anything beyond a toy app, youll need to do this all the time:
//! reading from or writing to a database that only runs on the server, running expensive ////! reading from or writing to a database that only runs on the server, running expensive
//! computations using libraries you dont want to ship down to the client, accessing ////! computations using libraries you dont want to ship down to the client, accessing
//! APIs that need to be called from the server rather than the client for CORS reasons ////! APIs that need to be called from the server rather than the client for CORS reasons
//! or because you need a secret API key thats stored on the server and definitely ////! or because you need a secret API key thats stored on the server and definitely
//! shouldnt be shipped down to a users browser. ////! shouldnt be shipped down to a users browser.
//! ////!
//! Traditionally, this is done by separating your server and client code, and by setting ////! Traditionally, this is done by separating your server and client code, and by setting
//! up something like a REST API or GraphQL API to allow your client to fetch and mutate ////! up something like a REST API or GraphQL API to allow your client to fetch and mutate
//! data on the server. This is fine, but it requires you to write and maintain your code ////! data on the server. This is fine, but it requires you to write and maintain your code
//! in multiple separate places (client-side code for fetching, server-side functions to run), ////! in multiple separate places (client-side code for fetching, server-side functions to run),
//! as well as creating a third thing to manage, which is the API contract between the two. ////! as well as creating a third thing to manage, which is the API contract between the two.
//! ////!
//! This package provides two simple primitives that allow you instead to write co-located, ////! This package provides two simple primitives that allow you instead to write co-located,
//! isomorphic server functions. (*Co-located* means you can write them in your app code so ////! isomorphic server functions. (*Co-located* means you can write them in your app code so
//! that they are “located alongside” the client code that calls them, rather than separating ////! that they are “located alongside” the client code that calls them, rather than separating
//! the client and server sides. *Isomorphic* means you can call them from the client as if ////! the client and server sides. *Isomorphic* means you can call them from the client as if
//! you were simply calling a function; the function call has the “same shape” on the client ////! you were simply calling a function; the function call has the “same shape” on the client
//! as it does on the server.) ////! as it does on the server.)
//! ////!
//! ### `#[server]` ////! ### `#[server]`
//! ////!
//! The [`#[server]`](https://docs.rs/leptos/latest/leptos/attr.server.html) macro allows you to annotate a function to ////! The [`#[server]`](https://docs.rs/leptos/latest/leptos/attr.server.html) macro allows you to annotate a function to
//! indicate that it should only run on the server (i.e., when you have an `ssr` feature in your ////! indicate that it should only run on the server (i.e., when you have an `ssr` feature in your
//! crate that is enabled). ////! crate that is enabled).
//! ////!
//! ```rust,ignore ////! ```rust,ignore
//! use leptos::*; ////! use leptos::*;
//! #[server(ReadFromDB)] ////! #[server(ReadFromDB)]
//! async fn read_posts(how_many: usize, query: String) -> Result<Vec<Posts>, ServerFnError> { ////! async fn read_posts(how_many: usize, query: String) -> Result<Vec<Posts>, ServerFnError> {
//! // do some server-only work here to access the database ////! // do some server-only work here to access the database
//! let posts = todo!();; ////! let posts = todo!();;
//! Ok(posts) ////! Ok(posts)
//! } ////! }
//! ////!
//! // call the function ////! // call the function
//! spawn_local(async { ////! spawn_local(async {
//! let posts = read_posts(3, "my search".to_string()).await; ////! let posts = read_posts(3, "my search".to_string()).await;
//! log::debug!("posts = {posts:#?}"); ////! log::debug!("posts = {posts:#?}");
//! }); ////! });
//! ``` ////! ```
//! ////!
//! If you call this function from the client, it will serialize the function arguments and `POST` ////! If you call this function from the client, it will serialize the function arguments and `POST`
//! them to the server as if they were the inputs in `<form method="POST">`. ////! them to the server as if they were the inputs in `<form method="POST">`.
//! ////!
//! Heres what you need to remember: ////! Heres what you need to remember:
//! - **Server functions must be `async`.** Even if the work being done inside the function body ////! - **Server functions must be `async`.** Even if the work being done inside the function body
//! can run synchronously on the server, from the clients perspective it involves an asynchronous ////! can run synchronously on the server, from the clients perspective it involves an asynchronous
//! function call. ////! function call.
//! - **Server functions must return `Result<T, ServerFnError>`.** Even if the work being done ////! - **Server functions must return `Result<T, ServerFnError>`.** Even if the work being done
//! inside the function body cant fail, the processes of serialization/deserialization and the ////! inside the function body cant fail, the processes of serialization/deserialization and the
//! network call are fallible. ////! network call are fallible.
//! - **Return types must be [Serializable](leptos_reactive::Serializable).** ////! - **Return types must be [Serializable](leptos_reactive::Serializable).**
//! This should be fairly obvious: we have to serialize arguments to send them to the server, and we ////! This should be fairly obvious: we have to serialize arguments to send them to the server, and we
//! need to deserialize the result to return it to the client. ////! need to deserialize the result to return it to the client.
//! - **Arguments must be implement [serde::Serialize].** They are serialized as an `application/x-www-form-urlencoded` ////! - **Arguments must be implement [serde::Serialize].** They are serialized as an `application/x-www-form-urlencoded`
//! form data using [`serde_qs`](https://docs.rs/serde_qs/latest/serde_qs/) or as `application/cbor` ////! form data using [`serde_qs`](https://docs.rs/serde_qs/latest/serde_qs/) or as `application/cbor`
//! using [`cbor`](https://docs.rs/cbor/latest/cbor/). **Note**: You should explicitly include `serde` with the ////! using [`cbor`](https://docs.rs/cbor/latest/cbor/). **Note**: You should explicitly include `serde` with the
//! `derive` feature enabled in your `Cargo.toml`. You can do this by running `cargo add serde --features=derive`. ////! `derive` feature enabled in your `Cargo.toml`. You can do this by running `cargo add serde --features=derive`.
//! - Context comes from the server. [`use_context`](leptos_reactive::use_context) can be used to access specific ////! - Context comes from the server. [`use_context`](leptos_reactive::use_context) can be used to access specific
//! server-related data, as documented in the server integrations. This allows accessing things like HTTP request ////! server-related data, as documented in the server integrations. This allows accessing things like HTTP request
//! headers as needed. However, server functions *not* have access to reactive state that exists in the client. ////! headers as needed. However, server functions *not* have access to reactive state that exists in the client.
//! ////!
//! ## Server Function Encodings ////! ## Server Function Encodings
//! ////!
//! By default, the server function call is a `POST` request that serializes the arguments as URL-encoded form data in the body ////! By default, the server function call is a `POST` request that serializes the arguments as URL-encoded form data in the body
//! of the request. But there are a few other methods supported. Optionally, we can provide another argument to the `#[server]` ////! of the request. But there are a few other methods supported. Optionally, we can provide another argument to the `#[server]`
//! macro to specify an alternate encoding: ////! macro to specify an alternate encoding:
//! ////!
//! ```rust,ignore ////! ```rust,ignore
//! #[server(AddTodo, "/api", "Url")] ////! #[server(AddTodo, "/api", "Url")]
//! #[server(AddTodo, "/api", "GetJson")] ////! #[server(AddTodo, "/api", "GetJson")]
//! #[server(AddTodo, "/api", "Cbor")] ////! #[server(AddTodo, "/api", "Cbor")]
//! #[server(AddTodo, "/api", "GetCbor")] ////! #[server(AddTodo, "/api", "GetCbor")]
//! ``` ////! ```
//! ////!
//! The four options use different combinations of HTTP verbs and encoding methods: ////! The four options use different combinations of HTTP verbs and encoding methods:
//! ////!
//! | Name | Method | Request | Response | ////! | Name | Method | Request | Response |
//! | ----------------- | ------ | ----------- | -------- | ////! | ----------------- | ------ | ----------- | -------- |
//! | **Url** (default) | POST | URL encoded | JSON | ////! | **Url** (default) | POST | URL encoded | JSON |
//! | **GetJson** | GET | URL encoded | JSON | ////! | **GetJson** | GET | URL encoded | JSON |
//! | **Cbor** | POST | CBOR | CBOR | ////! | **Cbor** | POST | CBOR | CBOR |
//! | **GetCbor** | GET | URL encoded | CBOR | ////! | **GetCbor** | GET | URL encoded | CBOR |
//! ////!
//! In other words, you have two choices: ////! In other words, you have two choices:
//! ////!
//! - `GET` or `POST`? This has implications for things like browser or CDN caching; while `POST` requests should not be cached, ////! - `GET` or `POST`? This has implications for things like browser or CDN caching; while `POST` requests should not be cached,
//! `GET` requests can be. ////! `GET` requests can be.
//! - Plain text (arguments sent with URL/form encoding, results sent as JSON) or a binary format (CBOR, encoded as a base64 ////! - Plain text (arguments sent with URL/form encoding, results sent as JSON) or a binary format (CBOR, encoded as a base64
//! string)? ////! string)?
//! ////!
//! ## Why not `PUT` or `DELETE`? Why URL/form encoding, and not JSON?** ////! ## Why not `PUT` or `DELETE`? Why URL/form encoding, and not JSON?**
//! ////!
//! These are reasonable questions. Much of the web is built on REST API patterns that encourage the use of semantic HTTP ////! These are reasonable questions. Much of the web is built on REST API patterns that encourage the use of semantic HTTP
//! methods like `DELETE` to delete an item from a database, and many devs are accustomed to sending data to APIs in the ////! methods like `DELETE` to delete an item from a database, and many devs are accustomed to sending data to APIs in the
//! JSON format. ////! JSON format.
//! ////!
//! The reason we use `POST` or `GET` with URL-encoded data by default is the `<form>` support. For better or for worse, ////! The reason we use `POST` or `GET` with URL-encoded data by default is the `<form>` support. For better or for worse,
//! HTML forms dont support `PUT` or `DELETE`, and they dont support sending JSON. This means that if you use anything ////! HTML forms dont support `PUT` or `DELETE`, and they dont support sending JSON. This means that if you use anything
//! but a `GET` or `POST` request with URL-encoded data, it can only work once WASM has loaded. ////! but a `GET` or `POST` request with URL-encoded data, it can only work once WASM has loaded.
//! ////!
//! The CBOR encoding is suported for historical reasons; an earlier version of server functions used a URL encoding that ////! The CBOR encoding is suported for historical reasons; an earlier version of server functions used a URL encoding that
//! didnt support nested objects like structs or vectors as server function arguments, which CBOR did. But note that the ////! didnt support nested objects like structs or vectors as server function arguments, which CBOR did. But note that the
//! CBOR forms encounter the same issue as `PUT`, `DELETE`, or JSON: they do not degrade gracefully if the WASM version of ////! CBOR forms encounter the same issue as `PUT`, `DELETE`, or JSON: they do not degrade gracefully if the WASM version of
//! your app is not available. ////! your app is not available.
pub use server_fn::{ pub use server_fn::{
error::ServerFnErrorErr, Encoding, Payload, ServerFnError, error::ServerFnErrorErr, ServerFnError,
}; };
mod action; mod action;
@ -123,250 +123,250 @@ pub use action::*;
pub use multi_action::*; pub use multi_action::*;
extern crate tracing; extern crate tracing;
#[cfg(any(feature = "ssr", doc))] //#[cfg(any(feature = "ssr", doc))]
use std::{ //use std::{
collections::HashMap, // collections::HashMap,
sync::{Arc, RwLock}, // sync::{Arc, RwLock},
}; //};
//
#[cfg(any(feature = "ssr", doc))] //#[cfg(any(feature = "ssr", doc))]
/// A concrete type for a server function. ///// A concrete type for a server function.
#[derive(Clone)] //#[derive(Clone)]
pub struct ServerFnTraitObj(pub server_fn::ServerFnTraitObj<()>); //pub struct ServerFnTraitObj(pub server_fn::ServerFnTraitObj<()>);
//
#[cfg(any(feature = "ssr", doc))] //#[cfg(any(feature = "ssr", doc))]
impl std::ops::Deref for ServerFnTraitObj { //impl std::ops::Deref for ServerFnTraitObj {
type Target = server_fn::ServerFnTraitObj<()>; // type Target = server_fn::ServerFnTraitObj<()>;
//
fn deref(&self) -> &Self::Target { // fn deref(&self) -> &Self::Target {
&self.0 // &self.0
} // }
} //}
//
#[cfg(any(feature = "ssr", doc))] //#[cfg(any(feature = "ssr", doc))]
impl std::ops::DerefMut for ServerFnTraitObj { //impl std::ops::DerefMut for ServerFnTraitObj {
fn deref_mut(&mut self) -> &mut Self::Target { // fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0 // &mut self.0
} // }
} //}
//
#[cfg(any(feature = "ssr", doc))] //#[cfg(any(feature = "ssr", doc))]
impl ServerFnTraitObj { //impl ServerFnTraitObj {
/// Create a new `ServerFnTraitObj` from a `server_fn::ServerFnTraitObj`. // /// Create a new `ServerFnTraitObj` from a `server_fn::ServerFnTraitObj`.
pub const fn from_generic_server_fn( // pub const fn from_generic_server_fn(
server_fn: server_fn::ServerFnTraitObj<()>, // server_fn: server_fn::ServerFnTraitObj<()>,
) -> Self { // ) -> Self {
Self(server_fn) // Self(server_fn)
} // }
} //}
//
#[cfg(feature = "ssr")] //#[cfg(feature = "ssr")]
inventory::collect!(ServerFnTraitObj); //inventory::collect!(ServerFnTraitObj);
//
#[allow(unused)] //#[allow(unused)]
type ServerFunction = server_fn::ServerFnTraitObj<()>; //type ServerFunction = server_fn::ServerFnTraitObj<()>;
//
#[cfg(any(feature = "ssr", doc))] //#[cfg(any(feature = "ssr", doc))]
lazy_static::lazy_static! { //lazy_static::lazy_static! {
static ref REGISTERED_SERVER_FUNCTIONS: Arc<RwLock<HashMap<&'static str, ServerFnTraitObj>>> = { // static ref REGISTERED_SERVER_FUNCTIONS: Arc<RwLock<HashMap<&'static str, ServerFnTraitObj>>> = {
let mut map = HashMap::new(); // let mut map = HashMap::new();
for server_fn in inventory::iter::<ServerFnTraitObj> { // for server_fn in inventory::iter::<ServerFnTraitObj> {
map.insert(server_fn.0.url(), server_fn.clone()); // map.insert(server_fn.0.url(), server_fn.clone());
} // }
Arc::new(RwLock::new(map)) // Arc::new(RwLock::new(map))
}; // };
} //}
//
#[cfg(any(feature = "ssr", doc))] //#[cfg(any(feature = "ssr", doc))]
/// The registry of all Leptos server functions. ///// The registry of all Leptos server functions.
pub struct LeptosServerFnRegistry; //pub struct LeptosServerFnRegistry;
//
#[cfg(any(feature = "ssr", doc))] //#[cfg(any(feature = "ssr", doc))]
impl server_fn::ServerFunctionRegistry<()> for LeptosServerFnRegistry { //impl server_fn::ServerFunctionRegistry<()> for LeptosServerFnRegistry {
type Error = ServerRegistrationFnError; // type Error = ServerRegistrationFnError;
//
/// Server functions are automatically registered on most platforms, (including Linux, macOS, // /// Server functions are automatically registered on most platforms, (including Linux, macOS,
/// iOS, FreeBSD, Android, and Windows). If you are on another platform, like a WASM server runtime, // /// iOS, FreeBSD, Android, and Windows). If you are on another platform, like a WASM server runtime,
/// you should register server functions by calling this `T::register_explicit()`. // /// you should register server functions by calling this `T::register_explicit()`.
fn register_explicit( // fn register_explicit(
prefix: &'static str, // prefix: &'static str,
url: &'static str, // url: &'static str,
server_function: server_fn::SerializedFnTraitObj<()>, // server_function: server_fn::SerializedFnTraitObj<()>,
encoding: Encoding, // encoding: Encoding,
) -> Result<(), Self::Error> { // ) -> Result<(), Self::Error> {
// store it in the hashmap // // store it in the hashmap
let mut func_write = REGISTERED_SERVER_FUNCTIONS // let mut func_write = REGISTERED_SERVER_FUNCTIONS
.write() // .write()
.map_err(|e| ServerRegistrationFnError::Poisoned(e.to_string()))?; // .map_err(|e| ServerRegistrationFnError::Poisoned(e.to_string()))?;
let prev = func_write.insert( // let prev = func_write.insert(
url, // url,
ServerFnTraitObj(server_fn::ServerFnTraitObj::new( // ServerFnTraitObj(server_fn::ServerFnTraitObj::new(
prefix, // prefix,
url, // url,
encoding, // encoding,
server_function, // server_function,
)), // )),
); // );
//
// if there was already a server function with this key, // // if there was already a server function with this key,
// return Err // // return Err
match prev { // match prev {
Some(_) => { // Some(_) => {
Err(ServerRegistrationFnError::AlreadyRegistered(format!( // Err(ServerRegistrationFnError::AlreadyRegistered(format!(
"There was already a server function registered at {:?}. \ // "There was already a server function registered at {:?}. \
This can happen if you use the same server function name \ // This can happen if you use the same server function name \
in two different modules // in two different modules
on `stable` or in `release` mode.", // on `stable` or in `release` mode.",
url // url
))) // )))
} // }
None => Ok(()), // None => Ok(()),
} // }
} // }
//
/// Returns the server function registered at the given URL, or `None` if no function is registered at that URL. // /// Returns the server function registered at the given URL, or `None` if no function is registered at that URL.
fn get(url: &str) -> Option<server_fn::ServerFnTraitObj<()>> { // fn get(url: &str) -> Option<server_fn::ServerFnTraitObj<()>> {
REGISTERED_SERVER_FUNCTIONS // REGISTERED_SERVER_FUNCTIONS
.read() // .read()
.ok() // .ok()
.and_then(|fns| fns.get(url).map(|sf| sf.0.clone())) // .and_then(|fns| fns.get(url).map(|sf| sf.0.clone()))
} // }
//
/// Returns the server function trait obj registered at the given URL, or `None` if no function is registered at that URL. // /// Returns the server function trait obj registered at the given URL, or `None` if no function is registered at that URL.
fn get_trait_obj(url: &str) -> Option<server_fn::ServerFnTraitObj<()>> { // fn get_trait_obj(url: &str) -> Option<server_fn::ServerFnTraitObj<()>> {
REGISTERED_SERVER_FUNCTIONS // REGISTERED_SERVER_FUNCTIONS
.read() // .read()
.ok() // .ok()
.and_then(|fns| fns.get(url).map(|sf| sf.0.clone())) // .and_then(|fns| fns.get(url).map(|sf| sf.0.clone()))
} // }
/// Return the // /// Return the
fn get_encoding(url: &str) -> Option<Encoding> { // fn get_encoding(url: &str) -> Option<Encoding> {
REGISTERED_SERVER_FUNCTIONS // REGISTERED_SERVER_FUNCTIONS
.read() // .read()
.ok() // .ok()
.and_then(|fns| fns.get(url).map(|sf| sf.encoding())) // .and_then(|fns| fns.get(url).map(|sf| sf.encoding()))
} // }
//
/// Returns a list of all registered server functions. // /// Returns a list of all registered server functions.
fn paths_registered() -> Vec<&'static str> { // fn paths_registered() -> Vec<&'static str> {
REGISTERED_SERVER_FUNCTIONS // REGISTERED_SERVER_FUNCTIONS
.read() // .read()
.ok() // .ok()
.map(|fns| fns.keys().cloned().collect()) // .map(|fns| fns.keys().cloned().collect())
.unwrap_or_default() // .unwrap_or_default()
} // }
} //}
//
#[cfg(any(feature = "ssr", doc))] //#[cfg(any(feature = "ssr", doc))]
/// Errors that can occur when registering a server function. ///// Errors that can occur when registering a server function.
#[derive( //#[derive(
thiserror::Error, Debug, Clone, serde::Serialize, serde::Deserialize, // thiserror::Error, Debug, Clone, serde::Serialize, serde::Deserialize,
)] //)]
pub enum ServerRegistrationFnError { //pub enum ServerRegistrationFnError {
/// The server function is already registered. // /// The server function is already registered.
#[error("The server function {0} is already registered")] // #[error("The server function {0} is already registered")]
AlreadyRegistered(String), // AlreadyRegistered(String),
/// The server function registry is poisoned. // /// The server function registry is poisoned.
#[error("The server function registry is poisoned: {0}")] // #[error("The server function registry is poisoned: {0}")]
Poisoned(String), // Poisoned(String),
} //}
//
/// Get a ServerFunction struct containing info about the server fn ///// Get a ServerFunction struct containing info about the server fn
#[cfg(any(feature = "ssr", doc))] //#[cfg(any(feature = "ssr", doc))]
pub fn server_fn_by_path(path: &str) -> Option<ServerFnTraitObj> { //pub fn server_fn_by_path(path: &str) -> Option<ServerFnTraitObj> {
REGISTERED_SERVER_FUNCTIONS // REGISTERED_SERVER_FUNCTIONS
.read() // .read()
.expect("Server function registry is poisoned") // .expect("Server function registry is poisoned")
.get(path) // .get(path)
.cloned() // .cloned()
} //}
//
/// Attempts to find a server function registered at the given path. ///// Attempts to find a server function registered at the given path.
/// /////
/// This can be used by a server to handle the requests, as in the following example (using `actix-web`) ///// This can be used by a server to handle the requests, as in the following example (using `actix-web`)
/// /////
/// ```rust, ignore ///// ```rust, ignore
/// #[post("{tail:.*}")] ///// #[post("{tail:.*}")]
/// async fn handle_server_fns( ///// async fn handle_server_fns(
/// req: HttpRequest, ///// req: HttpRequest,
/// params: web::Path<String>, ///// params: web::Path<String>,
/// body: web::Bytes, ///// body: web::Bytes,
/// ) -> impl Responder { ///// ) -> impl Responder {
/// let path = params.into_inner(); ///// let path = params.into_inner();
/// let accept_header = req ///// let accept_header = req
/// .headers() ///// .headers()
/// .get("Accept") ///// .get("Accept")
/// .and_then(|value| value.to_str().ok()); ///// .and_then(|value| value.to_str().ok());
/// if let Some(server_fn) = server_fn_by_path(path.as_str()) { ///// if let Some(server_fn) = server_fn_by_path(path.as_str()) {
/// let query = req.query_string().as_bytes(); ///// let query = req.query_string().as_bytes();
/// let data = match &server_fn.encoding { ///// let data = match &server_fn.encoding {
/// Encoding::Url | Encoding::Cbor => &body, ///// Encoding::Url | Encoding::Cbor => &body,
/// Encoding::GetJSON | Encoding::GetCBOR => query, ///// Encoding::GetJSON | Encoding::GetCBOR => query,
/// }; ///// };
/// match (server_fn.trait_obj)(data).await { ///// match (server_fn.trait_obj)(data).await {
/// Ok(serialized) => { ///// Ok(serialized) => {
/// // if this is Accept: application/json then send a serialized JSON response ///// // if this is Accept: application/json then send a serialized JSON response
/// if let Some("application/json") = accept_header { ///// if let Some("application/json") = accept_header {
/// HttpResponse::Ok().body(serialized) ///// HttpResponse::Ok().body(serialized)
/// } ///// }
/// // otherwise, it's probably a <form> submit or something: redirect back to the referrer ///// // otherwise, it's probably a <form> submit or something: redirect back to the referrer
/// else { ///// else {
/// HttpResponse::SeeOther() ///// HttpResponse::SeeOther()
/// .insert_header(("Location", "/")) ///// .insert_header(("Location", "/"))
/// .content_type("application/json") ///// .content_type("application/json")
/// .body(serialized) ///// .body(serialized)
/// } ///// }
/// } ///// }
/// Err(e) => { ///// Err(e) => {
/// eprintln!("server function error: {e:#?}"); ///// eprintln!("server function error: {e:#?}");
/// HttpResponse::InternalServerError().body(e.to_string()) ///// HttpResponse::InternalServerError().body(e.to_string())
/// } ///// }
/// } ///// }
/// } else { ///// } else {
/// HttpResponse::BadRequest().body(format!("Could not find a server function at that route.")) ///// HttpResponse::BadRequest().body(format!("Could not find a server function at that route."))
/// } ///// }
/// } ///// }
/// ``` ///// ```
#[cfg(any(feature = "ssr", doc))] //#[cfg(any(feature = "ssr", doc))]
pub fn server_fn_trait_obj_by_path(path: &str) -> Option<ServerFnTraitObj> { //pub fn server_fn_trait_obj_by_path(path: &str) -> Option<ServerFnTraitObj> {
server_fn::server_fn_trait_obj_by_path::<(), LeptosServerFnRegistry>(path) // server_fn::server_fn_trait_obj_by_path::<(), LeptosServerFnRegistry>(path)
.map(ServerFnTraitObj::from_generic_server_fn) // .map(ServerFnTraitObj::from_generic_server_fn)
} //}
//
/// Get the Encoding of a server fn if one is registered at that path. Otherwise, return None ///// Get the Encoding of a server fn if one is registered at that path. Otherwise, return None
#[cfg(any(feature = "ssr", doc))] //#[cfg(any(feature = "ssr", doc))]
pub fn server_fn_encoding_by_path(path: &str) -> Option<Encoding> { //pub fn server_fn_encoding_by_path(path: &str) -> Option<Encoding> {
server_fn::server_fn_encoding_by_path::<(), LeptosServerFnRegistry>(path) // server_fn::server_fn_encoding_by_path::<(), LeptosServerFnRegistry>(path)
} //}
//
/// Returns the set of currently-registered server function paths, for debugging purposes. ///// Returns the set of currently-registered server function paths, for debugging purposes.
#[cfg(any(feature = "ssr", doc))] //#[cfg(any(feature = "ssr", doc))]
pub fn server_fns_by_path() -> Vec<&'static str> { //pub fn server_fns_by_path() -> Vec<&'static str> {
server_fn::server_fns_by_path::<(), LeptosServerFnRegistry>() // server_fn::server_fns_by_path::<(), LeptosServerFnRegistry>()
} //}
//
/// Defines a "server function." A server function can be called from the server or the client, ///// Defines a "server function." A server function can be called from the server or the client,
/// but the body of its code will only be run on the server, i.e., if a crate feature `ssr` is enabled. ///// but the body of its code will only be run on the server, i.e., if a crate feature `ssr` is enabled.
/// /////
/// (This follows the same convention as the Leptos framework's distinction between `ssr` for server-side rendering, ///// (This follows the same convention as the Leptos framework's distinction between `ssr` for server-side rendering,
/// and `csr` and `hydrate` for client-side rendering and hydration, respectively.) ///// and `csr` and `hydrate` for client-side rendering and hydration, respectively.)
/// /////
/// Server functions are created using the `server` macro. ///// Server functions are created using the `server` macro.
/// /////
/// The function should be registered by calling `ServerFn::register()`. The set of server functions ///// The function should be registered by calling `ServerFn::register()`. The set of server functions
/// can be queried on the server for routing purposes by calling [server_fn_by_path]. ///// can be queried on the server for routing purposes by calling [server_fn_by_path].
/// /////
/// Technically, the trait is implemented on a type that describes the server function's arguments. ///// Technically, the trait is implemented on a type that describes the server function's arguments.
pub trait ServerFn: server_fn::ServerFn<()> { //pub trait ServerFn: server_fn::ServerFn<()> {
#[cfg(any(feature = "ssr", doc))] // #[cfg(any(feature = "ssr", doc))]
/// Explicitly registers the server function on platforms that require it, // /// Explicitly registers the server function on platforms that require it,
/// allowing the server to query it by URL. // /// allowing the server to query it by URL.
/// // ///
/// Explicit server function registration is no longer required on most platforms // /// Explicit server function registration is no longer required on most platforms
/// (including Linux, macOS, iOS, FreeBSD, Android, and Windows) // /// (including Linux, macOS, iOS, FreeBSD, Android, and Windows)
fn register_explicit() -> Result<(), ServerFnError> { // fn register_explicit() -> Result<(), ServerFnError> {
Self::register_in_explicit::<LeptosServerFnRegistry>() // Self::register_in_explicit::<LeptosServerFnRegistry>()
} // }
} //}
//
impl<T> ServerFn for T where T: server_fn::ServerFn<()> {} //impl<T> ServerFn for T where T: server_fn::ServerFn<()> {}

View file

@ -1,4 +1,4 @@
use crate::{ServerFn, ServerFnError}; use server_fn::{ServerFn, ServerFnError};
use leptos_reactive::{ use leptos_reactive::{
create_rw_signal, is_suppressing_resource_load, signal_prelude::*, create_rw_signal, is_suppressing_resource_load, signal_prelude::*,
spawn_local, store_value, untrack, ReadSignal, RwSignal, StoredValue, spawn_local, store_value, untrack, ReadSignal, RwSignal, StoredValue,

View file

@ -32,7 +32,7 @@ multer = { version = "3", optional = true }
## output encodings ## output encodings
# serde # serde
serde_json = { version = "1", optional = true } serde_json = "1"
futures = "0.3" futures = "0.3"
http = { version = "1", optional = true } http = { version = "1", optional = true }
ciborium = { version = "0.2", optional = true } ciborium = { version = "0.2", optional = true }
@ -81,7 +81,7 @@ browser = [
"dep:wasm-streams", "dep:wasm-streams",
"dep:wasm-bindgen-futures", "dep:wasm-bindgen-futures",
] ]
json = ["dep:serde_json"] #json = ["dep:serde_json"]
multipart = ["dep:multer"] multipart = ["dep:multer"]
url = ["dep:serde_qs"] url = ["dep:serde_qs"]
cbor = ["dep:ciborium"] cbor = ["dep:ciborium"]

View file

@ -1,8 +1,55 @@
use core::fmt::{self, Display}; use core::fmt::Display;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::{error, fmt, ops, sync::Arc};
use thiserror::Error; use thiserror::Error;
/// This is a result type into which any error can be converted,
/// and which can be used directly in your `view`.
///
/// All errors will be stored as [`struct@Error`].
pub type Result<T, E = Error> = core::result::Result<T, E>;
/// A generic wrapper for any error.
#[derive(Debug, Clone)]
#[repr(transparent)]
pub struct Error(Arc<dyn error::Error + Send + Sync>);
impl Error {
/// Converts the wrapper into the inner reference-counted error.
pub fn into_inner(self) -> Arc<dyn error::Error + Send + Sync> {
Arc::clone(&self.0)
}
}
impl ops::Deref for Error {
type Target = Arc<dyn error::Error + Send + Sync>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
impl<T> From<T> for Error
where
T: std::error::Error + Send + Sync + 'static,
{
fn from(value: T) -> Self {
Error(Arc::new(value))
}
}
impl From<ServerFnError> for Error {
fn from(e: ServerFnError) -> Self {
Error(Arc::new(ServerFnErrorErr::from(e)))
}
}
/// An empty value indicating that there is no custom error type associated /// An empty value indicating that there is no custom error type associated
/// with this server function. /// with this server function.
#[derive(Debug, Deserialize, Serialize)] #[derive(Debug, Deserialize, Serialize)]
@ -83,7 +130,12 @@ impl<E: Display + Clone> ViaError<E> for &WrapError<E> {
impl<E> ViaError<E> for WrapError<E> { impl<E> ViaError<E> for WrapError<E> {
#[track_caller] #[track_caller]
fn to_server_error(&self) -> ServerFnError<E> { fn to_server_error(&self) -> ServerFnError<E> {
panic!("At {}, you call `to_server_error()` or use `server_fn_error!` with a value that does not implement `Clone` and either `Error` or `Display`.", std::panic::Location::caller()); panic!(
"At {}, you call `to_server_error()` or use `server_fn_error!` \
with a value that does not implement `Clone` and either `Error` \
or `Display`.",
std::panic::Location::caller()
);
} }
} }
@ -129,19 +181,24 @@ where
f, f,
"{}", "{}",
match self { match self {
ServerFnError::Registration(s) => ServerFnError::Registration(s) => format!(
format!("error while trying to register the server function: {s}"), "error while trying to register the server function: {s}"
ServerFnError::Request(s) => ),
format!("error reaching server to call server function: {s}"), ServerFnError::Request(s) => format!(
ServerFnError::ServerError(s) => format!("error running server function: {s}"), "error reaching server to call server function: {s}"
),
ServerFnError::ServerError(s) =>
format!("error running server function: {s}"),
ServerFnError::Deserialization(s) => ServerFnError::Deserialization(s) =>
format!("error deserializing server function results: {s}"), format!("error deserializing server function results: {s}"),
ServerFnError::Serialization(s) => ServerFnError::Serialization(s) =>
format!("error serializing server function arguments: {s}"), format!("error serializing server function arguments: {s}"),
ServerFnError::Args(s) => ServerFnError::Args(s) => format!(
format!("error deserializing server function arguments: {s}"), "error deserializing server function arguments: {s}"
),
ServerFnError::MissingArg(s) => format!("missing argument {s}"), ServerFnError::MissingArg(s) => format!("missing argument {s}"),
ServerFnError::Response(s) => format!("error generating HTTP response: {s}"), ServerFnError::Response(s) =>
format!("error generating HTTP response: {s}"),
ServerFnError::WrappedServerError(e) => format!("{}", e), ServerFnError::WrappedServerError(e) => format!("{}", e),
} }
) )
@ -202,14 +259,26 @@ pub enum ServerFnErrorErr<E = NoCustomError> {
impl<CustErr> From<ServerFnError<CustErr>> for ServerFnErrorErr<CustErr> { impl<CustErr> From<ServerFnError<CustErr>> for ServerFnErrorErr<CustErr> {
fn from(value: ServerFnError<CustErr>) -> Self { fn from(value: ServerFnError<CustErr>) -> Self {
match value { match value {
ServerFnError::Registration(value) => ServerFnErrorErr::Registration(value), ServerFnError::Registration(value) => {
ServerFnErrorErr::Registration(value)
}
ServerFnError::Request(value) => ServerFnErrorErr::Request(value), ServerFnError::Request(value) => ServerFnErrorErr::Request(value),
ServerFnError::ServerError(value) => ServerFnErrorErr::ServerError(value), ServerFnError::ServerError(value) => {
ServerFnError::Deserialization(value) => ServerFnErrorErr::Deserialization(value), ServerFnErrorErr::ServerError(value)
ServerFnError::Serialization(value) => ServerFnErrorErr::Serialization(value), }
ServerFnError::Deserialization(value) => {
ServerFnErrorErr::Deserialization(value)
}
ServerFnError::Serialization(value) => {
ServerFnErrorErr::Serialization(value)
}
ServerFnError::Args(value) => ServerFnErrorErr::Args(value), ServerFnError::Args(value) => ServerFnErrorErr::Args(value),
ServerFnError::MissingArg(value) => ServerFnErrorErr::MissingArg(value), ServerFnError::MissingArg(value) => {
ServerFnError::WrappedServerError(value) => ServerFnErrorErr::WrappedServerError(value), ServerFnErrorErr::MissingArg(value)
}
ServerFnError::WrappedServerError(value) => {
ServerFnErrorErr::WrappedServerError(value)
}
ServerFnError::Response(value) => ServerFnErrorErr::Response(value), ServerFnError::Response(value) => ServerFnErrorErr::Response(value),
} }
} }