From b26c36293cd3243b0a57a3c252a7fd8f764cbd6e Mon Sep 17 00:00:00 2001 From: Kristofers Solo Date: Sat, 25 Jan 2025 16:14:33 +0200 Subject: [PATCH] refactor: use postgres --- Cargo.lock | 140 ++++++++++---------------- Cargo.toml | 6 +- app/Cargo.toml | 3 +- app/src/models/user.rs | 3 +- config/base.toml | 9 ++ config/local.toml | 5 + config/production.toml | 5 + justfile | 10 +- migrations/20250125123853_init.up.sql | 17 ++-- scripts/init_db | 46 +++++++++ server/Cargo.toml | 7 ++ server/src/config.rs | 114 +++++++++++++++++++++ server/src/db/mod.rs | 3 +- server/src/db/scores.rs | 0 server/src/db/user.rs | 13 +++ server/src/lib.rs | 3 + server/src/main.rs | 30 ++---- server/src/routes/health_check.rs | 5 + server/src/routes/mod.rs | 73 ++++++++++++++ server/src/startup.rs | 75 ++++++++++++++ 20 files changed, 439 insertions(+), 128 deletions(-) create mode 100644 config/base.toml create mode 100644 config/local.toml create mode 100644 config/production.toml create mode 100755 scripts/init_db create mode 100644 server/src/config.rs delete mode 100644 server/src/db/scores.rs create mode 100644 server/src/db/user.rs create mode 100644 server/src/routes/health_check.rs create mode 100644 server/src/routes/mod.rs create mode 100644 server/src/startup.rs diff --git a/Cargo.lock b/Cargo.lock index 19a23aa..4ee1252 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -111,6 +111,7 @@ dependencies = [ "tracing-bunyan-formatter", "tracing-log 0.2.0", "tracing-subscriber", + "uuid", ] [[package]] @@ -279,26 +280,6 @@ version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" -[[package]] -name = "bindgen" -version = "0.69.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "271383c67ccabffb7381723dea0672a673f292304fcb45c01cc648c7a8d58088" -dependencies = [ - "bitflags", - "cexpr", - "clang-sys", - "itertools 0.12.1", - "lazy_static", - "lazycell", - "proc-macro2", - "quote", - "regex", - "rustc-hash 1.1.0", - "shlex", - "syn", -] - [[package]] name = "bitflags" version = "2.8.0" @@ -373,15 +354,6 @@ dependencies = [ "shlex", ] -[[package]] -name = "cexpr" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" -dependencies = [ - "nom", -] - [[package]] name = "cfg-if" version = "1.0.0" @@ -403,17 +375,6 @@ dependencies = [ "windows-targets 0.52.6", ] -[[package]] -name = "clang-sys" -version = "1.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" -dependencies = [ - "glob", - "libc", - "libloading", -] - [[package]] name = "codee" version = "0.2.0" @@ -453,6 +414,18 @@ dependencies = [ "toml", ] +[[package]] +name = "config" +version = "0.15.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e329294a796e9b22329669c1f433a746983f9e324e07f4ef135be81bb2262de4" +dependencies = [ + "pathdiff", + "serde", + "toml", + "winnow", +] + [[package]] name = "const-oid" version = "0.9.6" @@ -901,12 +874,6 @@ version = "0.31.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" -[[package]] -name = "glob" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2" - [[package]] name = "gloo-net" version = "0.6.0" @@ -1337,15 +1304,6 @@ dependencies = [ "serde", ] -[[package]] -name = "itertools" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" -dependencies = [ - "either", -] - [[package]] name = "itertools" version = "0.13.0" @@ -1389,12 +1347,6 @@ dependencies = [ "spin", ] -[[package]] -name = "lazycell" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" - [[package]] name = "leptos" version = "0.7.4" @@ -1418,7 +1370,7 @@ dependencies = [ "paste", "rand", "reactive_graph", - "rustc-hash 2.1.0", + "rustc-hash", "send_wrapper", "serde", "serde_qs", @@ -1464,7 +1416,7 @@ version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5d874993c7664d757677d056c8f46b5cb5365fe622005e1bf26050f4996e7e52" dependencies = [ - "config", + "config 0.14.1", "regex", "serde", "thiserror 2.0.11", @@ -1530,7 +1482,7 @@ dependencies = [ "cfg-if", "convert_case", "html-escape", - "itertools 0.13.0", + "itertools", "leptos_hot_reload", "prettyplease", "proc-macro-error2", @@ -1622,16 +1574,6 @@ version = "0.2.169" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" -[[package]] -name = "libloading" -version = "0.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc2f4eb4bc735547cfed7c0a4922cbd04a4655978c09b54f1f7b228750664c34" -dependencies = [ - "cfg-if", - "windows-targets 0.48.5", -] - [[package]] name = "libm" version = "0.2.11" @@ -1644,7 +1586,6 @@ version = "0.30.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149" dependencies = [ - "bindgen", "pkg-config", "vcpkg", ] @@ -2191,7 +2132,7 @@ dependencies = [ "hydration_context", "or_poisoned", "pin-project-lite", - "rustc-hash 2.1.0", + "rustc-hash", "send_wrapper", "serde", "slotmap", @@ -2207,12 +2148,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "80bb1913eeb71f74028213455ee971550c2b3cb91b6acd5efa8a0f8dc59f5039" dependencies = [ "guardian", - "itertools 0.13.0", + "itertools", "or_poisoned", "paste", "reactive_graph", "reactive_stores_macro", - "rustc-hash 2.1.0", + "rustc-hash", ] [[package]] @@ -2322,12 +2263,6 @@ version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" -[[package]] -name = "rustc-hash" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" - [[package]] name = "rustc-hash" version = "2.1.0" @@ -2374,6 +2309,16 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "secrecy" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e891af845473308773346dc847b2c23ee78fe442e0472ac50e22a18a93d3ae5a" +dependencies = [ + "serde", + "zeroize", +] + [[package]] name = "send_wrapper" version = "0.6.0" @@ -2392,6 +2337,17 @@ dependencies = [ "serde_derive", ] +[[package]] +name = "serde-aux" +version = "4.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d2e8bfba469d06512e11e3311d4d051a4a387a5b42d010404fecf3200321c95" +dependencies = [ + "chrono", + "serde", + "serde_json", +] + [[package]] name = "serde_derive" version = "1.0.217" @@ -2463,13 +2419,20 @@ version = "0.1.0" dependencies = [ "app", "axum", + "config 0.15.6", "leptos", "leptos_axum", + "secrecy", + "serde", + "serde-aux", "sqlx", + "thiserror 2.0.11", "tokio", "tower", "tower-http", "tracing", + "tracing-log 0.2.0", + "uuid", ] [[package]] @@ -2680,6 +2643,7 @@ dependencies = [ "tokio-stream", "tracing", "url", + "uuid", ] [[package]] @@ -2761,6 +2725,7 @@ dependencies = [ "stringprep", "thiserror 2.0.11", "tracing", + "uuid", "whoami", ] @@ -2799,6 +2764,7 @@ dependencies = [ "stringprep", "thiserror 2.0.11", "tracing", + "uuid", "whoami", ] @@ -2824,6 +2790,7 @@ dependencies = [ "sqlx-core", "tracing", "url", + "uuid", ] [[package]] @@ -2902,7 +2869,7 @@ dependencies = [ "futures", "html-escape", "indexmap", - "itertools 0.13.0", + "itertools", "js-sys", "linear-map", "next_tuple", @@ -2913,7 +2880,7 @@ dependencies = [ "paste", "reactive_graph", "reactive_stores", - "rustc-hash 2.1.0", + "rustc-hash", "send_wrapper", "slotmap", "throw_error", @@ -3397,6 +3364,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b3758f5e68192bb96cc8f9b7e2c2cfdabb435499a28499a42f8f984092adad4b" dependencies = [ "getrandom", + "serde", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index f0955d2..3d03151 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -34,14 +34,18 @@ wasm-bindgen = "=0.2.100" sqlx = { version = "0.8", features = [ "runtime-tokio", "macros", - "sqlite-unbundled", + "postgres", + "uuid", "chrono", "migrate", ] } +uuid = { version = "1.12", features = ["v4", "serde"] } serde = { version = "1", features = ["derive"] } chrono = { version = "0.4", features = ["serde", "clock"] } secrecy = { version = "0.10", features = ["serde"] } validator = "0.20" +config = { version = "0.15", features = ["toml"], default-features = false } +serde-aux = "4" # See https://github.com/leptos-rs/cargo-leptos for documentation of all the parameters. diff --git a/app/Cargo.toml b/app/Cargo.toml index 8a8a070..4e06a8e 100644 --- a/app/Cargo.toml +++ b/app/Cargo.toml @@ -20,8 +20,9 @@ tracing-subscriber.workspace = true tracing-bunyan-formatter.workspace = true tracing-log.workspace = true sqlx.workspace = true -serde.workspace = true +uuid.workspace = true chrono.workspace = true +serde.workspace = true [features] default = [] diff --git a/app/src/models/user.rs b/app/src/models/user.rs index 8e85c2f..9bde063 100644 --- a/app/src/models/user.rs +++ b/app/src/models/user.rs @@ -1,9 +1,10 @@ use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; +use uuid::Uuid; #[derive(Debug, Serialize, Deserialize)] pub struct User { - pub id: i64, + pub id: Uuid, pub username: String, pub code: String, pub created_at: DateTime, diff --git a/config/base.toml b/config/base.toml new file mode 100644 index 0000000..b7d2e68 --- /dev/null +++ b/config/base.toml @@ -0,0 +1,9 @@ +[application] +port = 8000 + +[database] +host = "127.0.0.1" +port = 5432 +username = "postgres" +password = "password" +database_name = "maze_ascension" diff --git a/config/local.toml b/config/local.toml new file mode 100644 index 0000000..1be5491 --- /dev/null +++ b/config/local.toml @@ -0,0 +1,5 @@ +[application] +host = "127.0.0.1" + +[database] +require_ssl = false diff --git a/config/production.toml b/config/production.toml new file mode 100644 index 0000000..9775701 --- /dev/null +++ b/config/production.toml @@ -0,0 +1,5 @@ +[application] +host = "0.0.0.0" + +[database] +require_ssl = true diff --git a/justfile b/justfile index f5b6dad..346798b 100644 --- a/justfile +++ b/justfile @@ -19,7 +19,7 @@ setup: # Development Commands # Start development server with hot reload -dev: kill-server db-setup db-migrate +dev: kill-server db-migrate cargo leptos watch | bunyan # Run cargo check on both native and wasm targets @@ -86,7 +86,7 @@ kill-server: # Setup the database db-setup: - sqlite3 ${DATABASE_URL#sqlite:} ".databases" + ./scripts/init_db alias migrate:=db-migrate alias m:=db-migrate @@ -98,12 +98,6 @@ db-migrate: db-prepare: sqlx prepare -# Reset database -db-reset: - rm -f ${DATABASE_URL#sqlite:} - just db-setup - just db-migrate - alias migrations:=db-new-migration # Create new migration db-new-migration name: diff --git a/migrations/20250125123853_init.up.sql b/migrations/20250125123853_init.up.sql index 5cd9541..0f2932d 100644 --- a/migrations/20250125123853_init.up.sql +++ b/migrations/20250125123853_init.up.sql @@ -1,20 +1,23 @@ -- Add up migration script here +-- Enable UUID support +CREATE EXTENSION IF NOT EXISTS "pgcrypto"; + -- Users table with login codes CREATE TABLE IF NOT EXISTS users ( - id integer PRIMARY KEY AUTOINCREMENT, - username text NOT NULL UNIQUE, - code text NOT NULL UNIQUE, - created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP + id uuid PRIMARY KEY DEFAULT gen_random_uuid (), + username varchar(255) NOT NULL UNIQUE, + code varchar(255) NOT NULL UNIQUE, + created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ); -- Scores table with detailed game stats CREATE TABLE IF NOT EXISTS scores ( - id integer PRIMARY KEY AUTOINCREMENT, - user_id integer NOT NULL, + id bigserial PRIMARY KEY, + user_id uuid NOT NULL, score integer NOT NULL, floor_reached integer NOT NULL, play_time_seconds integer NOT NULL, - created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (user_id) REFERENCES users (id) ); diff --git a/scripts/init_db b/scripts/init_db new file mode 100755 index 0000000..c9c4cbc --- /dev/null +++ b/scripts/init_db @@ -0,0 +1,46 @@ +#!/usr/bin/env bash +set -eo pipefail + +if ! [ -x "$(command -v psql)" ]; then + echo >&2 "Error: psql is not installed." + exit 1 +fi + +if ! [ -x "$(command -v sqlx)" ]; then + echo >&2 "Error: sqlx is not installed." + echo >&2 "Use:" + echo >&2 " cargo install sqlx-cli --no-default-features --features rustls,postgres" + echo >&2 "to install it." + exit 1 +fi + +DB_USER="${POSTGRES_USER:=postgres}" +DB_PASSWORD="${POSTGRES_PASSWORD:=password}" +DB_NAME="${POSTGRES_DB:=maze_ascension}" +DB_PORT="${POSTGRES_PORT:=5432}" +DB_HOST="${POSTGRES_HOST:=localhost}" + +if [[ -z "${SKIP_DOCKER}" ]]; then + docker run\ + -e POSTGRES_USER=${DB_USER}\ + -e POSTGRES_PASSWORD=${DB_PASSWORD}\ + -e POSTGRES_DB=${DB_NAME}\ + -p "${DB_PORT}":5432\ + -d postgres\ + postgres -N 1000 + # Increase max number of connections for testing purposes +fi + +# Keep pinging Postgres until it's ready to accept commands +export PGPASSWORD="${DB_PASSWORD}" +until psql -h "${DB_HOST}" -U "${DB_USER}" -p "${DB_PORT}" -d "postgres" -c '\q'; do + >&2 echo "Postgres is still unavailable - sleeping" + sleep 1 +done + +>&2 echo "Postgres is still up and running on port ${DB_PORT} - runing migrations now!" + +DATABASE_URL="postgres://${DB_USER}:${DB_PASSWORD}@${DB_HOST}:${DB_PORT}/${DB_NAME}" +export DATABASE_URL +sqlx database create +sqlx migrate run diff --git a/server/Cargo.toml b/server/Cargo.toml index 307eb49..8b9394f 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -13,6 +13,13 @@ leptos_axum.workspace = true axum.workspace = true tokio.workspace = true tower.workspace = true +thiserror.workspace = true tower-http.workspace = true tracing.workspace = true +tracing-log.workspace = true sqlx.workspace = true +serde.workspace = true +serde-aux.workspace = true +config.workspace = true +secrecy.workspace = true +uuid.workspace = true diff --git a/server/src/config.rs b/server/src/config.rs new file mode 100644 index 0000000..8cb4c16 --- /dev/null +++ b/server/src/config.rs @@ -0,0 +1,114 @@ +use secrecy::{ExposeSecret, SecretString}; +use serde::Deserialize; +use serde_aux::field_attributes::deserialize_number_from_string; +use sqlx::{ + postgres::{PgConnectOptions, PgSslMode}, + ConnectOptions, +}; +use std::{fmt::Display, str::FromStr}; + +#[derive(Debug, Deserialize, Clone)] +pub struct Settings { + pub database: DatabaseSettings, + pub application: ApplicationSettings, +} + +#[derive(Debug, Deserialize, Clone)] +pub struct DatabaseSettings { + pub username: String, + pub password: SecretString, + #[serde(deserialize_with = "deserialize_number_from_string")] + pub port: u16, + pub host: String, + pub database_name: String, + pub require_ssl: bool, +} + +#[derive(Debug, Deserialize, Clone)] +pub struct ApplicationSettings { + #[serde(deserialize_with = "deserialize_number_from_string")] + pub port: u16, + pub host: String, +} + +#[derive(Debug, Clone)] +pub enum Environment { + Local, + Production, +} + +pub fn get_config() -> Result { + let base_path = std::env::current_dir().expect("Failed to determine current directory"); + let config_directory = base_path.join("config"); + let env: Environment = std::env::var("APP_ENVIRONMENT") + .unwrap_or_else(|_| "local".into()) + .try_into() + .expect("Failed to parse APP_ENVIRONMENT"); + + let env_filename = format!("{}.toml", &env); + + let settings = config::Config::builder() + .add_source(config::File::from(config_directory.join("base.toml"))) + .add_source(config::File::from(config_directory.join(env_filename))) + .add_source( + config::Environment::with_prefix("APP") + .prefix_separator("_") + .separator("__"), + ) + .build()?; + settings.try_deserialize::() +} + +impl DatabaseSettings { + pub fn without_db(&self) -> PgConnectOptions { + let ssl_mode = if self.require_ssl { + PgSslMode::Require + } else { + PgSslMode::Prefer + }; + + PgConnectOptions::new() + .host(&self.host) + .username(&self.username) + .password(self.password.expose_secret()) + .port(self.port) + .ssl_mode(ssl_mode) + } + + pub fn with_db(&self) -> PgConnectOptions { + self.without_db() + .database(&self.database_name) + .log_statements(tracing_log::log::LevelFilter::Trace) + } +} + +impl Display for Environment { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Environment::Local => write!(f, "local"), + Environment::Production => write!(f, "production"), + } + } +} + +impl TryFrom for Environment { + type Error = String; + fn try_from(value: String) -> Result { + match value.to_lowercase().as_str() { + "local" => Ok(Self::Local), + "production" => Ok(Self::Production), + other => Err(format!( + "{} is not supported environment. \ + Use either `local` or `production`.", + other + )), + } + } +} + +impl FromStr for Environment { + type Err = String; + fn from_str(s: &str) -> Result { + s.to_owned().try_into() + } +} diff --git a/server/src/db/mod.rs b/server/src/db/mod.rs index 0ab41e5..36dafd6 100644 --- a/server/src/db/mod.rs +++ b/server/src/db/mod.rs @@ -1 +1,2 @@ -mod scores; +mod user; + diff --git a/server/src/db/scores.rs b/server/src/db/scores.rs deleted file mode 100644 index e69de29..0000000 diff --git a/server/src/db/user.rs b/server/src/db/user.rs new file mode 100644 index 0000000..73b1b8f --- /dev/null +++ b/server/src/db/user.rs @@ -0,0 +1,13 @@ +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum UserError { + #[error("Database error: {0}")] + Database(#[from] sqlx::Error), + #[error("Username already taken")] + UsernameTaken, + #[error("User not found")] + NotFound, +} + +pub async fn create_user() {} diff --git a/server/src/lib.rs b/server/src/lib.rs index dec1023..36f192d 100644 --- a/server/src/lib.rs +++ b/server/src/lib.rs @@ -1 +1,4 @@ +pub mod config; pub mod db; +pub mod routes; +pub mod startup; diff --git a/server/src/main.rs b/server/src/main.rs index 7fcaa77..2f14d9c 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -1,33 +1,17 @@ use app::telemetry::{get_subscriber, init_subscriber}; -use app::*; -use axum::Router; use leptos::prelude::*; -use leptos_axum::{generate_route_list, LeptosRoutes}; -use tracing::info; +use server::config::get_config; +use server::startup::{Application, ApplicationError}; #[tokio::main] -async fn main() { - let conf = get_configuration(None).unwrap(); - let addr = conf.leptos_options.site_addr; - let leptos_options = conf.leptos_options; +async fn main() -> Result<(), ApplicationError> { // Generate the list of routes in your Leptos App let subscriber = get_subscriber("echoes-of-ascension-backend", "info", std::io::stdout); init_subscriber(subscriber); - let routes = generate_route_list(App); + let config = get_config().expect("Failed to read configuation."); - let app = Router::new() - .leptos_routes(&leptos_options, routes, { - let leptos_options = leptos_options.clone(); - move || shell(leptos_options.clone()) - }) - .fallback(leptos_axum::file_and_error_handler(shell)) - .with_state(leptos_options); - - // run app with hyper - info!("listening on http://{}", &addr); - let listener = tokio::net::TcpListener::bind(&addr).await.unwrap(); - axum::serve(listener, app.into_make_service()) - .await - .unwrap(); + let application = Application::build(config).await?; + application.run_until_stopped().await?; + Ok(()) } diff --git a/server/src/routes/health_check.rs b/server/src/routes/health_check.rs new file mode 100644 index 0000000..be39b52 --- /dev/null +++ b/server/src/routes/health_check.rs @@ -0,0 +1,5 @@ +use axum::{http::StatusCode, response::IntoResponse}; + +pub async fn health_check() -> impl IntoResponse { + StatusCode::OK +} diff --git a/server/src/routes/mod.rs b/server/src/routes/mod.rs new file mode 100644 index 0000000..e52205b --- /dev/null +++ b/server/src/routes/mod.rs @@ -0,0 +1,73 @@ +mod health_check; +use std::time::Duration; + +use app::{shell, App}; +use axum::{ + body::Bytes, + extract::MatchedPath, + http::{HeaderMap, Request}, + response::Response, + routing::get, + Router, +}; +use health_check::health_check; + +use leptos_axum::{generate_route_list, LeptosRoutes}; +use tower_http::{classify::ServerErrorsFailureClass, trace::TraceLayer}; +use tracing::{info_span, Span}; +use uuid::Uuid; + +use crate::startup::AppState; + +pub fn route(state: AppState) -> Router { + Router::new() + .merge(leptos_routes(state.clone())) + .merge(api_routes(state)) + .layer( + TraceLayer::new_for_http() + .make_span_with(|request: &Request<_>| { + let matched_path = request + .extensions() + .get::() + .map(MatchedPath::as_str); + info_span!( + "http-request", + method = ?request.method(), + matched_path, + some_other_field = tracing::field::Empty, + request_id=%Uuid::new_v4(), + ) + }) + .on_request(|_request: &Request<_>, _span: &Span| {}) + .on_response(|_response: &Response<_>, _latency: Duration, _span: &Span| {}) + .on_body_chunk(|_chunk: &Bytes, _latency: Duration, _span: &Span| {}) + .on_eos( + |_trailers: Option<&HeaderMap>, _stream_duration: Duration, _span: &Span| {}, + ) + .on_failure( + |_error: ServerErrorsFailureClass, _latency: Duration, _span: &Span| {}, + ), + ) +} + +fn leptos_routes(state: AppState) -> Router { + let leptos_options = state.leptos_options.clone(); + let routes = generate_route_list(App); + + Router::new() + .leptos_routes(&leptos_options, routes, { + let leptos_options = leptos_options.clone(); + move || shell(leptos_options.clone()) + }) + .fallback(leptos_axum::file_and_error_handler(shell)) + .with_state(leptos_options) +} + +fn api_routes(state: AppState) -> Router { + Router::new() + .nest( + "/api/v1", + Router::new().route("/health_check", get(health_check)), + ) + .with_state(state) +} diff --git a/server/src/startup.rs b/server/src/startup.rs new file mode 100644 index 0000000..908c6f1 --- /dev/null +++ b/server/src/startup.rs @@ -0,0 +1,75 @@ +use std::sync::Arc; + +use leptos::config::{errors::LeptosConfigError, get_configuration, LeptosOptions}; +use sqlx::{postgres::PgPoolOptions, PgPool}; +use thiserror::Error; +use tokio::{net::TcpListener, task::JoinHandle}; + +use crate::{ + config::{DatabaseSettings, Settings}, + routes::route, +}; + +pub type AppState = Arc; + +#[derive(Debug, Error)] +pub enum ApplicationError { + #[error("IO error: {0}")] + Io(#[from] std::io::Error), + + #[error("Leptos configuration error: {0}")] + LeptosConfig(#[from] LeptosConfigError), + + #[error("Database error: {0}")] + Database(#[from] sqlx::Error), + + #[error("Server error: {0}")] + Server(String), +} + +#[derive(Debug)] +pub struct App { + pub pool: PgPool, + pub leptos_options: LeptosOptions, +} + +#[derive(Debug)] +pub struct Application { + port: u16, + server: JoinHandle>, +} + +impl Application { + pub async fn build(config: Settings) -> Result { + let pool = get_connection_pool(&config.database); + + // Get Leptos configuration but override the address + let conf = get_configuration(None)?; + + // Use application's address configuration + let addr = conf.leptos_options.site_addr; + let listener = TcpListener::bind(addr).await?; + let port = listener.local_addr()?.port(); + + let app_state = App { + pool, + leptos_options: conf.leptos_options, + } + .into(); + let server = tokio::spawn(async move { axum::serve(listener, route(app_state)).await }); + + Ok(Self { port, server }) + } + + pub fn port(&self) -> u16 { + self.port + } + + pub async fn run_until_stopped(self) -> Result<(), std::io::Error> { + self.server.await? + } +} + +pub fn get_connection_pool(config: &DatabaseSettings) -> PgPool { + PgPoolOptions::new().connect_lazy_with(config.with_db()) +}