chore(server): scaffold axum com health endpoint e testes
This commit is contained in:
@@ -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)
|
||||
}
|
||||
Reference in New Issue
Block a user