Skip to content

Commit

Permalink
Use Arc<T> wrapper types to cut down on boilerplate
Browse files Browse the repository at this point in the history
In addition, move all route setup into the top-level main.rs source.
  • Loading branch information
matze committed Feb 8, 2025
1 parent 5f6bf4a commit 79d7c3d
Show file tree
Hide file tree
Showing 5 changed files with 83 additions and 91 deletions.
94 changes: 55 additions & 39 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,11 @@ use crate::assets::{Asset, CssAssets, Kind};
use crate::cache::Cache;
use crate::db::Database;
use crate::errors::Error;
use crate::highlight::Highlighter;
use axum::extract::{DefaultBodyLimit, FromRef, Request, State};
use axum::http::{HeaderName, HeaderValue, StatusCode};
use axum::middleware::{from_fn, from_fn_with_state, Next};
use axum::response::{IntoResponse, Response};
use axum::routing::get;
use axum::routing::{get, Router};
use axum_extra::extract::cookie::Key;
use highlight::Theme;
use http::header::{
Expand All @@ -23,7 +22,6 @@ use tower::ServiceBuilder;
use tower_http::compression::CompressionLayer;
use tower_http::timeout::TimeoutLayer;
use tower_http::trace::TraceLayer;
use url::Url;

mod assets;
mod cache;
Expand All @@ -40,28 +38,52 @@ mod test_helpers;

static PACKAGE_NAME: &str = env!("CARGO_PKG_NAME");

pub struct Page {
version: &'static str,
title: String,
assets: Assets,
base_url: Url,
pub mod page {
use crate::Assets;
use url::Url;

pub struct Page {
pub version: &'static str,
pub title: String,
pub assets: Assets,
pub base_url: Url,
}

impl Page {
/// Create new page meta data from generated `assets`, `title` and optional `base_url`.
#[must_use]
pub fn new(assets: Assets, title: String, base_url: Url) -> Self {
Self {
version: env!("CARGO_PKG_VERSION"),
title,
assets,
base_url,
}
}
}
}

/// Reference counted [`page::Page`] wrapper.
pub type Page = Arc<page::Page>;

pub struct Assets {
favicon: Asset,
css: CssAssets,
index_js: Asset,
paste_js: Asset,
}

/// Reference counted [`highlight::Highlighter`] wrapper.
pub type Highlighter = Arc<highlight::Highlighter>;

#[derive(Clone)]
pub struct AppState {
db: Database,
cache: Cache,
key: Key,
max_expiration: Option<NonZeroU32>,
page: Arc<Page>,
highlighter: Arc<Highlighter>,
page: Page,
highlighter: Highlighter,
}

impl FromRef<AppState> for Key {
Expand All @@ -70,13 +92,13 @@ impl FromRef<AppState> for Key {
}
}

impl FromRef<AppState> for Arc<Highlighter> {
impl FromRef<AppState> for Highlighter {
fn from_ref(state: &AppState) -> Self {
state.highlighter.clone()
}
}

impl FromRef<AppState> for Arc<Page> {
impl FromRef<AppState> for Page {
fn from_ref(state: &AppState) -> Self {
state.page.clone()
}
Expand Down Expand Up @@ -132,23 +154,7 @@ impl Assets {
}
}

impl Page {
/// Create new page meta data from generated `assets`, `title` and optional `base_url`.
fn new(assets: Assets, title: String, base_url: Url) -> Self {
Self {
version: env!("CARGO_PKG_VERSION"),
title,
assets,
base_url,
}
}
}

async fn handle_service_errors(
State(page): State<Arc<Page>>,
req: Request,
next: Next,
) -> Response {
async fn handle_service_errors(State(page): State<Page>, req: Request, next: Next) -> Response {
let response = next.run(req).await;

match response.status() {
Expand Down Expand Up @@ -192,27 +198,27 @@ async fn shutdown_signal() {
tracing::info!("received signal, exiting ...");
}

async fn favicon(State(page): State<Arc<Page>>) -> impl IntoResponse {
async fn favicon(State(page): State<Page>) -> impl IntoResponse {
page.assets.favicon.clone()
}

async fn style_css(State(page): State<Arc<Page>>) -> impl IntoResponse {
async fn style_css(State(page): State<Page>) -> impl IntoResponse {
page.assets.css.style.clone()
}

async fn dark_css(State(page): State<Arc<Page>>) -> impl IntoResponse {
async fn dark_css(State(page): State<Page>) -> impl IntoResponse {
page.assets.css.dark.clone()
}

async fn light_css(State(page): State<Arc<Page>>) -> impl IntoResponse {
async fn light_css(State(page): State<Page>) -> impl IntoResponse {
page.assets.css.light.clone()
}

async fn index_js(State(page): State<Arc<Page>>) -> impl IntoResponse {
async fn index_js(State(page): State<Page>) -> impl IntoResponse {
page.assets.index_js.clone()
}

async fn paste_js(State(page): State<Arc<Page>>) -> impl IntoResponse {
async fn paste_js(State(page): State<Page>) -> impl IntoResponse {
page.assets.paste_js.clone()
}

Expand All @@ -222,13 +228,22 @@ async fn serve(
timeout: Duration,
max_body_size: usize,
) -> Result<(), Box<dyn std::error::Error>> {
let app = routes::routes()
let app = Router::new()
.route(state.page.assets.favicon.route(), get(favicon))
.route(state.page.assets.css.style.route(), get(style_css))
.route(state.page.assets.css.dark.route(), get(dark_css))
.route(state.page.assets.css.light.route(), get(light_css))
.route(state.page.assets.index_js.route(), get(index_js))
.route(state.page.assets.paste_js.route(), get(paste_js))
.route("/", get(routes::index).post(routes::paste::insert))
.route(
"/:id",
get(routes::paste::get)
.post(routes::paste::get)
.delete(routes::paste::delete),
)
.route("/burn/:id", get(routes::paste::burn_created))
.route("/delete/:id", get(routes::paste::delete))
.layer(
ServiceBuilder::new()
.layer(DefaultBodyLimit::max(max_body_size))
Expand Down Expand Up @@ -271,14 +286,15 @@ async fn start() -> Result<(), Box<dyn std::error::Error>> {
tracing::debug!("maximum expiration time of {max_expiration:?} seconds");

let assets = Assets::new(theme);
let page = Page::new(assets, title, base_url);
let page = Arc::new(page::Page::new(assets, title, base_url));
let highlighter = Arc::new(highlight::Highlighter::default());
let state = AppState {
db,
cache,
key,
max_expiration,
page: Arc::new(page),
highlighter: Arc::new(Highlighter::default()),
page,
highlighter,
};

let listener = TcpListener::bind(&addr).await?;
Expand Down
44 changes: 17 additions & 27 deletions src/pages.rs
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
use crate::cache::Key as CacheKey;
use crate::highlight::{Highlighter, Html};
use crate::highlight::Html;
use crate::routes::paste::{Format, QueryData};
use crate::{errors, Page};
use crate::{errors, Highlighter, Page};
use askama::Template;
use axum::http::StatusCode;
use std::num::{NonZero, NonZeroU32};
use std::sync::{Arc, OnceLock};
use std::sync::OnceLock;

/// Error page showing a message.
#[derive(Template)]
#[template(path = "error.html")]
pub struct Error {
page: Arc<Page>,
page: Page,
description: String,
}

Expand All @@ -20,14 +20,14 @@ pub type ErrorResponse = (StatusCode, Error);

/// Create an error response from `error` consisting of [`StatusCode`] derive from `error` as well
/// as a rendered page with a description.
pub fn make_error(error: errors::Error, page: Arc<Page>) -> ErrorResponse {
pub fn make_error(error: errors::Error, page: Page) -> ErrorResponse {
let description = error.to_string();
(error.into(), Error { page, description })
}

impl Error {
/// Create new [`Error`] from `description`.
pub fn new(description: String, page: Arc<Page>) -> Self {
pub fn new(description: String, page: Page) -> Self {
Self { page, description }
}
}
Expand All @@ -36,17 +36,13 @@ impl Error {
#[derive(Template)]
#[template(path = "index.html")]
pub struct Index {
page: Arc<Page>,
page: Page,
max_expiration: Option<NonZeroU32>,
highlighter: Arc<Highlighter>,
highlighter: Highlighter,
}

impl Index {
pub fn new(
max_expiration: Option<NonZeroU32>,
page: Arc<Page>,
highlighter: Arc<Highlighter>,
) -> Self {
pub fn new(max_expiration: Option<NonZeroU32>, page: Page, highlighter: Highlighter) -> Self {
Self {
page,
max_expiration,
Expand Down Expand Up @@ -128,7 +124,7 @@ impl Index {
#[derive(Template)]
#[template(path = "formatted.html")]
pub struct Paste {
page: Arc<Page>,
page: Page,
id: String,
ext: String,
can_delete: bool,
Expand All @@ -138,13 +134,7 @@ pub struct Paste {

impl Paste {
/// Construct new paste view from cache `key` and paste `html`.
pub fn new(
key: CacheKey,
html: Html,
can_delete: bool,
title: String,
page: Arc<Page>,
) -> Self {
pub fn new(key: CacheKey, html: Html, can_delete: bool, title: String, page: Page) -> Self {
let html = html.into_inner();

Self {
Expand All @@ -162,15 +152,15 @@ impl Paste {
#[derive(Template)]
#[template(path = "encrypted.html")]
pub struct Encrypted {
page: Arc<Page>,
page: Page,
id: String,
ext: String,
query: String,
}

impl Encrypted {
/// Construct new paste view from cache `key` and paste `html`.
pub fn new(key: CacheKey, query: &QueryData, page: Arc<Page>) -> Self {
pub fn new(key: CacheKey, query: &QueryData, page: Page) -> Self {
let query = match query.fmt {
Some(Format::Raw) => "?fmt=raw".to_string(),
Some(Format::Qr) => "?fmt=qr".to_string(),
Expand Down Expand Up @@ -200,7 +190,7 @@ fn dark_modules(code: &qrcodegen::QrCode) -> Vec<(i32, i32)> {
#[derive(Template)]
#[template(path = "qr.html", escape = "none")]
pub struct Qr {
page: Arc<Page>,
page: Page,
id: String,
ext: String,
can_delete: bool,
Expand All @@ -210,7 +200,7 @@ pub struct Qr {

impl Qr {
/// Construct new QR code view from `code`.
pub fn new(code: qrcodegen::QrCode, key: CacheKey, title: String, page: Arc<Page>) -> Self {
pub fn new(code: qrcodegen::QrCode, key: CacheKey, title: String, page: Page) -> Self {
Self {
page,
id: key.id(),
Expand All @@ -230,14 +220,14 @@ impl Qr {
#[derive(Template)]
#[template(path = "burn.html", escape = "none")]
pub struct Burn {
page: Arc<Page>,
page: Page,
id: String,
code: qrcodegen::QrCode,
}

impl Burn {
/// Construct new burn page linking to `id`.
pub fn new(code: qrcodegen::QrCode, id: String, page: Arc<Page>) -> Self {
pub fn new(code: qrcodegen::QrCode, id: String, page: Page) -> Self {
Self { page, id, code }
}

Expand Down
14 changes: 1 addition & 13 deletions src/routes/mod.rs
Original file line number Diff line number Diff line change
@@ -1,31 +1,19 @@
use crate::pages::Index;
use crate::AppState;
use axum::extract::State;
use axum::routing::{get, Router};

mod form;
mod json;
pub(crate) mod paste;

async fn index(state: State<AppState>) -> Index {
pub async fn index(state: State<AppState>) -> Index {
Index::new(
state.max_expiration,
state.page.clone(),
state.highlighter.clone(),
)
}

pub fn routes() -> Router<AppState> {
Router::new()
.route("/", get(index).post(paste::insert))
.route(
"/:id",
get(paste::get).post(paste::get).delete(paste::delete),
)
.route("/burn/:id", get(paste::burn_created))
.route("/delete/:id", get(paste::delete))
}

#[cfg(test)]
mod tests {
use crate::db::write::Entry;
Expand Down
Loading

0 comments on commit 79d7c3d

Please sign in to comment.