use crate::{
LazyLock, Map,
datetime::DateTime,
extension::{JsonObjectExt, TomlTableExt},
schedule::{AsyncJobScheduler, AsyncScheduler, Scheduler},
state::{Env, State},
};
use ahash::{HashMap, HashMapExt};
use std::{
borrow::Cow,
env, fs,
path::{Component, Path, PathBuf},
thread,
};
use toml::value::Table;
mod agent;
mod app_code;
mod app_type;
mod plugin;
mod secret_key;
mod server_tag;
mod static_record;
#[cfg(feature = "http-client")]
pub(crate) mod http_client;
#[cfg(feature = "metrics")]
mod metrics_exporter;
#[cfg(feature = "preferences")]
mod preferences;
#[cfg(feature = "sentry")]
mod sentry_client;
#[cfg(feature = "tracing-subscriber")]
mod tracing_subscriber;
pub(crate) use secret_key::SECRET_KEY;
#[cfg(feature = "preferences")]
pub use preferences::Preferences;
#[cfg(feature = "http-client")]
use crate::{error::Error, extension::HeaderMapExt, trace::TraceContext};
pub use agent::Agent;
pub use app_code::ApplicationCode;
pub use app_type::AppType;
pub use plugin::Plugin;
pub use server_tag::ServerTag;
pub use static_record::StaticRecord;
pub trait Application {
type Routes;
const APP_TYPE: AppType;
fn register(self, routes: Self::Routes) -> Self;
fn run_with<T: AsyncScheduler + Send + 'static>(self, scheduler: T);
fn boot() -> Self
where
Self: Default,
{
#[cfg(feature = "dotenv")]
dotenvy::dotenv().ok();
#[cfg(feature = "tracing-subscriber")]
tracing_subscriber::init::<Self>();
secret_key::init::<Self>();
#[cfg(feature = "metrics")]
metrics_exporter::init::<Self>();
#[cfg(feature = "http-client")]
http_client::init::<Self>();
for path in SHARED_DIRS.values() {
if !path.exists()
&& 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 add_plugin(self, plugin: Plugin) -> Self
where
Self: Sized,
{
tracing::info!(plugin_name = plugin.name());
self
}
#[inline]
fn shared_state() -> &'static State<Map> {
&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_NAME.as_ref()
}
#[inline]
fn version() -> &'static str {
APP_VERSION.as_ref()
}
#[inline]
fn domain() -> &'static str {
APP_DOMAIN.as_ref()
}
#[inline]
fn secret_key() -> &'static [u8] {
SECRET_KEY.get().expect("fail to get the secret key")
}
#[inline]
fn project_dir() -> &'static PathBuf {
&PROJECT_DIR
}
#[inline]
fn config_dir() -> &'static PathBuf {
&CONFIG_DIR
}
#[inline]
fn shared_dir(name: &str) -> Cow<'_, PathBuf> {
SHARED_DIRS
.get(name)
.map(Cow::Borrowed)
.unwrap_or_else(|| Cow::Owned(Self::parse_path(name)))
}
#[inline]
fn parse_path(path: &str) -> PathBuf {
join_path(&PROJECT_DIR, path)
}
fn spawn<T>(self, mut scheduler: T) -> Self
where
Self: Sized,
T: Scheduler + Send + 'static,
{
thread::spawn(move || {
loop {
scheduler.tick();
if let Some(duration) = scheduler.time_till_next_job() {
thread::sleep(duration);
}
}
});
self
}
#[inline]
fn run(self)
where
Self: Sized,
{
self.run_with(AsyncJobScheduler::default());
}
#[inline]
async fn load() {}
#[inline]
async fn shutdown() {}
#[cfg(feature = "http-client")]
async fn fetch(url: &str, options: Option<&Map>) -> Result<reqwest::Response, Error> {
let mut trace_context = TraceContext::new();
trace_context.record_trace_state();
http_client::request_builder(url, options)?
.header("traceparent", trace_context.traceparent())
.header("tracestate", trace_context.tracestate())
.send()
.await
.map_err(Error::from)
}
#[cfg(feature = "http-client")]
async fn fetch_json<T: serde::de::DeserializeOwned>(
url: &str,
options: Option<&Map>,
) -> Result<T, Error> {
let response = Self::fetch(url, 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)
}
}
fn join_path(dir: &Path, path: &str) -> PathBuf {
fn join_path_components(mut full_path: PathBuf, path: &str) -> PathBuf {
for component in Path::new(path).components() {
match component {
Component::CurDir => (),
Component::ParentDir => {
full_path.pop();
}
_ => {
full_path.push(component);
}
}
}
full_path
}
if path.starts_with('/') {
path.into()
} else if let Some(path) = path.strip_prefix("~/") {
if let Some(home_dir) = env::home_dir() {
join_path_components(home_dir, path)
} else {
join_path_components(dir.to_path_buf(), path)
}
} else {
join_path_components(dir.to_path_buf(), path)
}
}
static APP_NAME: 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()
})
});
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()
})
});
static APP_DOMAIN: LazyLock<&'static str> = LazyLock::new(|| {
SHARED_APP_STATE
.config()
.get_str("domain")
.unwrap_or("localhost")
});
static PROJECT_DIR: LazyLock<PathBuf> = LazyLock::new(|| {
env::var("CARGO_MANIFEST_DIR")
.ok()
.filter(|var| !var.is_empty())
.map(PathBuf::from)
.unwrap_or_else(|| {
if cfg!(not(debug_assertions))
&& cfg!(target_os = "macos")
&& let Ok(mut path) = env::current_exe()
{
path.pop();
if path.ends_with("Contents/MacOS") {
path.pop();
path.push("Resources");
if path.exists() && path.is_dir() {
return path;
}
}
}
tracing::warn!(
"fail to get the environment variable `CARGO_MANIFEST_DIR`; \
current directory will be used as the project directory"
);
env::current_dir()
.expect("project directory does not exist or permissions are insufficient")
})
});
static CONFIG_DIR: LazyLock<PathBuf> = LazyLock::new(|| {
env::var("ZINO_APP_CONFIG_DIR")
.ok()
.filter(|var| !var.is_empty())
.map(PathBuf::from)
.unwrap_or_else(|| PROJECT_DIR.join("config"))
});
static SHARED_DIRS: LazyLock<HashMap<String, PathBuf>> = LazyLock::new(|| {
let mut dirs = HashMap::new();
if let Some(config) = SHARED_APP_STATE.get_config("dirs") {
for (key, value) in config {
if let Some(path) = value.as_str() {
dirs.insert(key.to_owned(), join_path(&PROJECT_DIR, path));
}
}
}
dirs
});
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 application name from config or environment variable")
});
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 application version from config or environment variable")
});
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
});