Initial commit: Axum web service template
This commit is contained in:
48
src/config.rs
Normal file
48
src/config.rs
Normal file
@@ -0,0 +1,48 @@
|
||||
use std::net::{IpAddr, Ipv4Addr};
|
||||
|
||||
use anyhow::Context;
|
||||
use axum_app_wrapper::AdHocPlugin;
|
||||
{% if include_aide %}use schemars::JsonSchema;
|
||||
{% endif %}
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::state::AppState;
|
||||
|
||||
#[derive(Debug, Clone, Deserialize{% if include_aide %}, JsonSchema{% endif %})]
|
||||
pub struct AppConfig {
|
||||
pub api_key: String,
|
||||
#[serde(default = "default_host")]
|
||||
pub host: IpAddr,
|
||||
#[serde(default = "default_port")]
|
||||
pub port: u16,
|
||||
#[serde(default = "default_log_level")]
|
||||
pub log_level: String,
|
||||
}
|
||||
fn default_host() -> IpAddr {
|
||||
IpAddr::V4(Ipv4Addr::LOCALHOST)
|
||||
}
|
||||
fn default_port() -> u16 {
|
||||
{{default_port}}
|
||||
}
|
||||
fn default_log_level() -> String {
|
||||
"{{default_log_level}}".to_string()
|
||||
}
|
||||
|
||||
/// Plugin that reads and validates configuration, and adds it to server state
|
||||
pub fn plugin() -> AdHocPlugin<AppState> {
|
||||
AdHocPlugin::new().on_init(|mut state| async move {
|
||||
let config = extract_config()?;
|
||||
state.insert(config);
|
||||
Ok(state)
|
||||
})
|
||||
}
|
||||
|
||||
/// Extract the configuration from env variables prefixed with `{{env_prefix}}_`.
|
||||
fn extract_config() -> anyhow::Result<AppConfig> {
|
||||
let config = figment::Figment::new()
|
||||
.merge(figment::providers::Env::prefixed("{{env_prefix}}_"))
|
||||
.extract::<AppConfig>()
|
||||
.context("Failed to extract valid configuration")?;
|
||||
|
||||
Ok(config)
|
||||
}
|
||||
13
src/lib.rs
Normal file
13
src/lib.rs
Normal file
@@ -0,0 +1,13 @@
|
||||
use axum_app_wrapper::App;
|
||||
|
||||
use crate::config::AppConfig;
|
||||
|
||||
mod config;
|
||||
mod state;
|
||||
|
||||
pub async fn create_app() -> anyhow::Result<(axum::Router, AppConfig, impl Future + Send)> {
|
||||
let (router, state, on_shutdown) = App::new().register(config::plugin()).init().await?;
|
||||
let app_config = state.config.to_owned();
|
||||
|
||||
Ok((router.with_state(state), app_config, on_shutdown))
|
||||
}
|
||||
70
src/main.rs
Normal file
70
src/main.rs
Normal file
@@ -0,0 +1,70 @@
|
||||
use std::{net::SocketAddr, str::FromStr};
|
||||
|
||||
use {{crate_name}}::create_app;
|
||||
use tracing::level_filters::LevelFilter;
|
||||
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> anyhow::Result<()> {
|
||||
// Read .env file in debug mode
|
||||
#[cfg(debug_assertions)]
|
||||
dotenvy::dotenv().ok();
|
||||
|
||||
// Build server
|
||||
let (router, config, on_shutdown) = create_app().await?;
|
||||
|
||||
// Configure logging output (use JSON logs in release mode)
|
||||
let level_filter = LevelFilter::from_str(&config.log_level)?;
|
||||
let env_filter = tracing_subscriber::EnvFilter::builder()
|
||||
.with_default_directive(level_filter.into())
|
||||
.from_env_lossy();
|
||||
if cfg!(debug_assertions) {
|
||||
tracing_subscriber::fmt()
|
||||
.with_env_filter(env_filter)
|
||||
.with_max_level(level_filter)
|
||||
.init();
|
||||
} else {
|
||||
tracing_subscriber::registry()
|
||||
.with(env_filter)
|
||||
.with(level_filter)
|
||||
.with(tracing_subscriber::fmt::layer().json().flatten_event(true))
|
||||
.init();
|
||||
}
|
||||
|
||||
// Start listening for requests
|
||||
let addr = SocketAddr::new(config.host, config.port);
|
||||
let listener = tokio::net::TcpListener::bind(addr).await?;
|
||||
tracing::info!("Server listening on http://{}...", listener.local_addr()?);
|
||||
axum::serve(listener, router.into_make_service())
|
||||
.with_graceful_shutdown(shutdown_signal(on_shutdown))
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Shutdown signal: listens for Ctrl-C, SIGINT, SIGTERM signals
|
||||
async fn shutdown_signal(on_shutdown: impl Future + Send) {
|
||||
let ctrl_c = async {
|
||||
tokio::signal::ctrl_c()
|
||||
.await
|
||||
.expect("failed to register Ctrl-C handler");
|
||||
};
|
||||
|
||||
#[cfg(unix)]
|
||||
let terminate = async {
|
||||
tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate())
|
||||
.expect("failed to register SIGTERM handler")
|
||||
.recv()
|
||||
.await;
|
||||
};
|
||||
|
||||
#[cfg(not(unix))]
|
||||
let terminate = std::future::pending::<()>();
|
||||
|
||||
tokio::select! {
|
||||
_ = ctrl_c => {},
|
||||
_ = terminate => {},
|
||||
}
|
||||
tracing::info!("Received shutdown signal, shutting down server...");
|
||||
on_shutdown.await;
|
||||
}
|
||||
39
src/state.rs
Normal file
39
src/state.rs
Normal file
@@ -0,0 +1,39 @@
|
||||
//! Application state
|
||||
|
||||
use std::{any::type_name, ops::Deref, sync::Arc};
|
||||
|
||||
use type_map::concurrent::TypeMap;
|
||||
|
||||
use crate::config::AppConfig;
|
||||
|
||||
/// App state stored in the Axum router
|
||||
#[derive(Clone)]
|
||||
pub struct AppState(Arc<AppStateInner>);
|
||||
|
||||
pub struct AppStateInner {
|
||||
pub config: AppConfig,
|
||||
}
|
||||
|
||||
impl Deref for AppState {
|
||||
type Target = AppStateInner;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<TypeMap> for AppState {
|
||||
type Error = anyhow::Error;
|
||||
|
||||
fn try_from(mut map: TypeMap) -> Result<Self, Self::Error> {
|
||||
Ok(Self(Arc::new(AppStateInner {
|
||||
config: extract(&mut map)?,
|
||||
})))
|
||||
}
|
||||
}
|
||||
|
||||
fn extract<T: 'static>(type_map: &mut TypeMap) -> anyhow::Result<T> {
|
||||
type_map
|
||||
.remove()
|
||||
.ok_or_else(|| anyhow::anyhow!("Type not found in state: {}", type_name::<T>()))
|
||||
}
|
||||
Reference in New Issue
Block a user