From 85804bc7f89532ff692aa6ecc1864427210541fb Mon Sep 17 00:00:00 2001 From: photino Date: Sun, 10 Dec 2023 20:32:55 +0800 Subject: [PATCH] Add support for the config format JSON and YAML --- zino-core/Cargo.toml | 3 +- zino-core/src/application/secret_key.rs | 3 +- zino-core/src/auth/access_key.rs | 3 +- zino-core/src/auth/jwt_claims.rs | 3 +- zino-core/src/model/column.rs | 6 +-- zino-core/src/orm/column.rs | 2 +- zino-core/src/orm/helper.rs | 3 +- zino-core/src/orm/scalar.rs | 4 +- zino-core/src/orm/schema.rs | 71 ++++++++++++++----------- zino-core/src/orm/transaction.rs | 4 +- zino-core/src/state/config.rs | 35 ++++++++++++ zino-core/src/state/mod.rs | 51 ++++++++---------- zino-derive/docs/model.md | 9 ++-- zino-derive/src/model.rs | 27 +++++++--- 14 files changed, 143 insertions(+), 81 deletions(-) create mode 100644 zino-core/src/state/config.rs diff --git a/zino-core/Cargo.toml b/zino-core/Cargo.toml index ad6a87a2..dff243f5 100644 --- a/zino-core/Cargo.toml +++ b/zino-core/Cargo.toml @@ -199,8 +199,9 @@ reqwest-middleware = "0.2.4" reqwest-retry = "0.3.0" reqwest-tracing = "0.4.6" rmp-serde = "1.1.2" -ryu = "1.0.15" +ryu = "1.0.16" serde_qs = "0.12.0" +serde_yaml = "0.9.27" sha2 = "0.10.8" sysinfo = "0.29.11" task-local-extensions = "0.1.4" diff --git a/zino-core/src/application/secret_key.rs b/zino-core/src/application/secret_key.rs index 6aedc859..34a5cdba 100644 --- a/zino-core/src/application/secret_key.rs +++ b/zino-core/src/application/secret_key.rs @@ -19,7 +19,8 @@ pub(super) fn init() { crypto::digest(secret.as_bytes()) }); - let secret_key = crypto::derive_key("ZINO:APPLICATION", &checksum); + let info = config.get_str("info").unwrap_or("ZINO:APPLICATION"); + let secret_key = crypto::derive_key(info, &checksum); SECRET_KEY .set(secret_key) .expect("fail to set the secret key"); diff --git a/zino-core/src/auth/access_key.rs b/zino-core/src/auth/access_key.rs index ff3864e2..2e365df3 100644 --- a/zino-core/src/auth/access_key.rs +++ b/zino-core/src/auth/access_key.rs @@ -136,5 +136,6 @@ static SECRET_KEY: LazyLock<[u8; 64]> = LazyLock::new(|| { }); crypto::digest(secret.as_bytes()) }); - crypto::derive_key("ZINO:ACCESS-KEY", &checksum) + let info = config.get_str("info").unwrap_or("ZINO:ACCESS-KEY"); + crypto::derive_key(info, &checksum) }); diff --git a/zino-core/src/auth/jwt_claims.rs b/zino-core/src/auth/jwt_claims.rs index 93ee704a..592635b9 100644 --- a/zino-core/src/auth/jwt_claims.rs +++ b/zino-core/src/auth/jwt_claims.rs @@ -204,7 +204,8 @@ static SECRET_KEY: LazyLock = LazyLock::new(|| { }); crypto::digest(secret.as_bytes()) }); - let secret_key = crypto::derive_key("ZINO:JWT", &checksum); + let info = config.get_str("info").unwrap_or("ZINO:JWT"); + let secret_key = crypto::derive_key(info, &checksum); JwtHmacKey::from_bytes(&secret_key) }); diff --git a/zino-core/src/model/column.rs b/zino-core/src/model/column.rs index 289753b2..dbf2e894 100644 --- a/zino-core/src/model/column.rs +++ b/zino-core/src/model/column.rs @@ -152,7 +152,7 @@ impl<'a> Column<'a> { self.extra.contains_key(attribute) } - /// Returns `true` if the user has any of the specific attributes. + /// Returns `true` if the column has any of the specific attributes. pub fn has_any_attributes(&self, attributes: &[&str]) -> bool { for attribute in attributes { if self.has_attribute(attribute) { @@ -162,7 +162,7 @@ impl<'a> Column<'a> { false } - /// Returns `true` if the user has all of the specific attributes. + /// Returns `true` if the column has all of the specific attributes. pub fn has_all_attributes(&self, attributes: &[&str]) -> bool { for attribute in attributes { if !self.has_attribute(attribute) { @@ -549,7 +549,7 @@ impl<'a> Column<'a> { } } - /// Generates a mocked Json value for the column + /// Generates a mocked Json value for the column. pub fn mock_value(&self) -> JsonValue { if self.reference().is_some() { return JsonValue::Null; diff --git a/zino-core/src/orm/column.rs b/zino-core/src/orm/column.rs index f2c97d22..9e93ccd4 100644 --- a/zino-core/src/orm/column.rs +++ b/zino-core/src/orm/column.rs @@ -4,7 +4,7 @@ use crate::{ }; use convert_case::{Case, Casing}; -/// Extension trait for [`Column`](crate::model::Column). +/// Extension trait for [`Column`]. pub(super) trait ColumnExt { /// Returns the type annotation. fn type_annotation(&self) -> &'static str; diff --git a/zino-core/src/orm/helper.rs b/zino-core/src/orm/helper.rs index bf6f29a5..9f6af138 100644 --- a/zino-core/src/orm/helper.rs +++ b/zino-core/src/orm/helper.rs @@ -83,5 +83,6 @@ static SECRET_KEY: LazyLock<[u8; 64]> = LazyLock::new(|| { }); crypto::digest(secret.as_bytes()) }); - crypto::derive_key("ZINO:ORM", &checksum) + let info = config.get_str("info").unwrap_or("ZINO:ORM"); + crypto::derive_key(info, &checksum) }); diff --git a/zino-core/src/orm/scalar.rs b/zino-core/src/orm/scalar.rs index 901fcc23..8a3abbee 100644 --- a/zino-core/src/orm/scalar.rs +++ b/zino-core/src/orm/scalar.rs @@ -155,9 +155,9 @@ where } } -impl ScalarQuery for T +impl ScalarQuery for M where + M: Schema, K: Default + Display + PartialEq, - T: Schema, { } diff --git a/zino-core/src/orm/schema.rs b/zino-core/src/orm/schema.rs index d5e2ed3c..7ef98721 100644 --- a/zino-core/src/orm/schema.rs +++ b/zino-core/src/orm/schema.rs @@ -709,9 +709,10 @@ pub trait Schema: 'static + Send + Sync + ModelHooks { /// Finds a list of models selected by the query in the table, /// and decodes it as `Vec`. - async fn find>( - query: &Query, - ) -> Result, Error> { + async fn find(query: &Query) -> Result, Error> + where + T: DecodeRow, + { let pool = Self::acquire_reader().await?.pool(); Self::before_query(query).await?; @@ -749,9 +750,10 @@ pub trait Schema: 'static + Send + Sync + ModelHooks { /// Finds one model selected by the query in the table, /// and decodes it as an instance of type `T`. - async fn find_one>( - query: &Query, - ) -> Result, Error> { + async fn find_one(query: &Query) -> Result, Error> + where + T: DecodeRow, + { let pool = Self::acquire_reader().await?.pool(); Self::before_query(query).await?; @@ -967,11 +969,15 @@ pub trait Schema: 'static + Send + Sync + ModelHooks { /// Performs a left outer join to another table to filter rows in the joined table, /// and decodes it as `Vec`. - async fn lookup>( + async fn lookup( query: &Query, - left_columns: &[&str], - right_columns: &[&str], - ) -> Result, Error> { + left_columns: [&str; N], + right_columns: [&str; N], + ) -> Result, Error> + where + M: Schema, + T: DecodeRow, + { let pool = Self::acquire_reader().await?.pool(); Self::before_query(query).await?; @@ -1016,12 +1022,16 @@ pub trait Schema: 'static + Send + Sync + ModelHooks { /// Performs a left outer join to another table to filter rows in the "joined" table, /// and parses it as `Vec`. - async fn lookup_as( + async fn lookup_as( query: &Query, - left_columns: &[&str], - right_columns: &[&str], - ) -> Result, Error> { - let mut data = Self::lookup::(query, left_columns, right_columns).await?; + left_columns: [&str; N], + right_columns: [&str; N], + ) -> Result, Error> + where + M: Schema, + T: DeserializeOwned, + { + let mut data = Self::lookup::(query, left_columns, right_columns).await?; let translate_enabled = query.translate_enabled(); for model in data.iter_mut() { Self::after_decode(model).await?; @@ -1070,10 +1080,10 @@ pub trait Schema: 'static + Send + Sync + ModelHooks { /// Counts the number of rows selected by the query in the table. /// The boolean value determines whether it only counts distinct values or not. - async fn count_many>( - query: &Query, - columns: &[(&str, bool)], - ) -> Result { + async fn count_many(query: &Query, columns: &[(&str, bool)]) -> Result + where + T: DecodeRow, + { let pool = Self::acquire_reader().await?.pool(); Self::before_count(query).await?; @@ -1136,10 +1146,10 @@ pub trait Schema: 'static + Send + Sync + ModelHooks { } /// Executes the query in the table, and decodes it as `Vec`. - async fn query>( - query: &str, - params: Option<&Map>, - ) -> Result, Error> { + async fn query(query: &str, params: Option<&Map>) -> Result, Error> + where + T: DecodeRow, + { let pool = Self::acquire_reader().await?.pool(); let (sql, values) = Query::prepare_query(query, params); @@ -1174,10 +1184,10 @@ pub trait Schema: 'static + Send + Sync + ModelHooks { } /// Executes the query in the table, and decodes it as an instance of type `T`. - async fn query_one>( - query: &str, - params: Option<&Map>, - ) -> Result, Error> { + async fn query_one(query: &str, params: Option<&Map>) -> Result, Error> + where + T: DecodeRow, + { let pool = Self::acquire_reader().await?.pool(); let (sql, values) = Query::prepare_query(query, params); @@ -1253,9 +1263,10 @@ pub trait Schema: 'static + Send + Sync + ModelHooks { /// Finds a model selected by the primary key in the table, /// and decodes it as an instance of type `T`. - async fn find_by_id>( - primary_key: &Self::PrimaryKey, - ) -> Result, Error> { + async fn find_by_id(primary_key: &Self::PrimaryKey) -> Result, Error> + where + T: DecodeRow, + { let pool = Self::acquire_reader().await?.pool(); let primary_key_name = Self::PRIMARY_KEY_NAME; diff --git a/zino-core/src/orm/transaction.rs b/zino-core/src/orm/transaction.rs index 2c8a3176..d1b3cf85 100644 --- a/zino-core/src/orm/transaction.rs +++ b/zino-core/src/orm/transaction.rs @@ -16,10 +16,10 @@ where } #[cfg(feature = "orm-sqlx")] -impl<'c, K, M> Transaction> for M +impl<'c, M, K> Transaction> for M where - K: Default + Display + PartialEq, M: Schema, + K: Default + Display + PartialEq, { /// Executes the specific operations inside a transaction. /// If the operations return an error, the transaction will be rolled back; diff --git a/zino-core/src/state/config.rs b/zino-core/src/state/config.rs new file mode 100644 index 00000000..a86cad88 --- /dev/null +++ b/zino-core/src/state/config.rs @@ -0,0 +1,35 @@ +use crate::error::Error; +use std::path::Path; +use toml::value::Table; + +/// Fetches the config from a URL. +pub(super) fn fetch_config_url(config_url: &str, env: &str) -> Result { + let res = ureq::get(config_url).query("env", env).call()?; + let config_table = match res.content_type() { + "application/json" => { + let data = res.into_string()?; + serde_json::from_str(&data)? + } + "application/yaml" => { + let data = res.into_string()?; + serde_yaml::from_str(&data)? + } + _ => res.into_string()?.parse()?, + }; + tracing::info!(env, "`{config_url}` fetched"); + Ok(config_table) +} + +/// Reads the config from a local file. +pub(super) fn read_config_file(config_file: &Path, env: &str) -> Result { + let data = std::fs::read_to_string(config_file)?; + let config_table = match config_file.extension().and_then(|s| s.to_str()) { + Some("json") => serde_json::from_str(&data)?, + Some("yaml" | "yml") => serde_yaml::from_str(&data)?, + _ => data.parse()?, + }; + if let Some(file_name) = config_file.file_name().and_then(|s| s.to_str()) { + tracing::info!(env, "`{file_name}` loaded"); + } + Ok(config_table) +} diff --git a/zino-core/src/state/mod.rs b/zino-core/src/state/mod.rs index f0dc63e0..22c29790 100644 --- a/zino-core/src/state/mod.rs +++ b/zino-core/src/state/mod.rs @@ -14,6 +14,7 @@ use std::{ }; use toml::value::Table; +mod config; mod data; mod env; @@ -42,39 +43,31 @@ impl State { } } - /// Loads the config file according to the specific env. + /// Loads the config according to the specific env. + /// + /// It supports the `json`, `yaml` or `toml` format of configuration source data, + /// which can be specified by the environment variable `ZINO_APP_CONFIG_FORMAT`. + /// By default, it reads the config from a local file. If `ZINO_APP_CONFIG_URL` is set, + /// it will fetch the config from the URL instead. pub fn load_config(&mut self) { let env = self.env.as_str(); - let config = if let Ok(config_url) = std::env::var("ZINO_APP_CONFIG_URL") { - match ureq::get(&config_url) - .query("env", env) - .call() - .and_then(|res| res.into_string().map_err(|err| err.into())) - { - Ok(toml_str) => { - tracing::info!(env, "`{config_url}` fetched"); - toml_str.parse().unwrap_or_default() - } - Err(err) => { - tracing::error!("fail to fetch the config url `{config_url}`: {err}"); - Table::new() - } - } + let config_table = if let Ok(config_url) = std::env::var("ZINO_APP_CONFIG_URL") { + config::fetch_config_url(&config_url, env).unwrap_or_else(|err| { + tracing::error!("fail to fetch the config url `{config_url}`: {err}"); + Table::new() + }) } else { - let config_file = application::PROJECT_DIR.join(format!("./config/config.{env}.toml")); - match std::fs::read_to_string(&config_file) { - Ok(toml_str) => { - tracing::info!(env, "`config.{env}.toml` loaded"); - toml_str.parse().unwrap_or_default() - } - Err(err) => { - let config_file = config_file.display(); - tracing::error!("fail to read the config file `{config_file}`: {err}"); - Table::new() - } - } + let format = std::env::var("ZINO_APP_CONFIG_FORMAT") + .map(|s| s.to_ascii_lowercase()) + .unwrap_or_else(|_| "toml".to_owned()); + let config_file = format!("./config/config.{env}.{format}"); + let config_file_path = application::PROJECT_DIR.join(&config_file); + config::read_config_file(&config_file_path, env).unwrap_or_else(|err| { + tracing::error!("fail to read the config file `{config_file}`: {err}"); + Table::new() + }) }; - self.config = config; + self.config = config_table; } /// Set the state data. diff --git a/zino-derive/docs/model.md b/zino-derive/docs/model.md index 4d0c570e..c8af4aed 100644 --- a/zino-derive/docs/model.md +++ b/zino-derive/docs/model.md @@ -6,11 +6,11 @@ Derives the [`Model`](zino_core::model::Model) trait. create a new instance of the column type in `Model::new()`. The function must be callable as `fn() -> T`. -- **`#[schema(read_only)]`**: The `read_only` annotation is used to indicate that +- **`#[schema(read_only)]`**: The `read_only` annotation indicates that the column is read-only and can not be modified after creation. It also can not been seen in the model definition. -- **`#[schema(generated)]`**: The `generated` annotation is used to indicate that +- **`#[schema(generated)]`**: The `generated` annotation indicates that the column value is generated by the backend and do not need any frontend input. The column will not been seen in the model definition. @@ -19,7 +19,10 @@ Derives the [`Model`](zino_core::model::Model) trait. **`created_at`** | **`updated_at`** | **`deleted_at`** | **`is_deleted`** | **`is_locked`** | **`is_archived`** | **`version`** | **`edition`**. -- **`#[schema(auto_initialized)]`**: The `auto_initialized` annotation is used to indicate that +- **`#[schema(auto_initialized)]`**: The `auto_initialized` annotation indicates that the column has an automatically initialized value. Different to the `generated` column, an `auto_initialized` value can be modified after creation. The column will not been seen in the model definition. + +- **`#[schema(inherent)]`**: The `inherent` annotation indicates that + the column value is parsed by an associated function in the model's inherent implementation. diff --git a/zino-derive/src/model.rs b/zino-derive/src/model.rs index 5cd315e8..9ff6c181 100644 --- a/zino-derive/src/model.rs +++ b/zino-derive/src/model.rs @@ -33,6 +33,7 @@ pub(super) fn parse_token_stream(input: DeriveInput) -> TokenStream { { let name = ident.to_string(); let mut enable_setter = true; + let mut is_inherent = false; for attr in field.attrs.iter() { let arguments = parser::parse_schema_attr(attr); for (key, value) in arguments.into_iter() { @@ -58,6 +59,9 @@ pub(super) fn parse_token_stream(input: DeriveInput) -> TokenStream { "read_only" | "generated" | "reserved" => { enable_setter = false; } + "inherent" => { + is_inherent = true; + } _ => (), } } @@ -65,12 +69,23 @@ pub(super) fn parse_token_stream(input: DeriveInput) -> TokenStream { if enable_setter && !RESERVED_FIELDS.contains(&name.as_str()) { let setter = if type_name == "String" { if name == "password" { - quote! { - if let Some(password) = data.parse_string("password") { - use zino_core::orm::ModelHelper; - match Self::encrypt_password(&password) { - Ok(password) => self.password = password, - Err(err) => validation.record_fail("password", err), + if is_inherent { + quote! { + if let Some(password) = data.parse_string("password") { + match Self::encrypt_password(&password) { + Ok(password) => self.password = password, + Err(err) => validation.record_fail("password", err), + } + } + } + } else { + quote! { + if let Some(password) = data.parse_string("password") { + use zino_core::orm::ModelHelper; + match Self::encrypt_password(&password) { + Ok(password) => self.password = password, + Err(err) => validation.record_fail("password", err), + } } } }