diff --git a/README.md b/README.md index c304781..d68902e 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,7 @@ # zino -`zino` is a full featured web application framework which focuses on productivity and performance. +`zino` is a full featured web application framework for Rust which focuses on +productivity and performance. [![Crates.io](https://img.shields.io/crates/v/zino)][zino] [![Documentation](https://shields.io/docsrs/zino)][zino-docs] diff --git a/examples/axum-app/app/src/controller/user.rs b/examples/axum-app/app/src/controller/user.rs index 1513079..4ab7420 100644 --- a/examples/axum-app/app/src/controller/user.rs +++ b/examples/axum-app/app/src/controller/user.rs @@ -1,6 +1,6 @@ use serde_json::json; -use zino::Request; -use zino_core::{Model, Query, Rejection, RequestContext, Response, Schema, Uuid}; +use zino::{AxumCluster, Request}; +use zino_core::{Application, Model, Query, Rejection, RequestContext, Response, Schema, Uuid}; use zino_model::User; pub(crate) async fn new(mut req: Request) -> zino::Result { @@ -63,7 +63,8 @@ pub(crate) async fn view(mut req: Request) -> zino::Result { let data = json!({ "user": user, - "state": state_data, + "app_state_data": AxumCluster::state_data(), + "state_data": state_data, "config": req.config(), }); res.set_data(data); diff --git a/examples/axum-app/config/config.dev.toml b/examples/axum-app/config/config.dev.toml index bfc4496..7abe01a 100644 --- a/examples/axum-app/config/config.dev.toml +++ b/examples/axum-app/config/config.dev.toml @@ -1,6 +1,6 @@ name = "data-cube" -version = "0.2.2" +version = "0.3.0" [main] host = "127.0.0.1" diff --git a/examples/axum-app/config/config.prod.toml b/examples/axum-app/config/config.prod.toml index 50c9f14..1b551ca 100644 --- a/examples/axum-app/config/config.prod.toml +++ b/examples/axum-app/config/config.prod.toml @@ -1,6 +1,6 @@ name = "data-cube" -version = "0.2.2" +version = "0.3.0" [main] host = "127.0.0.1" diff --git a/zino-core/src/application.rs b/zino-core/src/application.rs index ec15e90..1a65bda 100644 --- a/zino-core/src/application.rs +++ b/zino-core/src/application.rs @@ -1,5 +1,5 @@ -use crate::{AsyncCronJob, CronJob, Job, JobScheduler, State}; -use std::{collections::HashMap, sync::LazyLock, thread, time::Instant}; +use crate::{AsyncCronJob, CronJob, Job, JobScheduler, Map, State}; +use std::{collections::HashMap, thread}; use toml::value::Table; /// Application. @@ -10,8 +10,8 @@ pub trait Application { /// Creates a new application. fn new() -> Self; - /// Returns the start time. - fn start_time(&self) -> Instant; + /// Returns a reference to the shared application state. + fn shared() -> &'static State; /// Registers routes. fn register(self, routes: HashMap<&'static str, Self::Router>) -> Self; @@ -22,13 +22,19 @@ pub trait Application { /// Returns the application env. #[inline] fn env() -> &'static str { - SHARED_STATE.env() + Self::shared().env() } - /// Returns a reference to the application scoped config. + /// Returns a reference to the shared application config. #[inline] fn config() -> &'static Table { - SHARED_STATE.config() + Self::shared().config() + } + + /// Returns a reference to the shared application state data. + #[inline] + fn state_data() -> &'static Map { + Self::shared().data() } /// Spawns a new thread to run cron jobs. @@ -47,6 +53,3 @@ pub trait Application { self } } - -/// Shared application state. -static SHARED_STATE: LazyLock = LazyLock::new(State::default); diff --git a/zino-core/src/database/column.rs b/zino-core/src/database/column.rs index c08d5f2..fe8988b 100644 --- a/zino-core/src/database/column.rs +++ b/zino-core/src/database/column.rs @@ -4,7 +4,7 @@ use serde::Serialize; use serde_json::Value; use sqlx::{postgres::PgRow, Column as _, Error, Row, TypeInfo}; -/// A column is a model field with associated metadata. +/// A model field with associated metadata. #[derive(Debug, Clone, Serialize)] pub struct Column<'a> { /// Column name. diff --git a/zino-core/src/database/mod.rs b/zino-core/src/database/mod.rs index 331eb64..b1f625b 100644 --- a/zino-core/src/database/mod.rs +++ b/zino-core/src/database/mod.rs @@ -1,4 +1,4 @@ -use crate::{crypto, State}; +use crate::{crypto, state::SHARED_STATE}; use sqlx::{ postgres::{PgConnectOptions, PgPoolOptions}, PgPool, @@ -167,21 +167,31 @@ impl ConnectionPool { } } -/// Shared state. -pub(super) static SHARED_STATE: LazyLock = LazyLock::new(|| { - let mut state = State::default(); +/// A list of database connection pools. +#[derive(Debug, Clone)] +pub(crate) struct ConnectionPools(Vec); + +impl ConnectionPools { + /// Returns a connection pool with the specific name. + #[inline] + pub(crate) fn get_pool(&self, name: &str) -> Option<&ConnectionPool> { + self.0.iter().find(|c| c.name() == name) + } +} + +/// Shared connection pools. +pub(super) static SHARED_CONNECTION_POOLS: LazyLock = LazyLock::new(|| { + let config = SHARED_STATE.config(); // Application name. - let application_name = state - .config() + let application_name = config .get("name") .and_then(|t| t.as_str()) .expect("the `name` field should be specified"); // Database connection pools. let mut pools = Vec::new(); - let databases = state - .config() + let databases = config .get("postgres") .expect("the `postgres` field should be specified") .as_array() @@ -195,10 +205,7 @@ pub(super) static SHARED_STATE: LazyLock = LazyLock::new(|| { pools.push(pool); } } - if !pools.is_empty() { - state.set_pools(pools); - } - state + ConnectionPools(pools) }); /// Database namespace prefix. diff --git a/zino-core/src/database/schema.rs b/zino-core/src/database/schema.rs index aeb5075..06711ba 100644 --- a/zino-core/src/database/schema.rs +++ b/zino-core/src/database/schema.rs @@ -57,7 +57,7 @@ pub trait Schema: 'static + Send + Sync + Model { /// Initializes the model reader. #[inline] fn init_reader() -> Result<&'static ConnectionPool, Error> { - super::SHARED_STATE + super::SHARED_CONNECTION_POOLS .get_pool(Self::READER_NAME) .ok_or(Error::PoolClosed) } @@ -65,7 +65,7 @@ pub trait Schema: 'static + Send + Sync + Model { /// Initializes the model writer. #[inline] fn init_writer() -> Result<&'static ConnectionPool, Error> { - super::SHARED_STATE + super::SHARED_CONNECTION_POOLS .get_pool(Self::WRITER_NAME) .ok_or(Error::PoolClosed) } diff --git a/zino-core/src/datetime.rs b/zino-core/src/datetime.rs index 943db0b..2b6c781 100644 --- a/zino-core/src/datetime.rs +++ b/zino-core/src/datetime.rs @@ -3,6 +3,7 @@ use chrono::{ Local, SecondsFormat, TimeZone, Utc, }; use serde::{Deserialize, Serialize}; +use serde_json::Value; use std::{ fmt, ops::{Add, AddAssign, Sub, SubAssign}, @@ -64,15 +65,16 @@ impl DateTime { /// Returns an RFC 2822 date and time string. #[inline] pub fn to_utc_string(&self) -> String { - let datetime = self.0.with_timezone(&Utc).to_rfc2822(); - format!("{} GMT", datetime.trim_end_matches(" +0000")) + let datetime = self.0.with_timezone(&Utc); + format!("{} GMT", datetime.to_rfc2822().trim_end_matches(" +0000")) } /// Return an RFC 3339 and ISO 8601 date and time string with subseconds /// formatted as [`SecondsFormat::Millis`](chrono::SecondsFormat::Millis). #[inline] pub fn to_iso_string(&self) -> String { - self.0.to_rfc3339_opts(SecondsFormat::Millis, true) + let datetime = self.0.with_timezone(&Utc); + datetime.to_rfc3339_opts(SecondsFormat::Millis, true) } } @@ -83,15 +85,21 @@ impl Default for DateTime { } } +impl From> for DateTime { + fn from(dt: chrono::DateTime) -> Self { + Self(dt) + } +} + impl From for chrono::DateTime { fn from(dt: DateTime) -> Self { dt.0 } } -impl From> for DateTime { - fn from(dt: chrono::DateTime) -> Self { - Self(dt) +impl From for Value { + fn from(dt: DateTime) -> Self { + Value::String(dt.to_string()) } } diff --git a/zino-core/src/request/mod.rs b/zino-core/src/request/mod.rs index f2ee746..02c0f13 100644 --- a/zino-core/src/request/mod.rs +++ b/zino-core/src/request/mod.rs @@ -16,7 +16,7 @@ pub use validation::Validation; /// Request context. pub trait RequestContext { - /// Returns a reference to the application scoped config. + /// Returns a reference to the application config. fn config(&self) -> &Table; /// Returns a reference to the request scoped state data. diff --git a/zino-core/src/state.rs b/zino-core/src/state.rs index 3c1a4c5..7ff1d04 100644 --- a/zino-core/src/state.rs +++ b/zino-core/src/state.rs @@ -1,5 +1,5 @@ -use crate::{ConnectionPool, Map}; -use std::{env, fs, path::Path}; +use crate::Map; +use std::{env, fs, path::Path, sync::LazyLock}; use toml::value::{Table, Value}; /// Application state. @@ -9,8 +9,6 @@ pub struct State { env: String, /// Configuration. config: Table, - /// Connection pools. - pools: Vec, /// Associated data. data: Map, } @@ -22,7 +20,6 @@ impl State { Self { env, config: Table::new(), - pools: Vec::new(), data: Map::new(), } } @@ -46,6 +43,12 @@ impl State { } } + /// Set the state data. + #[inline] + pub fn set_data(&mut self, data: Map) { + self.data = data; + } + /// Returns the env as `&str`. #[inline] pub fn env(&self) -> &str { @@ -73,7 +76,7 @@ impl State { /// Returns a list of listeners as `Vec`. pub fn listeners(&self) -> Vec { let config = self.config(); - let mut listeners = vec![]; + let mut listeners = Vec::new(); // Main server. let main = config @@ -122,32 +125,24 @@ impl State { listeners } - - /// Returns a connection pool with the specific name. - #[inline] - pub(crate) fn get_pool(&self, name: &str) -> Option<&ConnectionPool> { - self.pools.iter().find(|c| c.name() == name) - } - - /// Sets the connection pools. - #[inline] - pub(crate) fn set_pools(&mut self, pools: Vec) { - self.pools = pools; - } } impl Default for State { - #[inline] fn default() -> Self { - let mut app_env = "dev".to_string(); - for arg in env::args() { - if arg.starts_with("--env=") { - app_env = arg.strip_prefix("--env=").unwrap().to_string(); - } - } - - let mut state = State::new(app_env); - state.load_config(); - state + SHARED_STATE.clone() } } + +/// Shared application state. +pub(crate) static SHARED_STATE: LazyLock = LazyLock::new(|| { + let mut app_env = "dev".to_string(); + for arg in env::args() { + if arg.starts_with("--env=") { + app_env = arg.strip_prefix("--env=").unwrap().to_string(); + } + } + + let mut state = State::new(app_env); + state.load_config(); + state +}); diff --git a/zino/Cargo.toml b/zino/Cargo.toml index 1b4d0c2..20f3681 100644 --- a/zino/Cargo.toml +++ b/zino/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "zino" -description = "A full featured web application framework." +description = "Full featured web application framework for Rust." version = "0.3.0" rust-version = "1.68" edition = "2021" diff --git a/zino/README.md b/zino/README.md index b5a6201..586cbc4 100644 --- a/zino/README.md +++ b/zino/README.md @@ -1,6 +1,7 @@ # zino -`zino` is a full featured web application framework which focuses on productivity and performance. +`zino` is a full featured web application framework for Rust which focuses on +productivity and performance. ## Highlights diff --git a/zino/src/channel/axum_channel.rs b/zino/src/channel/axum_channel.rs index 2187fa5..8141e6f 100644 --- a/zino/src/channel/axum_channel.rs +++ b/zino/src/channel/axum_channel.rs @@ -117,8 +117,9 @@ impl Default for MessageChannel { } /// Channel capacity. -static CHANNEL_CAPACITY: LazyLock = - LazyLock::new(|| match crate::AxumCluster::config().get("channel") { +static CHANNEL_CAPACITY: LazyLock = LazyLock::new(|| { + let config = crate::AxumCluster::config(); + match config.get("channel") { Some(config) => config .as_table() .expect("the `channel` field should be a table") @@ -129,7 +130,8 @@ static CHANNEL_CAPACITY: LazyLock = .try_into() .expect("the `channel.capacity` field should be a positive integer"), None => 10000, - }); + } +}); /// Channel subscribers. static CHANNEL_SUBSCRIBERS: LazyLock>> = diff --git a/zino/src/cluster/axum_cluster.rs b/zino/src/cluster/axum_cluster.rs index 5597fd7..6801643 100644 --- a/zino/src/cluster/axum_cluster.rs +++ b/zino/src/cluster/axum_cluster.rs @@ -12,8 +12,8 @@ use std::{ env, io, net::SocketAddr, path::Path, - sync::{Arc, LazyLock}, - time::{Duration, Instant}, + sync::LazyLock, + time::Duration, }; use tokio::runtime::Builder; use tower::{ @@ -27,12 +27,10 @@ use tower_http::{ }; use tracing::Level; use tracing_subscriber::fmt::{time, writer::MakeWriterExt}; -use zino_core::{Application, AsyncCronJob, Job, JobScheduler, Response, State}; +use zino_core::{Application, AsyncCronJob, DateTime, Job, JobScheduler, Map, Response, State}; /// An HTTP server cluster for `axum`. pub struct AxumCluster { - /// Start time. - start_time: Instant, /// Routes. routes: HashMap<&'static str, Router>, } @@ -98,15 +96,14 @@ impl Application for AxumCluster { .init(); Self { - start_time: Instant::now(), routes: HashMap::new(), } } - /// Returns the start time. + /// Returns a reference to the shared application state. #[inline] - fn start_time(&self) -> Instant { - self.start_time + fn shared() -> &'static State { + LazyLock::force(&SHARED_CLUSTER_STATE) } /// Registers routes. @@ -171,7 +168,7 @@ impl Application for AxumCluster { app = app.nest(path, route.clone()); } - let state = Arc::new(app_state.clone()); + let state = app_state.clone(); app = app .fallback_service(tower::service_fn(|_| async { let res = Response::new(StatusCode::NOT_FOUND); @@ -220,3 +217,24 @@ impl Application for AxumCluster { }); } } + +/// Shared cluster state. +static SHARED_CLUSTER_STATE: LazyLock = LazyLock::new(|| { + let mut state = State::default(); + let config = state.config(); + let app_name = config + .get("name") + .and_then(|t| t.as_str()) + .expect("the `name` field should be specified"); + let app_version = config + .get("version") + .and_then(|t| t.as_str()) + .expect("the `version` field should be specified"); + + let mut data = Map::new(); + data.insert("app_name".to_string(), app_name.into()); + data.insert("app_version".to_string(), app_version.into()); + data.insert("cluster_start_at".to_string(), DateTime::now().into()); + state.set_data(data); + state +}); diff --git a/zino/src/lib.rs b/zino/src/lib.rs index db8e4f9..b37d019 100644 --- a/zino/src/lib.rs +++ b/zino/src/lib.rs @@ -1,4 +1,5 @@ -//! [`zino`] is a full featured web application framework which focuses on productivity and performance. +//! [`zino`] is a full featured web application framework for Rust +//! which focuses on productivity and performance. //! //! ## Highlights //! diff --git a/zino/src/middleware/tower_cors.rs b/zino/src/middleware/tower_cors.rs index 13b485b..0b6a390 100644 --- a/zino/src/middleware/tower_cors.rs +++ b/zino/src/middleware/tower_cors.rs @@ -4,10 +4,8 @@ use zino_core::Application; // CORS middleware. pub(crate) static CORS_MIDDLEWARE: LazyLock = LazyLock::new(|| { - match crate::AxumCluster::config() - .get("cors") - .and_then(|t| t.as_table()) - { + let config = crate::AxumCluster::config(); + match config.get("cors").and_then(|t| t.as_table()) { Some(cors) => { let allow_credentials = cors .get("allow-credentials") diff --git a/zino/src/request/axum_request.rs b/zino/src/request/axum_request.rs index 06e06cb..e9fd8a8 100644 --- a/zino/src/request/axum_request.rs +++ b/zino/src/request/axum_request.rs @@ -10,7 +10,6 @@ use serde::de::DeserializeOwned; use std::{ convert::Infallible, ops::{Deref, DerefMut}, - sync::Arc, }; use toml::value::Table; use zino_core::{CloudEvent, Context, Map, Rejection, RequestContext, State, Validation}; @@ -37,18 +36,18 @@ impl DerefMut for AxumExtractor { impl RequestContext for AxumExtractor> { #[inline] fn config(&self) -> &Table { - let state = self.extensions().get::>().unwrap(); + let state = self.extensions().get::().unwrap(); state.config() } fn state_data(&self) -> &Map { - let state = self.extensions().get::>().unwrap(); + let state = self.extensions().get::().unwrap(); state.data() } fn state_data_mut(&mut self) -> &mut Map { - let state = self.extensions_mut().get_mut::>().unwrap(); - Arc::make_mut(state).data_mut() + let state = self.extensions_mut().get_mut::().unwrap(); + state.data_mut() } #[inline]