mirror of
https://github.com/rm-dr/servable.git
synced 2025-11-28 13:29:59 -08:00
Compare commits
2 Commits
1f26bd4938
...
8674bd9c85
| Author | SHA1 | Date | |
|---|---|---|---|
| 8674bd9c85 | |||
| e5926bccbf |
5
.github/workflows/ci.yml
vendored
5
.github/workflows/ci.yml
vendored
@@ -52,8 +52,6 @@ jobs:
|
|||||||
buildandtest:
|
buildandtest:
|
||||||
name: "Build and test"
|
name: "Build and test"
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
permissions:
|
|
||||||
contents: write
|
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
@@ -71,3 +69,6 @@ jobs:
|
|||||||
|
|
||||||
- name: Test
|
- name: Test
|
||||||
run: cargo test-all-features -- --release
|
run: cargo test-all-features -- --release
|
||||||
|
|
||||||
|
- name: Publish
|
||||||
|
run: cargo publish --dry-run
|
||||||
2
Cargo.lock
generated
2
Cargo.lock
generated
@@ -1274,7 +1274,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "servable"
|
name = "servable"
|
||||||
version = "0.0.2"
|
version = "0.0.4"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"axum",
|
"axum",
|
||||||
"chrono",
|
"chrono",
|
||||||
|
|||||||
28
Cargo.toml
28
Cargo.toml
@@ -5,7 +5,7 @@ resolver = "2"
|
|||||||
[workspace.package]
|
[workspace.package]
|
||||||
rust-version = "1.90.0"
|
rust-version = "1.90.0"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
version = "0.0.2"
|
version = "0.0.4"
|
||||||
license = "GPL-3.0"
|
license = "GPL-3.0"
|
||||||
repository = "https://github.com/rm-dr/servable"
|
repository = "https://github.com/rm-dr/servable"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
@@ -70,16 +70,16 @@ cargo_common_metadata = "deny"
|
|||||||
|
|
||||||
[workspace.dependencies]
|
[workspace.dependencies]
|
||||||
|
|
||||||
axum = "0.8.7"
|
axum = "0.8"
|
||||||
chrono = "0.4.42"
|
chrono = "0.4"
|
||||||
image = "0.25.9"
|
image = "0.25"
|
||||||
maud = "0.27.0"
|
maud = "0.27"
|
||||||
rand = "0.9.0"
|
rand = "0.9"
|
||||||
serde = { version = "1.0.228", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
serde_urlencoded = "0.7.1"
|
serde_urlencoded = "0.7"
|
||||||
strum = { version = "0.27.2", features = ["derive"] }
|
strum = { version = "0.27", features = ["derive"] }
|
||||||
thiserror = "2.0.17"
|
thiserror = "2.0"
|
||||||
tokio = "1.48.0"
|
tokio = "1.48"
|
||||||
tower = "0.5.2"
|
tower = "0.5"
|
||||||
tower-http = { version = "0.6.7", features = ["compression-full"] }
|
tower-http = { version = "0.6", features = ["compression-full"] }
|
||||||
tracing = "0.1.41"
|
tracing = "0.1"
|
||||||
|
|||||||
179
README.md
179
README.md
@@ -1,179 +0,0 @@
|
|||||||
# Servable: a simple web framework
|
|
||||||
|
|
||||||
[](https://github.com/rm-dr/servable/actions)
|
|
||||||
[](https://crates.io/crates/servable)
|
|
||||||
[](https://docs.rs/servable/)
|
|
||||||
|
|
||||||
A tiny, convenient web micro-framework built around [htmx](https://htmx.org), [Axum](https://github.com/tokio-rs/axum), and [Maud](https://maud.lambda.xyz).
|
|
||||||
Inspired by the "MASH" stack described [here](https://yree.io/mash) and [here](https://emschwartz.me/building-a-fast-website-with-the-mash-stack-in-rust).
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## Features
|
|
||||||
|
|
||||||
`servable` provides abstractions that implement common utilities needed by an http server. \
|
|
||||||
|
|
||||||
- response headers and cache-busting utilities
|
|
||||||
- client device detection (mobile / desktop)
|
|
||||||
- server-side image optimization (see the `image` feature below)
|
|
||||||
- ergonomic [htmx](https://htmx.org) integration (see `htmx-*` features below)
|
|
||||||
|
|
||||||
|
|
||||||
-------------------
|
|
||||||
|
|
||||||
|
|
||||||
## Quick Start
|
|
||||||
|
|
||||||
```rust,ignore
|
|
||||||
use servable::{ServableRouter, servable::StaticAsset, mime::MimeType};
|
|
||||||
|
|
||||||
#[tokio::main]
|
|
||||||
async fn main() {
|
|
||||||
let route = ServableRouter::new()
|
|
||||||
.add_page(
|
|
||||||
"/hello",
|
|
||||||
StaticAsset {
|
|
||||||
bytes: b"Hello, World!",
|
|
||||||
mime: MimeType::Text,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
// usual axum startup routine
|
|
||||||
let app = route.into_router();
|
|
||||||
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000")
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
axum::serve(listener, app).await.unwrap();
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
# Core Concepts
|
|
||||||
|
|
||||||
## The `Servable` trait
|
|
||||||
|
|
||||||
The `Servable` trait is the foundation of this stack. \
|
|
||||||
`servable` provides implementations for a few common servables:
|
|
||||||
|
|
||||||
|
|
||||||
- `StaticAsset`, for static files like CSS, JavaScript, images, or plain bytes:
|
|
||||||
```rust
|
|
||||||
use servable::{StaticAsset, mime::MimeType};
|
|
||||||
|
|
||||||
let asset = StaticAsset {
|
|
||||||
bytes: b"body { color: red; }",
|
|
||||||
mime: MimeType::Css,
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
- `Redirect`, for simple http redirects:
|
|
||||||
```rust
|
|
||||||
use servable::Redirect;
|
|
||||||
|
|
||||||
let redirect = Redirect::new("/new-location").unwrap();
|
|
||||||
```
|
|
||||||
|
|
||||||
- `HtmlPage`, for dynamically-rendered HTML pages
|
|
||||||
```rust
|
|
||||||
use servable::{HtmlPage, PageMetadata};
|
|
||||||
use maud::html;
|
|
||||||
use std::pin::Pin;
|
|
||||||
|
|
||||||
let page = HtmlPage::default()
|
|
||||||
.with_meta(PageMetadata {
|
|
||||||
title: "My Page".into(),
|
|
||||||
description: Some("A great page".into()),
|
|
||||||
..Default::default()
|
|
||||||
})
|
|
||||||
.with_render(|_page, ctx| {
|
|
||||||
Box::pin(async move {
|
|
||||||
html! {
|
|
||||||
h1 { "Welcome!" }
|
|
||||||
p { "Route: " (ctx.route) }
|
|
||||||
}
|
|
||||||
})
|
|
||||||
});
|
|
||||||
```
|
|
||||||
`HtmlPage` automatically generates a `<head>` and wraps its rendered html in `<html><body>`.
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## `ServableRouter`
|
|
||||||
|
|
||||||
A `ServableRouter` exposes a collection of `Servable`s under different routes. It implements `tower`'s `Service` trait, and can be easily be converted into an Axum `Router`. Construct one as follows:
|
|
||||||
|
|
||||||
```rust
|
|
||||||
# use servable::{ServableRouter, StaticAsset, mime::MimeType};
|
|
||||||
# let home_page = StaticAsset { bytes: b"home", mime: MimeType::Html };
|
|
||||||
# let about_page = StaticAsset { bytes: b"about", mime: MimeType::Html };
|
|
||||||
# let stylesheet = StaticAsset { bytes: b"css", mime: MimeType::Css };
|
|
||||||
# let custom_404_page = StaticAsset { bytes: b"404", mime: MimeType::Html };
|
|
||||||
let route = ServableRouter::new()
|
|
||||||
.add_page("/", home_page)
|
|
||||||
.add_page("/about", about_page)
|
|
||||||
.add_page("/style.css", stylesheet)
|
|
||||||
.with_404(custom_404_page); // override default 404
|
|
||||||
```
|
|
||||||
|
|
||||||
# Features
|
|
||||||
- `image`: enable image transformation via query parameters. This makes `tokio` a dependency. \
|
|
||||||
When this is enabled, all `StaticAssets` with a valid mimetype can take an optional `t=` query parameter. \
|
|
||||||
See the `TransformerEnum` in this crate's documentation for details.
|
|
||||||
|
|
||||||
When `image` is enabled, the image below...
|
|
||||||
```rust
|
|
||||||
# use servable::{ServableRouter, StaticAsset, mime::MimeType};
|
|
||||||
let route = ServableRouter::new()
|
|
||||||
.add_page(
|
|
||||||
"/image.png",
|
|
||||||
StaticAsset {
|
|
||||||
bytes: b"fake image data",
|
|
||||||
mime: MimeType::Png,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
```
|
|
||||||
...may be accessed as follows:
|
|
||||||
|
|
||||||
```r
|
|
||||||
# Original image
|
|
||||||
GET /image.png
|
|
||||||
|
|
||||||
# Resize to max 800px on longest side
|
|
||||||
GET /image.png?t=maxdim(800)
|
|
||||||
|
|
||||||
# Crop to a 400x400 square at the center of the image
|
|
||||||
GET /image.png?t=crop(400,400,c)
|
|
||||||
|
|
||||||
# Chain transformations and transcode
|
|
||||||
GET /image.png?t=maxdim(800);crop(400,400);format(webp)
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
- `htmx-2.0.8`: Include htmx sources in the compiled executable. \
|
|
||||||
Use as follows:
|
|
||||||
```rust
|
|
||||||
# use servable::ServableRouter;
|
|
||||||
# #[cfg(feature = "htmx-2.0.8")]
|
|
||||||
let route = ServableRouter::new()
|
|
||||||
.add_page("/htmx.js", servable::HTMX_2_0_8)
|
|
||||||
.add_page("/htmx-json-enc.js", servable::EXT_JSON_1_19_12);
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## Caching and cache-busting
|
|
||||||
|
|
||||||
Control caching behavior per servable:
|
|
||||||
|
|
||||||
```rust
|
|
||||||
use chrono::TimeDelta;
|
|
||||||
use servable::HtmlPage;
|
|
||||||
|
|
||||||
let page = HtmlPage::default()
|
|
||||||
.with_ttl(Some(TimeDelta::hours(1)))
|
|
||||||
.with_immutable(false);
|
|
||||||
```
|
|
||||||
|
|
||||||
Headers are automatically generated:
|
|
||||||
- `Cache-Control: public, max-age=3600`
|
|
||||||
- `Cache-Control: immutable, public, max-age=31536000` (for immutable assets)
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "servable"
|
name = "servable"
|
||||||
description = "A tiny web stack featuring htmx, Axum, Rust, and Maud."
|
description = "A tiny web stack built around htmx, Axum, and Maud."
|
||||||
keywords = ["htmx", "web", "webui", "maud", "framework"]
|
keywords = ["htmx", "web", "webui", "maud", "framework"]
|
||||||
categories = ["web-programming", "web-programming::http-server"]
|
categories = ["web-programming", "web-programming::http-server"]
|
||||||
rust-version = { workspace = true }
|
rust-version = { workspace = true }
|
||||||
|
|||||||
@@ -4,14 +4,12 @@
|
|||||||
[](https://crates.io/crates/servable)
|
[](https://crates.io/crates/servable)
|
||||||
[](https://docs.rs/servable/)
|
[](https://docs.rs/servable/)
|
||||||
|
|
||||||
A tiny, convenient web micro-framework built around [htmx](https://htmx.org), [Axum](https://github.com/tokio-rs/axum), and [Maud](https://maud.lambda.xyz).
|
A minimal, convenient web micro-framework built around [htmx](https://htmx.org), [Axum](https://github.com/tokio-rs/axum), and [Maud](https://maud.lambda.xyz). \
|
||||||
Inspired by the "MASH" stack described [here](https://yree.io/mash) and [here](https://emschwartz.me/building-a-fast-website-with-the-mash-stack-in-rust).
|
This powers [my homepage](https://betalupi.com). See example usage [here](https://git.betalupi.com/Mark/webpage/src/branch/main/crates/service/service-webpage/src/routes/mod.rs).
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
`servable` provides abstractions that implement common utilities needed by an http server. \
|
`servable` provides abstractions that implement common utilities needed by an http server.
|
||||||
|
|
||||||
- response headers and cache-busting utilities
|
- response headers and cache-busting utilities
|
||||||
- client device detection (mobile / desktop)
|
- client device detection (mobile / desktop)
|
||||||
@@ -63,6 +61,7 @@ The `Servable` trait is the foundation of this stack. \
|
|||||||
let asset = StaticAsset {
|
let asset = StaticAsset {
|
||||||
bytes: b"body { color: red; }",
|
bytes: b"body { color: red; }",
|
||||||
mime: MimeType::Css,
|
mime: MimeType::Css,
|
||||||
|
ttl: StaticAsset::DEFAULT_TTL
|
||||||
};
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -104,10 +103,10 @@ A `ServableRouter` exposes a collection of `Servable`s under different routes. I
|
|||||||
|
|
||||||
```rust
|
```rust
|
||||||
# use servable::{ServableRouter, StaticAsset, mime::MimeType};
|
# use servable::{ServableRouter, StaticAsset, mime::MimeType};
|
||||||
# let home_page = StaticAsset { bytes: b"home", mime: MimeType::Html };
|
# let home_page = StaticAsset { bytes: b"home", mime: MimeType::Html, ttl: StaticAsset::DEFAULT_TTL};
|
||||||
# let about_page = StaticAsset { bytes: b"about", mime: MimeType::Html };
|
# let about_page = StaticAsset { bytes: b"about", mime: MimeType::Html, ttl: StaticAsset::DEFAULT_TTL };
|
||||||
# let stylesheet = StaticAsset { bytes: b"css", mime: MimeType::Css };
|
# let stylesheet = StaticAsset { bytes: b"css", mime: MimeType::Css, ttl: StaticAsset::DEFAULT_TTL };
|
||||||
# let custom_404_page = StaticAsset { bytes: b"404", mime: MimeType::Html };
|
# let custom_404_page = StaticAsset { bytes: b"404", mime: MimeType::Html, ttl: StaticAsset::DEFAULT_TTL };
|
||||||
let route = ServableRouter::new()
|
let route = ServableRouter::new()
|
||||||
.add_page("/", home_page)
|
.add_page("/", home_page)
|
||||||
.add_page("/about", about_page)
|
.add_page("/about", about_page)
|
||||||
@@ -129,6 +128,7 @@ let route = ServableRouter::new()
|
|||||||
StaticAsset {
|
StaticAsset {
|
||||||
bytes: b"fake image data",
|
bytes: b"fake image data",
|
||||||
mime: MimeType::Png,
|
mime: MimeType::Png,
|
||||||
|
ttl: StaticAsset::DEFAULT_TTL
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
```
|
```
|
||||||
@@ -139,13 +139,13 @@ let route = ServableRouter::new()
|
|||||||
GET /image.png
|
GET /image.png
|
||||||
|
|
||||||
# Resize to max 800px on longest side
|
# Resize to max 800px on longest side
|
||||||
GET /image.png?t=maxdim(800)
|
GET /image.png?t=maxdim(800,800)
|
||||||
|
|
||||||
# Crop to a 400x400 square at the center of the image
|
# Crop to a 400x400 square at the center of the image
|
||||||
GET /image.png?t=crop(400,400,c)
|
GET /image.png?t=crop(400,400,c)
|
||||||
|
|
||||||
# Chain transformations and transcode
|
# Chain transformations and transcode
|
||||||
GET /image.png?t=maxdim(800);crop(400,400);format(webp)
|
GET /image.png?t=maxdim(800,800);crop(400,400);format(webp)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
@@ -171,9 +171,37 @@ use servable::HtmlPage;
|
|||||||
|
|
||||||
let page = HtmlPage::default()
|
let page = HtmlPage::default()
|
||||||
.with_ttl(Some(TimeDelta::hours(1)))
|
.with_ttl(Some(TimeDelta::hours(1)))
|
||||||
.with_immutable(false);
|
.with_private(false);
|
||||||
```
|
```
|
||||||
|
|
||||||
Headers are automatically generated:
|
Headers are automatically generated:
|
||||||
- `Cache-Control: public, max-age=3600`
|
- `Cache-Control: public, max-age=3600` (default)
|
||||||
- `Cache-Control: immutable, public, max-age=31536000` (for immutable assets)
|
- `Cache-Control: private, max-age=31536000` (if `private` is true)
|
||||||
|
|
||||||
|
We also provide a static `CACHE_BUST_STR`, which may be formatted into urls to force cache refresh
|
||||||
|
whenever the server is restarted:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use chrono::TimeDelta;
|
||||||
|
use servable::{HtmlPage, CACHE_BUST_STR, ServableWithRoute, StaticAsset, ServableRouter};
|
||||||
|
use servable::mime::MimeType;
|
||||||
|
|
||||||
|
pub static HTMX: ServableWithRoute<StaticAsset> = ServableWithRoute::new(
|
||||||
|
|| format!("/{}/main.css", *CACHE_BUST_STR),
|
||||||
|
StaticAsset {
|
||||||
|
bytes: "div{}".as_bytes(),
|
||||||
|
mime: MimeType::Css,
|
||||||
|
ttl: StaticAsset::DEFAULT_TTL,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
let route = HTMX.route();
|
||||||
|
println!("Css is at {route}");
|
||||||
|
|
||||||
|
let router = ServableRouter::new()
|
||||||
|
.add_page_with_route(&HTMX);
|
||||||
|
```
|
||||||
|
|
||||||
|
## TODO:
|
||||||
|
- cache-busting fonts in css is not possible, we need to dynamic replace urls
|
||||||
|
|||||||
@@ -1,4 +1,8 @@
|
|||||||
#![doc = include_str!("../README.md")]
|
#![doc = include_str!("../README.md")]
|
||||||
|
// readme is symlinked to the root of this repo
|
||||||
|
// because `cargo publish` works from a different dir,
|
||||||
|
// and needs a different relative path than cargo build.
|
||||||
|
// https://github.com/rust-lang/cargo/issues/13309
|
||||||
|
|
||||||
pub mod mime;
|
pub mod mime;
|
||||||
|
|
||||||
@@ -44,6 +48,7 @@ pub static CACHE_BUST_STR: std::sync::LazyLock<String> = std::sync::LazyLock::ne
|
|||||||
pub const HTMX_2_0_8: servable::StaticAsset = servable::StaticAsset {
|
pub const HTMX_2_0_8: servable::StaticAsset = servable::StaticAsset {
|
||||||
bytes: include_str!("../htmx/htmx-2.0.8.min.js").as_bytes(),
|
bytes: include_str!("../htmx/htmx-2.0.8.min.js").as_bytes(),
|
||||||
mime: mime::MimeType::Javascript,
|
mime: mime::MimeType::Javascript,
|
||||||
|
ttl: StaticAsset::DEFAULT_TTL,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// HTMX json extension, 1.19.2.
|
/// HTMX json extension, 1.19.2.
|
||||||
@@ -53,4 +58,5 @@ pub const HTMX_2_0_8: servable::StaticAsset = servable::StaticAsset {
|
|||||||
pub const EXT_JSON_1_19_12: servable::StaticAsset = servable::StaticAsset {
|
pub const EXT_JSON_1_19_12: servable::StaticAsset = servable::StaticAsset {
|
||||||
bytes: include_str!("../htmx/json-enc-1.9.12.js").as_bytes(),
|
bytes: include_str!("../htmx/json-enc-1.9.12.js").as_bytes(),
|
||||||
mime: mime::MimeType::Javascript,
|
mime: mime::MimeType::Javascript,
|
||||||
|
ttl: StaticAsset::DEFAULT_TTL,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -225,6 +225,13 @@ impl<'de> Deserialize<'de> for MimeType {
|
|||||||
|
|
||||||
impl Default for MimeType {
|
impl Default for MimeType {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
|
Self::const_default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MimeType {
|
||||||
|
/// [Default::default], but const
|
||||||
|
pub const fn const_default() -> Self {
|
||||||
Self::Blob
|
Self::Blob
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,9 +35,9 @@ impl Servable for Default404 {
|
|||||||
code: StatusCode::NOT_FOUND,
|
code: StatusCode::NOT_FOUND,
|
||||||
body: (),
|
body: (),
|
||||||
ttl: Some(TimeDelta::days(1)),
|
ttl: Some(TimeDelta::days(1)),
|
||||||
immutable: true,
|
|
||||||
headers: HeaderMap::new(),
|
headers: HeaderMap::new(),
|
||||||
mime: Some(MimeType::Html),
|
mime: Some(MimeType::Html),
|
||||||
|
private: false,
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -73,6 +73,7 @@ impl Servable for Default404 {
|
|||||||
/// StaticAsset {
|
/// StaticAsset {
|
||||||
/// bytes: "I am a page".as_bytes(),
|
/// bytes: "I am a page".as_bytes(),
|
||||||
/// mime: MimeType::Text,
|
/// mime: MimeType::Text,
|
||||||
|
/// ttl: StaticAsset::DEFAULT_TTL
|
||||||
/// },
|
/// },
|
||||||
/// );
|
/// );
|
||||||
///
|
///
|
||||||
@@ -243,14 +244,15 @@ impl Service<Request<Body>> for ServableRouter {
|
|||||||
// Tweak headers
|
// Tweak headers
|
||||||
{
|
{
|
||||||
if !rend.headers.contains_key(header::CACHE_CONTROL) {
|
if !rend.headers.contains_key(header::CACHE_CONTROL) {
|
||||||
let max_age = rend.ttl.map(|x| x.num_seconds()).unwrap_or(1).max(1);
|
let max_age = rend.ttl.map(|x| x.num_seconds()).unwrap_or(0).max(0);
|
||||||
|
|
||||||
let mut value = String::new();
|
let mut value = String::new();
|
||||||
if rend.immutable {
|
|
||||||
value.push_str("immutable, ");
|
|
||||||
}
|
|
||||||
|
|
||||||
value.push_str("public, ");
|
value.push_str(match rend.private {
|
||||||
|
true => "private, ",
|
||||||
|
false => "public, ",
|
||||||
|
});
|
||||||
|
|
||||||
value.push_str(&format!("max-age={}, ", max_age));
|
value.push_str(&format!("max-age={}, ", max_age));
|
||||||
|
|
||||||
#[expect(clippy::unwrap_used)]
|
#[expect(clippy::unwrap_used)]
|
||||||
|
|||||||
@@ -4,8 +4,6 @@ use std::pin::Pin;
|
|||||||
|
|
||||||
use crate::{RenderContext, Rendered, RenderedBody, mime::MimeType, servable::Servable};
|
use crate::{RenderContext, Rendered, RenderedBody, mime::MimeType, servable::Servable};
|
||||||
|
|
||||||
const TTL: Option<TimeDelta> = Some(TimeDelta::days(1));
|
|
||||||
|
|
||||||
/// A static blob of bytes
|
/// A static blob of bytes
|
||||||
pub struct StaticAsset {
|
pub struct StaticAsset {
|
||||||
/// The data to return
|
/// The data to return
|
||||||
@@ -13,6 +11,21 @@ pub struct StaticAsset {
|
|||||||
|
|
||||||
/// The type of `bytes`
|
/// The type of `bytes`
|
||||||
pub mime: MimeType,
|
pub mime: MimeType,
|
||||||
|
|
||||||
|
/// How long to cache this response.
|
||||||
|
/// If None, never cache
|
||||||
|
pub ttl: Option<TimeDelta>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl StaticAsset {
|
||||||
|
/// Default ttl of a [StaticAsset]
|
||||||
|
pub const DEFAULT_TTL: Option<TimeDelta> = Some(TimeDelta::days(14));
|
||||||
|
|
||||||
|
/// Set `self.ttl`
|
||||||
|
pub const fn with_ttl(mut self, ttl: Option<TimeDelta>) -> Self {
|
||||||
|
self.ttl = ttl;
|
||||||
|
self
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "image")]
|
#[cfg(feature = "image")]
|
||||||
@@ -36,8 +49,8 @@ impl Servable for StaticAsset {
|
|||||||
return Rendered {
|
return Rendered {
|
||||||
code: StatusCode::BAD_REQUEST,
|
code: StatusCode::BAD_REQUEST,
|
||||||
body: (),
|
body: (),
|
||||||
ttl: TTL,
|
ttl: self.ttl,
|
||||||
immutable: true,
|
private: false,
|
||||||
|
|
||||||
headers: HeaderMap::new(),
|
headers: HeaderMap::new(),
|
||||||
mime: None,
|
mime: None,
|
||||||
@@ -51,8 +64,8 @@ impl Servable for StaticAsset {
|
|||||||
return Rendered {
|
return Rendered {
|
||||||
code: StatusCode::OK,
|
code: StatusCode::OK,
|
||||||
body: (),
|
body: (),
|
||||||
ttl: TTL,
|
ttl: self.ttl,
|
||||||
immutable: true,
|
private: false,
|
||||||
|
|
||||||
headers: HeaderMap::new(),
|
headers: HeaderMap::new(),
|
||||||
mime: Some(
|
mime: Some(
|
||||||
@@ -67,8 +80,8 @@ impl Servable for StaticAsset {
|
|||||||
return Rendered {
|
return Rendered {
|
||||||
code: StatusCode::OK,
|
code: StatusCode::OK,
|
||||||
body: (),
|
body: (),
|
||||||
ttl: TTL,
|
ttl: self.ttl,
|
||||||
immutable: true,
|
private: false,
|
||||||
|
|
||||||
headers: HeaderMap::new(),
|
headers: HeaderMap::new(),
|
||||||
mime: Some(self.mime.clone()),
|
mime: Some(self.mime.clone()),
|
||||||
@@ -99,8 +112,8 @@ impl Servable for StaticAsset {
|
|||||||
return Rendered {
|
return Rendered {
|
||||||
code: StatusCode::BAD_REQUEST,
|
code: StatusCode::BAD_REQUEST,
|
||||||
body: RenderedBody::String(err),
|
body: RenderedBody::String(err),
|
||||||
ttl: TTL,
|
ttl: self.ttl,
|
||||||
immutable: true,
|
private: false,
|
||||||
|
|
||||||
headers: HeaderMap::new(),
|
headers: HeaderMap::new(),
|
||||||
mime: None,
|
mime: None,
|
||||||
@@ -131,7 +144,7 @@ impl Servable for StaticAsset {
|
|||||||
"Error while transforming image: {error:?}"
|
"Error while transforming image: {error:?}"
|
||||||
)),
|
)),
|
||||||
ttl: None,
|
ttl: None,
|
||||||
immutable: true,
|
private: false,
|
||||||
|
|
||||||
headers: HeaderMap::new(),
|
headers: HeaderMap::new(),
|
||||||
mime: None,
|
mime: None,
|
||||||
@@ -144,8 +157,8 @@ impl Servable for StaticAsset {
|
|||||||
return Rendered {
|
return Rendered {
|
||||||
code: StatusCode::OK,
|
code: StatusCode::OK,
|
||||||
body: RenderedBody::Bytes(bytes),
|
body: RenderedBody::Bytes(bytes),
|
||||||
ttl: TTL,
|
ttl: self.ttl,
|
||||||
immutable: true,
|
private: false,
|
||||||
|
|
||||||
headers: HeaderMap::new(),
|
headers: HeaderMap::new(),
|
||||||
mime: Some(mime),
|
mime: Some(mime),
|
||||||
@@ -156,8 +169,8 @@ impl Servable for StaticAsset {
|
|||||||
return Rendered {
|
return Rendered {
|
||||||
code: StatusCode::INTERNAL_SERVER_ERROR,
|
code: StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
body: RenderedBody::String(format!("{err}")),
|
body: RenderedBody::String(format!("{err}")),
|
||||||
ttl: TTL,
|
ttl: self.ttl,
|
||||||
immutable: true,
|
private: false,
|
||||||
|
|
||||||
headers: HeaderMap::new(),
|
headers: HeaderMap::new(),
|
||||||
mime: None,
|
mime: None,
|
||||||
@@ -170,8 +183,9 @@ impl Servable for StaticAsset {
|
|||||||
return Rendered {
|
return Rendered {
|
||||||
code: StatusCode::OK,
|
code: StatusCode::OK,
|
||||||
body: RenderedBody::Static(self.bytes),
|
body: RenderedBody::Static(self.bytes),
|
||||||
ttl: TTL,
|
ttl: self.ttl,
|
||||||
immutable: true,
|
private: false,
|
||||||
|
|
||||||
headers: HeaderMap::new(),
|
headers: HeaderMap::new(),
|
||||||
mime: Some(self.mime.clone()),
|
mime: Some(self.mime.clone()),
|
||||||
};
|
};
|
||||||
@@ -191,8 +205,8 @@ impl Servable for StaticAsset {
|
|||||||
return Rendered {
|
return Rendered {
|
||||||
code: StatusCode::OK,
|
code: StatusCode::OK,
|
||||||
body: (),
|
body: (),
|
||||||
ttl: TTL,
|
ttl: self.ttl,
|
||||||
immutable: true,
|
private: false,
|
||||||
|
|
||||||
headers: HeaderMap::new(),
|
headers: HeaderMap::new(),
|
||||||
mime: Some(self.mime.clone()),
|
mime: Some(self.mime.clone()),
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ pub struct HtmlPage {
|
|||||||
pub meta: PageMetadata,
|
pub meta: PageMetadata,
|
||||||
|
|
||||||
/// If true, the contents of this page never change
|
/// If true, the contents of this page never change
|
||||||
pub immutable: bool,
|
pub private: bool,
|
||||||
|
|
||||||
/// How long this page's html may be cached.
|
/// How long this page's html may be cached.
|
||||||
/// This controls the maximum age of a page shown to the user.
|
/// This controls the maximum age of a page shown to the user.
|
||||||
@@ -94,7 +94,7 @@ impl Default for HtmlPage {
|
|||||||
HtmlPage {
|
HtmlPage {
|
||||||
// No cache by default
|
// No cache by default
|
||||||
ttl: None,
|
ttl: None,
|
||||||
immutable: false,
|
private: false,
|
||||||
|
|
||||||
meta: Default::default(),
|
meta: Default::default(),
|
||||||
render: Arc::new(|_, _| Box::pin(async { html!() })),
|
render: Arc::new(|_, _| Box::pin(async { html!() })),
|
||||||
@@ -132,10 +132,10 @@ impl HtmlPage {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set `self.immutable`
|
/// Set `self.private`
|
||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
pub fn with_immutable(mut self, immutable: bool) -> Self {
|
pub fn with_private(mut self, private: bool) -> Self {
|
||||||
self.immutable = immutable;
|
self.private = private;
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -223,7 +223,7 @@ impl Servable for HtmlPage {
|
|||||||
code: self.response_code,
|
code: self.response_code,
|
||||||
body: (),
|
body: (),
|
||||||
ttl: self.ttl,
|
ttl: self.ttl,
|
||||||
immutable: self.immutable,
|
private: self.private,
|
||||||
headers: HeaderMap::new(),
|
headers: HeaderMap::new(),
|
||||||
mime: Some(MimeType::Html),
|
mime: Some(MimeType::Html),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ impl Servable for Redirect {
|
|||||||
headers,
|
headers,
|
||||||
body: (),
|
body: (),
|
||||||
ttl: None,
|
ttl: None,
|
||||||
immutable: true,
|
private: false,
|
||||||
mime: None,
|
mime: None,
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -58,8 +58,8 @@ pub struct Rendered<T: RenderedBodyType> {
|
|||||||
/// If none, don't cache.
|
/// If none, don't cache.
|
||||||
pub ttl: Option<TimeDelta>,
|
pub ttl: Option<TimeDelta>,
|
||||||
|
|
||||||
/// If true, the data at this route will never change.
|
/// If true, this response sets `Cache-Control: private`
|
||||||
pub immutable: bool,
|
pub private: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Rendered<()> {
|
impl Rendered<()> {
|
||||||
@@ -71,7 +71,7 @@ impl Rendered<()> {
|
|||||||
body,
|
body,
|
||||||
mime: self.mime,
|
mime: self.mime,
|
||||||
ttl: self.ttl,
|
ttl: self.ttl,
|
||||||
immutable: self.immutable,
|
private: self.private,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user