diff --git a/Cargo.lock b/Cargo.lock index fd2198e..771ccaf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -542,6 +542,28 @@ dependencies = [ "windows-link", ] +[[package]] +name = "chrono-tz" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "efdce149c370f133a071ca8ef6ea340b7b88748ab0810097a9e2976eaa34b4f3" +dependencies = [ + "chrono", + "chrono-tz-build", + "phf", + "serde", +] + +[[package]] +name = "chrono-tz-build" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f10f8c9340e31fc120ff885fcdb54a0b48e474bbd77cab557f0c30a3e569402" +dependencies = [ + "parse-zoneinfo", + "phf_codegen", +] + [[package]] name = "clap" version = "4.5.37" @@ -795,9 +817,12 @@ dependencies = [ name = "dpts-config" version = "0.1.0" dependencies = [ + "chrono-tz", "dpts-entity", "dpts-error", + "iana-time-zone", "serde", + "thiserror 2.0.12", "tokio", "toml", ] @@ -821,6 +846,7 @@ dependencies = [ "axum", "chrono", "dotenv", + "dpts-config", "dpts-entity", "dpts-migration", "log", @@ -1895,6 +1921,15 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "parse-zoneinfo" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f2a05b18d44e2957b88f96ba460715e295bc1d7510468a2f3d3b44535d26c24" +dependencies = [ + "regex", +] + [[package]] name = "pem-rfc7468" version = "0.7.0" @@ -1955,6 +1990,44 @@ dependencies = [ "sha2", ] +[[package]] +name = "phf" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" +dependencies = [ + "phf_shared", +] + +[[package]] +name = "phf_codegen" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aef8048c789fa5e851558d709946d6d79a8ff88c0440c587967f8e94bfb1216a" +dependencies = [ + "phf_generator", + "phf_shared", +] + +[[package]] +name = "phf_generator" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" +dependencies = [ + "phf_shared", + "rand", +] + +[[package]] +name = "phf_shared" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" +dependencies = [ + "siphasher", +] + [[package]] name = "pin-project-lite" version = "0.2.16" @@ -2580,6 +2653,12 @@ dependencies = [ "rand_core", ] +[[package]] +name = "siphasher" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" + [[package]] name = "slab" version = "0.4.9" diff --git a/Cargo.toml b/Cargo.toml index 4457dd9..e292f05 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,9 +25,11 @@ dpts-entity = { path = "dpts-entity" } dpts-error = { path = "dpts-error" } dpts-migration = {path = "dpts-migration"} chrono = {version = "0.4", features = ["serde"]} +chrono-tz = {version = "0.10.3", features = ["serde"]} clap = "4.5" dotenv = "0.15.0" serde = { version = "1.0", features = ["derive"] } +thiserror = "2.0.12" tokio = "1.44.2" toml = "0.8.22" diff --git a/dpts-config/Cargo.toml b/dpts-config/Cargo.toml index b70c6eb..c30c131 100644 --- a/dpts-config/Cargo.toml +++ b/dpts-config/Cargo.toml @@ -9,10 +9,14 @@ repository.workspace = true default = ["client", "server"] client = [] server = [] +test = [] [dependencies] +chrono-tz.workspace = true dpts-entity.workspace = true dpts-error.workspace = true +iana-time-zone = "0.1.63" serde.workspace = true +thiserror.workspace = true tokio.workspace = true toml.workspace = true diff --git a/dpts-config/src/client.rs b/dpts-config/src/client.rs index e69de29..5bab7f9 100644 --- a/dpts-config/src/client.rs +++ b/dpts-config/src/client.rs @@ -0,0 +1,87 @@ +mod storage; + +pub use storage::*; + +use serde::{ + Deserialize, + Serialize +}; + +use chrono_tz::{Tz, UTC}; +use crate::{get_host_time_zone_or_utc, Error}; +use std::{ + str::FromStr, + net::IpAddr +}; + + + +pub struct ClientConfig { + +} +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] +pub struct PartialClientConfig { + time_zone: Option, + storage: Option, +} + +#[cfg(test)] +mod tests { + use super::*; + use tokio::sync::OnceCell; + use std::path::PathBuf; + const EMPTY_CONFIG_TOML: &str = r#""#; + static EMPTY_CONFIG_STRUCT: OnceCell = OnceCell::const_new(); + + async fn get_empty_config_struct() -> &'static PartialClientConfig { + EMPTY_CONFIG_STRUCT.get_or_init(|| async { + PartialClientConfig{ + time_zone: None, + storage: None, + } + }).await + + } + + #[tokio::test] + async fn deserialize_empty_client_config() { + let config: PartialClientConfig = toml::from_str(EMPTY_CONFIG_TOML).unwrap(); + assert_eq!(&config, get_empty_config_struct().await); + } + + #[tokio::test] + async fn serialize_empty_client_config() { + assert_eq!(EMPTY_CONFIG_TOML, toml::to_string(get_empty_config_struct().await).unwrap()); + } + + const LOCAL_STORAGE_CONFIG_TOML: &str = r#"time_zone = "UTC" + +[storage.local] +database_path = "." +"#; + static LOCAL_STORAGE_CONFIG_STRUCT: OnceCell = OnceCell::const_new(); + + async fn get_local_storage_client_config_struct() -> &'static PartialClientConfig { + LOCAL_STORAGE_CONFIG_STRUCT.get_or_init(|| async { + PartialClientConfig{ + time_zone: Some(UTC), + storage: Some(PartialClientStorageConfig::Local( + PartialClientLocalStorageConfig { + database_path: Some(PathBuf::from_str(".").unwrap()), + } + )), + } + }).await + } + #[tokio::test] + async fn deserialize_local_storage_client_config() { + let config: PartialClientConfig = toml::from_str(LOCAL_STORAGE_CONFIG_TOML).unwrap(); + assert_eq!(&config, get_local_storage_client_config_struct().await); + } + + #[tokio::test] + async fn serialize_local_storage_client_config() { + assert_eq!(LOCAL_STORAGE_CONFIG_TOML, toml::to_string(get_local_storage_client_config_struct().await).unwrap()); + } +} + diff --git a/dpts-config/src/client/storage.rs b/dpts-config/src/client/storage.rs new file mode 100644 index 0000000..5b991ab --- /dev/null +++ b/dpts-config/src/client/storage.rs @@ -0,0 +1,29 @@ +mod local; +mod remote; + +pub use local::{ + ClientLocalStorageConfig, + PartialClientLocalStorageConfig, +}; + +pub use remote::{ + ClientRemoteStorageConfig, + PartialClientRemoteStorageConfig, +}; + +use serde::{ + Deserialize, + Serialize +}; + +pub enum ClientStorageConfig { + Local(ClientLocalStorageConfig), + Remote(ClientRemoteStorageConfig), +} + +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] +#[serde(rename_all = "snake_case")] +pub enum PartialClientStorageConfig { + Local(PartialClientLocalStorageConfig), + Remote(PartialClientRemoteStorageConfig), +} \ No newline at end of file diff --git a/dpts-config/src/client/storage/local.rs b/dpts-config/src/client/storage/local.rs new file mode 100644 index 0000000..ebcf729 --- /dev/null +++ b/dpts-config/src/client/storage/local.rs @@ -0,0 +1,15 @@ +use std::path::PathBuf; +use serde::{ + Deserialize, + Serialize +}; + +pub struct ClientLocalStorageConfig { + pub database_path: PathBuf +} + +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] +pub struct PartialClientLocalStorageConfig { + pub database_path: Option, +} + diff --git a/dpts-config/src/client/storage/remote.rs b/dpts-config/src/client/storage/remote.rs new file mode 100644 index 0000000..c7c13b2 --- /dev/null +++ b/dpts-config/src/client/storage/remote.rs @@ -0,0 +1,22 @@ +use chrono_tz::{Tz, UTC}; +use crate::{get_host_time_zone_or_utc, Error}; +use serde::{ + Deserialize, + Serialize +}; + +use std::{ + str::FromStr, + net::IpAddr +}; + +pub struct ClientRemoteStorageConfig { + pub endpoint: String, + pub access_key: String, +} + +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] +pub struct PartialClientRemoteStorageConfig { + pub endpoint: Option, + pub access_key: Option, +} \ No newline at end of file diff --git a/dpts-config/src/error.rs b/dpts-config/src/error.rs new file mode 100644 index 0000000..acc3400 --- /dev/null +++ b/dpts-config/src/error.rs @@ -0,0 +1,8 @@ +#[derive(thiserror::Error, Debug)] +pub enum Error { + #[error("Parse toml error")] + TomlDe(#[from] toml::de::Error), + #[error("Missing config value: ({0})")] + MissingConfig(String) + +} diff --git a/dpts-config/src/lib.rs b/dpts-config/src/lib.rs index 43d2a5d..469b4c3 100644 --- a/dpts-config/src/lib.rs +++ b/dpts-config/src/lib.rs @@ -1,9 +1,28 @@ mod client; -mod global; +mod error; mod server; mod user; +pub use error::Error; pub use server::{ ServerConfig, PartialServerConfig, -}; \ No newline at end of file + SERVER_CONFIG, +}; + +pub use client::{ + ClientConfig, + PartialClientConfig, +}; + +use chrono_tz::{Tz, UTC}; + +fn get_host_time_zone_or_utc() -> Tz { + match iana_time_zone::get_timezone() { + Ok(x) => match x.parse(){ + Ok(x) => x, + Err(_) => UTC + }, + Err(_) => UTC + } +} \ No newline at end of file diff --git a/dpts-config/src/server.rs b/dpts-config/src/server.rs index f21c088..113a0dd 100644 --- a/dpts-config/src/server.rs +++ b/dpts-config/src/server.rs @@ -1,37 +1,114 @@ -use dpts_error::Error; -use serde::{Deserialize, Serialize}; +use chrono_tz::{Tz, UTC}; +use crate::{get_host_time_zone_or_utc, Error}; +use serde::Deserialize; +use std::{ + str::FromStr, + net::IpAddr +}; +use tokio::sync::OnceCell; + +pub static SERVER_CONFIG: OnceServerConfig = OnceServerConfig::const_new(); #[derive(Deserialize)] pub struct ServerConfig { - pub listen_ips: Vec, + pub listen_ips: Vec, pub port: u16, - pub database_url: String + pub database_url: String, + pub time_zone: Tz, +} + +impl ServerConfig { + +} + +impl TryFrom for ServerConfig { + type Error = Error; + fn try_from(p: PartialServerConfig) -> Result{ + Ok(ServerConfig{ + listen_ips: p.listen_ips.ok_or(Error::MissingConfig("listen_ips".to_string()))?, + port: p.port.ok_or(Error::MissingConfig("port".to_string()))?, + database_url: p.database_url.ok_or(Error::MissingConfig("database_url".to_string()))?, + time_zone: p.time_zone.ok_or(Error::MissingConfig("time_zone".to_string()))?, + }) + } +} + +pub struct OnceServerConfig { + inner: OnceCell, +} + +impl OnceServerConfig { + const fn const_new() -> Self { + Self { + inner: OnceCell::const_new(), + } + } + pub fn get(&self) -> Option<&ServerConfig> { + self.inner.get() + } + pub async fn get_or_init(&self, f: F) -> &ServerConfig where + F: FnOnce() -> T, + T: Future + { + self.inner.get_or_init(f).await + } } #[derive(Deserialize)] pub struct PartialServerConfig { - pub listen_ips: Option>, + pub listen_ips: Option>, pub port: Option, pub database_url: Option, + pub time_zone: Option, } impl PartialServerConfig { - /// #Examples - /// ``` - /// use dpts_config::PartialServerConfig; - /// let config = PartialServerConfig::try_from_toml(r#" - /// listen_ips = ["0.0.0.0"] - /// port = 8000 - /// database_url = "sqlite::memory:" - /// "#).unwrap(); - /// assert_eq!(config.listen_ips, Some(vec!["0.0.0.0".to_string()])); - /// assert_eq!(config.port, Some(8000)); - /// assert_eq!(config.database_url, Some("sqlite::memory:".to_string())); - /// ``` - /// + pub fn try_from_toml(s: &str) -> Result { Ok(toml::from_str(s)?) } } +impl Default for PartialServerConfig { + /// #Examples + /// ``` + /// use dpts_config::PartialServerConfig; + /// let config = PartialServerConfig::default(); + /// assert_eq!(config.listen_ips, Some(vec!["127.0.0.1".parse().unwrap(), "::1".parse().unwrap()])); + /// assert_eq!(config.port, Some(3000)); + /// assert_eq!(config.database_url, None); + /// assert_eq!(config.time_zone, Some(iana_time_zone::get_timezone().unwrap().parse().unwrap())) + /// ``` + /// + fn default() -> Self { + PartialServerConfig { + listen_ips: Some(vec!["127.0.0.1".parse().unwrap(), "::1".parse().unwrap()]), + port: Some(3000), + database_url: None, + time_zone: Some(get_host_time_zone_or_utc()) + } + } +} + +impl FromStr for PartialServerConfig { + type Err = Error; + /// #Examples + /// ``` + /// use dpts_config::PartialServerConfig; + /// let config: PartialServerConfig = r#" + /// listen_ips = ["0.0.0.0"] + /// port = 8000 + /// database_url = "sqlite::memory:" + /// time_zone = "Asia/Tokyo" + /// "#.parse().unwrap(); + /// assert_eq!(config.listen_ips, Some(vec!["0.0.0.0".parse().unwrap()])); + /// assert_eq!(config.port, Some(8000)); + /// assert_eq!(config.database_url, Some("sqlite::memory:".to_string())); + /// assert_eq!(config.time_zone, Some("Asia/Tokyo".parse().unwrap())) + /// ``` + /// + fn from_str(s: &str) -> Result { + Ok(toml::from_str(s)?) + } +} diff --git a/dpts-database/Cargo.toml b/dpts-database/Cargo.toml index 89627de..14b9fe6 100644 --- a/dpts-database/Cargo.toml +++ b/dpts-database/Cargo.toml @@ -6,10 +6,13 @@ license.workspace = true repository.workspace = true [features] -default = [] +default = ["server", "client"] +client = [] +server = [] test = [] [dependencies] +dpts-config.workspace = true dpts-entity = { workspace = true } dpts-migration = { workspace = true } async-graphql = {version = "7.0", features = ["chrono"]} @@ -28,4 +31,7 @@ features = [ "sqlx-sqlite", "with-chrono", ] -default-features = false \ No newline at end of file +default-features = false + +[dev-dependencies] +dpts-config = { workspace = true, features = ["test"] } \ No newline at end of file diff --git a/dpts-database/src/connection.rs b/dpts-database/src/connection.rs index 1688316..96b8c07 100644 --- a/dpts-database/src/connection.rs +++ b/dpts-database/src/connection.rs @@ -1,7 +1,7 @@ use std::time::Duration; use sea_orm::{entity::*, query::*, ConnectOptions, Database, DatabaseConnection}; use dpts_migration::{Migrator, MigratorTrait}; - +use dpts_config::ServerConfig; use tokio::sync::OnceCell; @@ -25,6 +25,27 @@ impl OnceDatabaseConnection { self.inner.get_or_init(f).await } + pub async fn get_or_init_with_server_config(&self, c: &ServerConfig) -> &DatabaseConnection { + self.get_or_init( || async { + let mut opt = ConnectOptions::new(&c.database_url); + opt.max_connections(100) + .min_connections(5) + .connect_timeout(Duration::from_secs(8)) + .acquire_timeout(Duration::from_secs(8)) + .idle_timeout(Duration::from_secs(8)) + .max_lifetime(Duration::from_secs(8)) + .sqlx_logging(true) + .sqlx_logging_level(log::LevelFilter::Info); + //.set_schema_search_path("my_schema"); // Setting default PostgreSQL schema + let db = Database::connect(opt).await.unwrap(); + Migrator::fresh(&db).await.unwrap(); + db + }).await + } + pub async fn get_or_init_with_static_server_config(&self) -> &DatabaseConnection { + self.get_or_init_with_server_config(dpts_config::SERVER_CONFIG.get().unwrap()).await + } + #[cfg(test)] pub async fn init_test(&self) { self.get_or_init( || async { @@ -47,6 +68,7 @@ impl OnceDatabaseConnection { } + pub static DATABASE_CONNECTION: OnceDatabaseConnection = OnceDatabaseConnection::new(); #[cfg(test)] diff --git a/dpts-config/src/global.rs b/dpts-error/src/config.rs similarity index 100% rename from dpts-config/src/global.rs rename to dpts-error/src/config.rs diff --git a/dpts-error/src/lib.rs b/dpts-error/src/lib.rs index 0b889bc..b99e6ee 100644 --- a/dpts-error/src/lib.rs +++ b/dpts-error/src/lib.rs @@ -3,11 +3,14 @@ pub enum Error { #[error("Parser error")] Clap(#[from] clap::Error), #[error("Parse int error")] - ParseIntError(#[from] std::num::ParseIntError), + ParseInt(#[from] std::num::ParseIntError), #[error("IO Error")] - IoError(#[from] std::io::Error), + Io(#[from] std::io::Error), #[error("Parse toml error")] TomlDe(#[from] toml::de::Error), + #[error("Missing config value: ({0})")] + MissingConfig(String) + } impl Error {