chore(server): scaffold axum com health endpoint e testes
This commit is contained in:
Generated
+1796
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,21 @@
|
|||||||
|
[package]
|
||||||
|
name = "ares-server"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
publish = false
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
axum = "0.8"
|
||||||
|
tokio = { version = "1", features = ["full", "macros", "rt-multi-thread"] }
|
||||||
|
serde = { version = "1", features = ["derive"] }
|
||||||
|
serde_json = "1"
|
||||||
|
tracing = "0.1"
|
||||||
|
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||||
|
tower-http = { version = "0.6", features = ["cors", "trace"] }
|
||||||
|
rusqlite = { version = "0.32", features = ["bundled"] }
|
||||||
|
thiserror = "2"
|
||||||
|
chrono = { version = "0.4", default-features = false, features = ["clock"] }
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
axum-test = "21"
|
||||||
|
tempfile = "3"
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
use axum::Json;
|
||||||
|
use serde_json::{json, Value};
|
||||||
|
|
||||||
|
pub async fn health() -> Json<Value> {
|
||||||
|
Json(json!({
|
||||||
|
"status": "ok",
|
||||||
|
"version": env!("CARGO_PKG_VERSION"),
|
||||||
|
"service": "ares-next",
|
||||||
|
}))
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
pub mod health;
|
||||||
|
|
||||||
|
use axum::{routing::get, Router};
|
||||||
|
use tower_http::cors::CorsLayer;
|
||||||
|
use crate::state::SharedState;
|
||||||
|
|
||||||
|
pub fn router(state: SharedState) -> Router {
|
||||||
|
let api = Router::new()
|
||||||
|
.route("/health", get(health::health))
|
||||||
|
.with_state(state);
|
||||||
|
|
||||||
|
Router::new()
|
||||||
|
.nest("/api/v1", api)
|
||||||
|
.layer(CorsLayer::permissive())
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
use std::env;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct Config {
|
||||||
|
pub host: String,
|
||||||
|
pub port: u16,
|
||||||
|
pub db_path: String,
|
||||||
|
pub max_body_mb: usize,
|
||||||
|
pub max_results: i64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Config {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
host: "127.0.0.1".into(),
|
||||||
|
port: 3000,
|
||||||
|
db_path: "data/aresnext.db".into(),
|
||||||
|
max_body_mb: 2,
|
||||||
|
max_results: 50,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Config {
|
||||||
|
pub fn from_env() -> Self {
|
||||||
|
let def = Self::default();
|
||||||
|
Self {
|
||||||
|
host: env::var("ARS_NEXT_HOST").unwrap_or(def.host),
|
||||||
|
port: env::var("ARS_NEXT_PORT")
|
||||||
|
.ok()
|
||||||
|
.and_then(|s| s.parse().ok())
|
||||||
|
.unwrap_or(def.port),
|
||||||
|
db_path: env::var("ARS_NEXT_DB").unwrap_or(def.db_path),
|
||||||
|
max_body_mb: env::var("ARS_NEXT_MAX_BODY_MB")
|
||||||
|
.ok()
|
||||||
|
.and_then(|s| s.parse().ok())
|
||||||
|
.unwrap_or(def.max_body_mb),
|
||||||
|
max_results: env::var("ARS_NEXT_MAX_RESULTS")
|
||||||
|
.ok()
|
||||||
|
.and_then(|s| s.parse().ok())
|
||||||
|
.unwrap_or(def.max_results),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn max_body_bytes(&self) -> usize {
|
||||||
|
self.max_body_mb * 1024 * 1024
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
use axum::{http::StatusCode, response::{IntoResponse, Response}, Json};
|
||||||
|
use serde_json::json;
|
||||||
|
|
||||||
|
#[derive(Debug, thiserror::Error)]
|
||||||
|
pub enum AppError {
|
||||||
|
#[error("bad request: {0}")]
|
||||||
|
BadRequest(String),
|
||||||
|
#[error("not found: {0}")]
|
||||||
|
NotFound(String),
|
||||||
|
#[error("database error: {0}")]
|
||||||
|
Db(#[from] rusqlite::Error),
|
||||||
|
#[error("internal: {0}")]
|
||||||
|
Internal(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AppError {
|
||||||
|
fn status(&self) -> StatusCode {
|
||||||
|
match self {
|
||||||
|
Self::BadRequest(_) => StatusCode::BAD_REQUEST,
|
||||||
|
Self::NotFound(_) => StatusCode::NOT_FOUND,
|
||||||
|
Self::Db(_) | Self::Internal(_) => StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IntoResponse for AppError {
|
||||||
|
fn into_response(self) -> Response {
|
||||||
|
let body = json!({ "error": self.to_string() });
|
||||||
|
(self.status(), Json(body)).into_response()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type AppResult<T> = Result<T, AppError>;
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
pub mod api;
|
||||||
|
pub mod config;
|
||||||
|
pub mod error;
|
||||||
|
pub mod state;
|
||||||
|
pub mod store;
|
||||||
|
|
||||||
|
use axum::Router;
|
||||||
|
use crate::config::Config;
|
||||||
|
use crate::state::{AppState, SharedState};
|
||||||
|
|
||||||
|
pub async fn build_app(cfg: Config) -> SharedState {
|
||||||
|
let conn = store::open(&cfg).expect("open database");
|
||||||
|
AppState::new(cfg, conn)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn router(state: SharedState) -> Router {
|
||||||
|
api::router(state)
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
use ares_server::config::Config;
|
||||||
|
use ares_server::{build_app, router};
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() {
|
||||||
|
tracing_subscriber::fmt()
|
||||||
|
.with_env_filter(tracing_subscriber::EnvFilter::from_default_env())
|
||||||
|
.init();
|
||||||
|
|
||||||
|
let cfg = Config::from_env();
|
||||||
|
let addr = format!("{}:{}", cfg.host, cfg.port);
|
||||||
|
let state = build_app(cfg).await;
|
||||||
|
let app = router(state);
|
||||||
|
|
||||||
|
let listener = tokio::net::TcpListener::bind(&addr)
|
||||||
|
.await
|
||||||
|
.expect("bind listener");
|
||||||
|
tracing::info!("AresNext listening on http://{addr}");
|
||||||
|
axum::serve(listener, app).await.expect("server");
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
use std::sync::{Arc, Mutex};
|
||||||
|
use rusqlite::Connection;
|
||||||
|
use crate::config::Config;
|
||||||
|
|
||||||
|
pub type DbConn = Mutex<Connection>;
|
||||||
|
|
||||||
|
pub struct AppState {
|
||||||
|
pub config: Config,
|
||||||
|
pub db: DbConn,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AppState {
|
||||||
|
pub fn new(config: Config, db: Connection) -> Arc<Self> {
|
||||||
|
Arc::new(Self {
|
||||||
|
config,
|
||||||
|
db: Mutex::new(db),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type SharedState = Arc<AppState>;
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
use rusqlite::Connection;
|
||||||
|
use crate::config::Config;
|
||||||
|
use crate::error::AppResult;
|
||||||
|
|
||||||
|
pub fn open(cfg: &Config) -> AppResult<Connection> {
|
||||||
|
if let Some(parent) = std::path::Path::new(&cfg.db_path).parent() {
|
||||||
|
std::fs::create_dir_all(parent).ok();
|
||||||
|
}
|
||||||
|
let db = Connection::open(&cfg.db_path)?;
|
||||||
|
db.pragma_update(None, "journal_mode", "WAL")?;
|
||||||
|
db.pragma_update(None, "synchronous", "NORMAL")?;
|
||||||
|
Ok(db)
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
use axum_test::TestServer;
|
||||||
|
use ares_server::config::Config;
|
||||||
|
use ares_server::{build_app, router};
|
||||||
|
use tempfile::tempdir;
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn health_returns_ok() {
|
||||||
|
let dir = tempdir().unwrap();
|
||||||
|
let cfg = Config {
|
||||||
|
db_path: dir.path().join("t.db").to_string_lossy().into(),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
let state = build_app(cfg).await;
|
||||||
|
let server = TestServer::new(router(state));
|
||||||
|
let res = server.get("/api/v1/health").await;
|
||||||
|
assert_eq!(res.status_code(), 200);
|
||||||
|
assert_eq!(res.json::<serde_json::Value>()["status"], "ok");
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user