Refactor application state

This commit is contained in:
photino 2023-01-09 22:33:15 +08:00
parent 2e03cd0e39
commit 5bdd6081be
18 changed files with 126 additions and 92 deletions

View File

@ -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]

View File

@ -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);

View File

@ -1,6 +1,6 @@
name = "data-cube"
version = "0.2.2"
version = "0.3.0"
[main]
host = "127.0.0.1"

View File

@ -1,6 +1,6 @@
name = "data-cube"
version = "0.2.2"
version = "0.3.0"
[main]
host = "127.0.0.1"

View File

@ -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<State> = LazyLock::new(State::default);

View File

@ -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.

View File

@ -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<State> = LazyLock::new(|| {
let mut state = State::default();
/// A list of database connection pools.
#[derive(Debug, Clone)]
pub(crate) struct ConnectionPools(Vec<ConnectionPool>);
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<ConnectionPools> = 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<State> = LazyLock::new(|| {
pools.push(pool);
}
}
if !pools.is_empty() {
state.set_pools(pools);
}
state
ConnectionPools(pools)
});
/// Database namespace prefix.

View File

@ -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)
}

View File

@ -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<chrono::DateTime<Local>> for DateTime {
fn from(dt: chrono::DateTime<Local>) -> Self {
Self(dt)
}
}
impl From<DateTime> for chrono::DateTime<Local> {
fn from(dt: DateTime) -> Self {
dt.0
}
}
impl From<chrono::DateTime<Local>> for DateTime {
fn from(dt: chrono::DateTime<Local>) -> Self {
Self(dt)
impl From<DateTime> for Value {
fn from(dt: DateTime) -> Self {
Value::String(dt.to_string())
}
}

View File

@ -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.

View File

@ -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<ConnectionPool>,
/// 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<String>`.
pub fn listeners(&self) -> Vec<String> {
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<ConnectionPool>) {
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<State> = 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
});

View File

@ -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"

View File

@ -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

View File

@ -117,8 +117,9 @@ impl Default for MessageChannel {
}
/// Channel capacity.
static CHANNEL_CAPACITY: LazyLock<usize> =
LazyLock::new(|| match crate::AxumCluster::config().get("channel") {
static CHANNEL_CAPACITY: LazyLock<usize> = 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<usize> =
.try_into()
.expect("the `channel.capacity` field should be a positive integer"),
None => 10000,
});
}
});
/// Channel subscribers.
static CHANNEL_SUBSCRIBERS: LazyLock<RwLock<HashMap<Uuid, Subscriber>>> =

View File

@ -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<State> = 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
});

View File

@ -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
//!

View File

@ -4,10 +4,8 @@ use zino_core::Application;
// CORS middleware.
pub(crate) static CORS_MIDDLEWARE: LazyLock<CorsLayer> = 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")

View File

@ -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<T> DerefMut for AxumExtractor<T> {
impl RequestContext for AxumExtractor<Request<Body>> {
#[inline]
fn config(&self) -> &Table {
let state = self.extensions().get::<Arc<State>>().unwrap();
let state = self.extensions().get::<State>().unwrap();
state.config()
}
fn state_data(&self) -> &Map {
let state = self.extensions().get::<Arc<State>>().unwrap();
let state = self.extensions().get::<State>().unwrap();
state.data()
}
fn state_data_mut(&mut self) -> &mut Map {
let state = self.extensions_mut().get_mut::<Arc<State>>().unwrap();
Arc::make_mut(state).data_mut()
let state = self.extensions_mut().get_mut::<State>().unwrap();
state.data_mut()
}
#[inline]