zino_core/application/
mod.rs1use crate::{
38 LazyLock, Map,
39 datetime::DateTime,
40 extension::{JsonObjectExt, TomlTableExt},
41 schedule::{AsyncJobScheduler, AsyncScheduler, Scheduler},
42 state::{Env, State},
43};
44use ahash::{HashMap, HashMapExt};
45use std::{
46 borrow::Cow,
47 env, fs,
48 path::{Component, Path, PathBuf},
49 thread,
50};
51use toml::value::Table;
52
53mod agent;
54mod app_type;
55mod plugin;
56mod secret_key;
57mod server_tag;
58mod static_record;
59
60#[cfg(feature = "http-client")]
61pub(crate) mod http_client;
62
63#[cfg(feature = "metrics")]
64mod metrics_exporter;
65
66#[cfg(feature = "preferences")]
67mod preferences;
68
69#[cfg(feature = "sentry")]
70mod sentry_client;
71
72#[cfg(feature = "tracing-subscriber")]
73mod tracing_subscriber;
74
75pub(crate) use secret_key::SECRET_KEY;
76
77#[cfg(feature = "preferences")]
78pub use preferences::Preferences;
79
80#[cfg(feature = "http-client")]
81use crate::{error::Error, extension::HeaderMapExt, trace::TraceContext};
82
83pub use agent::Agent;
84pub use app_type::AppType;
85pub use plugin::Plugin;
86pub use server_tag::ServerTag;
87pub use static_record::StaticRecord;
88
89pub trait Application {
91 type Routes;
93
94 const APP_TYPE: AppType;
96
97 fn register(self, routes: Self::Routes) -> Self;
99
100 fn run_with<T: AsyncScheduler + Send + 'static>(self, scheduler: T);
102
103 fn boot() -> Self
105 where
106 Self: Default,
107 {
108 #[cfg(feature = "dotenv")]
110 dotenvy::dotenv().ok();
111
112 #[cfg(feature = "tracing-subscriber")]
114 tracing_subscriber::init::<Self>();
115
116 secret_key::init::<Self>();
118
119 #[cfg(feature = "metrics")]
121 metrics_exporter::init::<Self>();
122
123 #[cfg(feature = "http-client")]
125 http_client::init::<Self>();
126
127 for path in SHARED_DIRS.values() {
129 if !path.exists() {
130 if let Err(err) = fs::create_dir_all(path) {
131 let path = path.display();
132 tracing::error!("fail to create the directory {path}: {err}");
133 }
134 }
135 }
136
137 Self::default()
138 }
139
140 fn boot_with<F>(init: F) -> Self
142 where
143 Self: Default,
144 F: FnOnce(&'static State<Map>),
145 {
146 let app = Self::boot();
147 init(Self::shared_state());
148 app
149 }
150
151 #[inline]
153 fn register_with(self, server_tag: ServerTag, routes: Self::Routes) -> Self
154 where
155 Self: Sized,
156 {
157 if server_tag == ServerTag::Debug {
158 self.register(routes)
159 } else {
160 self
161 }
162 }
163
164 #[inline]
166 fn register_debug(self, routes: Self::Routes) -> Self
167 where
168 Self: Sized,
169 {
170 self.register_with(ServerTag::Debug, routes)
171 }
172
173 #[inline]
175 fn add_plugin(self, plugin: Plugin) -> Self
176 where
177 Self: Sized,
178 {
179 tracing::info!(plugin_name = plugin.name());
180 self
181 }
182
183 #[inline]
185 fn shared_state() -> &'static State<Map> {
186 &SHARED_APP_STATE
187 }
188
189 #[inline]
191 fn env() -> &'static Env {
192 SHARED_APP_STATE.env()
193 }
194
195 #[inline]
197 fn config() -> &'static Table {
198 SHARED_APP_STATE.config()
199 }
200
201 #[inline]
203 fn state_data() -> &'static Map {
204 SHARED_APP_STATE.data()
205 }
206
207 #[inline]
209 fn name() -> &'static str {
210 APP_NAME.as_ref()
211 }
212
213 #[inline]
215 fn version() -> &'static str {
216 APP_VERSION.as_ref()
217 }
218
219 #[inline]
221 fn domain() -> &'static str {
222 APP_DOMAIN.as_ref()
223 }
224
225 #[inline]
232 fn secret_key() -> &'static [u8] {
233 SECRET_KEY.get().expect("fail to get the secret key")
234 }
235
236 #[inline]
238 fn project_dir() -> &'static PathBuf {
239 &PROJECT_DIR
240 }
241
242 #[inline]
249 fn config_dir() -> &'static PathBuf {
250 &CONFIG_DIR
251 }
252
253 #[inline]
265 fn shared_dir(name: &str) -> Cow<'_, PathBuf> {
266 SHARED_DIRS
267 .get(name)
268 .map(Cow::Borrowed)
269 .unwrap_or_else(|| Cow::Owned(Self::parse_path(name)))
270 }
271
272 #[inline]
274 fn parse_path(path: &str) -> PathBuf {
275 join_path(&PROJECT_DIR, path)
276 }
277
278 fn spawn<T>(self, mut scheduler: T) -> Self
280 where
281 Self: Sized,
282 T: Scheduler + Send + 'static,
283 {
284 thread::spawn(move || {
285 loop {
286 scheduler.tick();
287 if let Some(duration) = scheduler.time_till_next_job() {
288 thread::sleep(duration);
289 }
290 }
291 });
292 self
293 }
294
295 #[inline]
297 fn run(self)
298 where
299 Self: Sized,
300 {
301 self.run_with(AsyncJobScheduler::default());
302 }
303
304 #[inline]
306 async fn load() {}
307
308 #[inline]
310 async fn shutdown() {}
311
312 #[cfg(feature = "http-client")]
314 async fn fetch(url: &str, options: Option<&Map>) -> Result<reqwest::Response, Error> {
315 let mut trace_context = TraceContext::new();
316 trace_context.record_trace_state();
317 http_client::request_builder(url, options)?
318 .header("traceparent", trace_context.traceparent())
319 .header("tracestate", trace_context.tracestate())
320 .send()
321 .await
322 .map_err(Error::from)
323 }
324
325 #[cfg(feature = "http-client")]
328 async fn fetch_json<T: serde::de::DeserializeOwned>(
329 url: &str,
330 options: Option<&Map>,
331 ) -> Result<T, Error> {
332 let response = Self::fetch(url, options).await?.error_for_status()?;
333 let data = if response.headers().has_json_content_type() {
334 response.json().await?
335 } else {
336 let text = response.text().await?;
337 serde_json::from_str(&text)?
338 };
339 Ok(data)
340 }
341}
342
343fn join_path(dir: &Path, path: &str) -> PathBuf {
345 fn join_path_components(mut full_path: PathBuf, path: &str) -> PathBuf {
346 for component in Path::new(path).components() {
347 match component {
348 Component::CurDir => (),
349 Component::ParentDir => {
350 full_path.pop();
351 }
352 _ => {
353 full_path.push(component);
354 }
355 }
356 }
357 full_path
358 }
359
360 if path.starts_with('/') {
361 path.into()
362 } else if let Some(path) = path.strip_prefix("~/") {
363 if let Some(home_dir) = dirs::home_dir() {
364 join_path_components(home_dir, path)
365 } else {
366 join_path_components(dir.to_path_buf(), path)
367 }
368 } else {
369 join_path_components(dir.to_path_buf(), path)
370 }
371}
372
373static APP_NAME: LazyLock<&'static str> = LazyLock::new(|| {
375 SHARED_APP_STATE
376 .config()
377 .get_str("name")
378 .unwrap_or_else(|| {
379 env::var("CARGO_PKG_NAME")
380 .expect("fail to get the environment variable `CARGO_PKG_NAME`")
381 .leak()
382 })
383});
384
385static APP_VERSION: LazyLock<&'static str> = LazyLock::new(|| {
387 SHARED_APP_STATE
388 .config()
389 .get_str("version")
390 .unwrap_or_else(|| {
391 env::var("CARGO_PKG_VERSION")
392 .expect("fail to get the environment variable `CARGO_PKG_VERSION`")
393 .leak()
394 })
395});
396
397static APP_DOMAIN: LazyLock<&'static str> = LazyLock::new(|| {
399 SHARED_APP_STATE
400 .config()
401 .get_str("domain")
402 .unwrap_or("localhost")
403});
404
405static PROJECT_DIR: LazyLock<PathBuf> = LazyLock::new(|| {
407 env::var("CARGO_MANIFEST_DIR")
408 .ok()
409 .filter(|var| !var.is_empty())
410 .map(PathBuf::from)
411 .unwrap_or_else(|| {
412 if cfg!(not(debug_assertions)) && cfg!(target_os = "macos") {
413 if let Ok(mut path) = env::current_exe() {
414 path.pop();
415 if path.ends_with("Contents/MacOS") {
416 path.pop();
417 path.push("Resources");
418 if path.exists() && path.is_dir() {
419 return path;
420 }
421 }
422 }
423 }
424 tracing::warn!(
425 "fail to get the environment variable `CARGO_MANIFEST_DIR`; \
426 current directory will be used as the project directory"
427 );
428 env::current_dir()
429 .expect("project directory does not exist or permissions are insufficient")
430 })
431});
432
433static CONFIG_DIR: LazyLock<PathBuf> = LazyLock::new(|| {
435 env::var("ZINO_APP_CONFIG_DIR")
436 .ok()
437 .filter(|var| !var.is_empty())
438 .map(PathBuf::from)
439 .unwrap_or_else(|| PROJECT_DIR.join("config"))
440});
441
442static SHARED_DIRS: LazyLock<HashMap<String, PathBuf>> = LazyLock::new(|| {
444 let mut dirs = HashMap::new();
445 if let Some(config) = SHARED_APP_STATE.get_config("dirs") {
446 for (key, value) in config {
447 if let Some(path) = value.as_str() {
448 dirs.insert(key.to_owned(), join_path(&PROJECT_DIR, path));
449 }
450 }
451 }
452 dirs
453});
454
455static SHARED_APP_STATE: LazyLock<State<Map>> = LazyLock::new(|| {
457 let mut state = State::default();
458 state.load_config();
459
460 let config = state.config();
461 let app_name = config
462 .get_str("name")
463 .map(|s| s.to_owned())
464 .unwrap_or_else(|| {
465 env::var("CARGO_PKG_NAME")
466 .expect("fail to get the environment variable `CARGO_PKG_NAME`")
467 });
468 let app_version = config
469 .get_str("version")
470 .map(|s| s.to_owned())
471 .unwrap_or_else(|| {
472 env::var("CARGO_PKG_VERSION")
473 .expect("fail to get the environment variable `CARGO_PKG_VERSION`")
474 });
475
476 let mut data = Map::new();
477 data.upsert("app.name", app_name);
478 data.upsert("app.version", app_version);
479 data.upsert("app.booted_at", DateTime::now());
480 state.set_data(data);
481 state
482});