Add config for client

This commit is contained in:
fluo10 2025-05-03 11:17:38 +09:00
parent b3368076e8
commit bacc24e801
14 changed files with 398 additions and 25 deletions

79
Cargo.lock generated
View file

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

View file

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

View file

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

View file

@ -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<Tz>,
storage: Option<PartialClientStorageConfig>,
}
#[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<PartialClientConfig> = 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<PartialClientConfig> = 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());
}
}

View file

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

View file

@ -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<PathBuf>,
}

View file

@ -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<String>,
pub access_key: Option<String>,
}

8
dpts-config/src/error.rs Normal file
View file

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

View file

@ -1,9 +1,28 @@
mod client;
mod global;
mod error;
mod server;
mod user;
pub use error::Error;
pub use server::{
ServerConfig,
PartialServerConfig,
};
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
}
}

View file

@ -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<String>,
pub listen_ips: Vec<IpAddr>,
pub port: u16,
pub database_url: String
pub database_url: String,
pub time_zone: Tz,
}
impl ServerConfig {
}
impl TryFrom<PartialServerConfig> for ServerConfig {
type Error = Error;
fn try_from(p: PartialServerConfig) -> Result<ServerConfig, Error>{
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<ServerConfig>,
}
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<F, T>(&self, f: F) -> &ServerConfig where
F: FnOnce() -> T,
T: Future<Output = ServerConfig>
{
self.inner.get_or_init(f).await
}
}
#[derive(Deserialize)]
pub struct PartialServerConfig {
pub listen_ips: Option<Vec<String>>,
pub listen_ips: Option<Vec<IpAddr>>,
pub port: Option<u16>,
pub database_url: Option<String>,
pub time_zone: Option<Tz>,
}
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<Self, Error> {
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<Self, Self::Err> {
Ok(toml::from_str(s)?)
}
}

View file

@ -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
default-features = false
[dev-dependencies]
dpts-config = { workspace = true, features = ["test"] }

View file

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

View file

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