use crate::{
datetime::DateTime,
error::Error,
extension::{HeaderMapExt, JsonObjectExt, TomlTableExt},
openapi,
schedule::{AsyncJobScheduler, AsyncScheduler, Scheduler},
state::{Env, State},
trace::TraceContext,
LazyLock, Map,
};
use reqwest::Response;
use serde::de::DeserializeOwned;
use std::{env, fs, path::PathBuf, thread};
use toml::value::Table;
use utoipa::openapi::{OpenApi, OpenApiBuilder};
mod secret_key;
mod server_tag;
mod static_record;
mod system_monitor;
mod tracing_subscriber;
#[cfg(feature = "metrics")]
mod metrics_exporter;
pub(crate) mod http_client;
pub(crate) use secret_key::SECRET_KEY;
pub use server_tag::ServerTag;
pub use static_record::StaticRecord;
pub trait Application {
type Routes;
fn register(self, routes: Self::Routes) -> Self;
fn run_with<T: AsyncScheduler + Send + 'static>(self, scheduler: T);
fn boot() -> Self
where
Self: Default,
{
dotenvy::dotenv().ok();
tracing_subscriber::init::<Self>();
secret_key::init::<Self>();
#[cfg(feature = "metrics")]
self::metrics_exporter::init::<Self>();
http_client::init::<Self>();
#[cfg(feature = "view")]
crate::view::init::<Self>();
if let Some(dirs) = SHARED_APP_STATE.get_config("dirs") {
let project_dir = Self::project_dir();
for dir in dirs.values().filter_map(|v| v.as_str()) {
let path = if dir.starts_with('/') {
PathBuf::from(dir)
} else {
project_dir.join(dir)
};
if !path.exists() {
if let Err(err) = fs::create_dir_all(&path) {
let path = path.display();
tracing::error!("fail to create the directory {path}: {err}");
}
}
}
}
Self::default()
}
fn boot_with<F>(init: F) -> Self
where
Self: Default,
F: FnOnce(&'static State<Map>),
{
let app = Self::boot();
init(Self::shared_state());
app
}
#[inline]
fn register_with(self, server_tag: ServerTag, routes: Self::Routes) -> Self
where
Self: Sized,
{
if server_tag == ServerTag::Debug {
self.register(routes)
} else {
self
}
}
#[inline]
fn register_debug(self, routes: Self::Routes) -> Self
where
Self: Sized,
{
self.register_with(ServerTag::Debug, routes)
}
#[inline]
fn sysinfo() -> Map {
system_monitor::refresh_and_retrieve()
}
#[inline]
fn openapi() -> OpenApi {
OpenApiBuilder::new()
.paths(openapi::default_paths()) .components(Some(openapi::default_components()))
.tags(Some(openapi::default_tags()))
.servers(Some(openapi::default_servers()))
.security(Some(openapi::default_securities()))
.external_docs(openapi::default_external_docs())
.info(openapi::openapi_info(Self::name(), Self::version()))
.build()
}
#[inline]
fn shared_state() -> &'static State<Map> {
LazyLock::force(&SHARED_APP_STATE)
}
#[inline]
fn env() -> &'static Env {
SHARED_APP_STATE.env()
}
#[inline]
fn config() -> &'static Table {
SHARED_APP_STATE.config()
}
#[inline]
fn state_data() -> &'static Map {
SHARED_APP_STATE.data()
}
#[inline]
fn name() -> &'static str {
APP_NMAE.as_ref()
}
#[inline]
fn version() -> &'static str {
APP_VERSION.as_ref()
}
#[inline]
fn domain() -> &'static str {
APP_DOMAIN.as_ref()
}
#[inline]
fn project_dir() -> &'static PathBuf {
LazyLock::force(&PROJECT_DIR)
}
#[inline]
fn secret_key() -> &'static [u8] {
SECRET_KEY.get().expect("fail to get the secret key")
}
fn shared_dir(name: &str) -> PathBuf {
let path = if let Some(path) = SHARED_APP_STATE
.get_config("dirs")
.and_then(|t| t.get_str(name))
{
path
} else {
name
};
Self::project_dir().join(path)
}
fn spawn<T>(self, mut scheduler: T) -> Self
where
Self: Sized,
T: Scheduler + Send + 'static,
{
thread::spawn(move || loop {
scheduler.tick();
thread::sleep(scheduler.time_till_next_job());
});
self
}
#[inline]
fn run(self)
where
Self: Sized,
{
self.run_with(AsyncJobScheduler::default());
}
async fn load() {
}
async fn shutdown() {
}
async fn fetch(resource: &str, options: Option<&Map>) -> Result<Response, Error> {
let mut trace_context = TraceContext::new();
let span_id = trace_context.span_id();
trace_context
.trace_state_mut()
.push("edm", format!("{span_id:x}"));
http_client::request_builder(resource, options)?
.header("traceparent", trace_context.traceparent())
.header("tracestate", trace_context.tracestate())
.send()
.await
.map_err(Error::from)
}
async fn fetch_json<T: DeserializeOwned>(
resource: &str,
options: Option<&Map>,
) -> Result<T, Error> {
let response = Self::fetch(resource, options).await?.error_for_status()?;
let data = if response.headers().has_json_content_type() {
response.json().await?
} else {
let text = response.text().await?;
serde_json::from_str(&text)?
};
Ok(data)
}
}
pub(crate) static APP_NMAE: LazyLock<&'static str> = LazyLock::new(|| {
SHARED_APP_STATE
.config()
.get_str("name")
.unwrap_or_else(|| {
env::var("CARGO_PKG_NAME")
.expect("fail to get the environment variable `CARGO_PKG_NAME`")
.leak()
})
});
pub(crate) static APP_VERSION: LazyLock<&'static str> = LazyLock::new(|| {
SHARED_APP_STATE
.config()
.get_str("version")
.unwrap_or_else(|| {
env::var("CARGO_PKG_VERSION")
.expect("fail to get the environment variable `CARGO_PKG_VERSION`")
.leak()
})
});
pub(crate) static APP_DOMAIN: LazyLock<&'static str> = LazyLock::new(|| {
SHARED_APP_STATE
.config()
.get_str("domain")
.unwrap_or("localhost")
});
pub(crate) static PROJECT_DIR: LazyLock<PathBuf> = LazyLock::new(|| {
env::var("CARGO_MANIFEST_DIR")
.map(PathBuf::from)
.unwrap_or_else(|err| {
tracing::warn!(
"fail to get the environment variable `CARGO_MANIFEST_DIR`: {err}; \
the current directory will be used as the project directory"
);
env::current_dir()
.expect("the project directory does not exist or permissions are insufficient")
})
});
static SHARED_APP_STATE: LazyLock<State<Map>> = LazyLock::new(|| {
let mut state = State::default();
state.load_config();
let config = state.config();
let app_name = config
.get_str("name")
.map(|s| s.to_owned())
.unwrap_or_else(|| {
env::var("CARGO_PKG_NAME")
.expect("fail to get the environment variable `CARGO_PKG_NAME`")
});
let app_version = config
.get_str("version")
.map(|s| s.to_owned())
.unwrap_or_else(|| {
env::var("CARGO_PKG_VERSION")
.expect("fail to get the environment variable `CARGO_PKG_VERSION`")
});
let mut data = Map::new();
data.upsert("app.name", app_name);
data.upsert("app.version", app_version);
data.upsert("app.booted_at", DateTime::now());
state.set_data(data);
state
});