From cce64b1f16c99476652e6b034e9adb06aeb8dbf8 Mon Sep 17 00:00:00 2001 From: photino Date: Wed, 25 Oct 2023 16:11:22 +0800 Subject: [PATCH] Release 0.14.4 --- examples/actix-app/Cargo.toml | 8 +- examples/actix-app/src/controller/user.rs | 2 + examples/axum-app/Cargo.toml | 8 +- examples/axum-app/src/controller/user.rs | 2 + examples/dioxus-desktop/Cargo.toml | 6 +- .../dioxus-desktop/src/service/stargazer.rs | 4 +- zino-core/Cargo.toml | 8 +- zino-core/README.md | 26 +++++++ zino-core/src/application/mod.rs | 4 +- zino-core/src/auth/authentication.rs | 4 +- zino-core/src/auth/session_id.rs | 4 +- zino-core/src/auth/user_session.rs | 4 +- .../connector/connector_arrow/arrow_schema.rs | 4 +- zino-core/src/file/mod.rs | 8 +- zino-core/src/helper/query.rs | 4 +- zino-core/src/helper/sql_query.rs | 4 +- zino-core/src/model/query.rs | 12 +-- zino-core/src/openapi/model.rs | 8 +- zino-core/src/openapi/parser.rs | 4 +- zino-core/src/orm/accessor.rs | 8 +- zino-core/src/orm/helper.rs | 8 +- zino-core/src/orm/mutation.rs | 2 +- zino-core/src/orm/mysql.rs | 7 +- zino-core/src/orm/postgres.rs | 7 +- zino-core/src/orm/schema.rs | 77 ++++++++++++------- zino-core/src/orm/sqlite.rs | 10 ++- zino-core/src/request/mod.rs | 37 +++++++-- zino-core/src/response/mod.rs | 15 +++- zino-derive/Cargo.toml | 4 +- zino-derive/src/lib.rs | 54 ++++++++----- zino-derive/src/parser.rs | 4 +- zino-model/Cargo.toml | 6 +- zino-model/src/user/jwt_auth.rs | 8 +- zino/Cargo.toml | 5 +- zino/src/application/actix_cluster.rs | 3 +- zino/src/controller/mod.rs | 17 ---- 36 files changed, 264 insertions(+), 132 deletions(-) diff --git a/examples/actix-app/Cargo.toml b/examples/actix-app/Cargo.toml index 271cb0e8..0767ca8f 100644 --- a/examples/actix-app/Cargo.toml +++ b/examples/actix-app/Cargo.toml @@ -17,12 +17,12 @@ features = ["derive"] [dependencies.zino] path = "../../zino" -version = "0.13.3" +version = "0.13.4" features = ["actix"] [dependencies.zino-core] path = "../../zino-core" -version = "0.14.3" +version = "0.14.4" features = [ "connector", "connector-arrow", @@ -32,8 +32,8 @@ features = [ [dependencies.zino-derive] path = "../../zino-derive" -version = "0.11.3" +version = "0.11.4" [dependencies.zino-model] path = "../../zino-model" -version = "0.11.3" +version = "0.11.4" diff --git a/examples/actix-app/src/controller/user.rs b/examples/actix-app/src/controller/user.rs index 13cb770e..c206951d 100644 --- a/examples/actix-app/src/controller/user.rs +++ b/examples/actix-app/src/controller/user.rs @@ -23,6 +23,8 @@ pub async fn new(mut req: Request) -> Result { "path": req.request_path(), "user_intro": user_intro, }); + let locale = req.new_cookie("locale".into(), "en-US".into(), None); + res.set_cookie(&locale); res.set_code(StatusCode::CREATED); res.set_json_data(data); Ok(res.into()) diff --git a/examples/axum-app/Cargo.toml b/examples/axum-app/Cargo.toml index fe01f187..bf8bd533 100644 --- a/examples/axum-app/Cargo.toml +++ b/examples/axum-app/Cargo.toml @@ -17,12 +17,12 @@ features = ["derive"] [dependencies.zino] path = "../../zino" -version = "0.13.3" +version = "0.13.4" features = ["axum"] [dependencies.zino-core] path = "../../zino-core" -version = "0.14.3" +version = "0.14.4" features = [ "crypto-sm", "orm-mysql", @@ -31,8 +31,8 @@ features = [ [dependencies.zino-derive] path = "../../zino-derive" -version = "0.11.3" +version = "0.11.4" [dependencies.zino-model] path = "../../zino-model" -version = "0.11.3" +version = "0.11.4" diff --git a/examples/axum-app/src/controller/user.rs b/examples/axum-app/src/controller/user.rs index dfa33774..d189d30f 100644 --- a/examples/axum-app/src/controller/user.rs +++ b/examples/axum-app/src/controller/user.rs @@ -23,6 +23,8 @@ pub async fn new(mut req: Request) -> Result { "path": req.request_path(), "user_intro": user_intro, }); + let locale = req.new_cookie("locale".into(), "en-US".into(), None); + res.set_cookie(&locale); res.set_code(StatusCode::CREATED); res.set_json_data(data); Ok(res.into()) diff --git a/examples/dioxus-desktop/Cargo.toml b/examples/dioxus-desktop/Cargo.toml index 6c30a261..ae3807d3 100644 --- a/examples/dioxus-desktop/Cargo.toml +++ b/examples/dioxus-desktop/Cargo.toml @@ -26,14 +26,14 @@ features = ["derive"] [dependencies.zino] path = "../../zino" -version = "0.13.3" +version = "0.13.4" features = ["dioxus-desktop"] [dependencies.zino-core] path = "../../zino-core" -version = "0.14.3" +version = "0.14.4" features = ["orm-sqlite"] [dependencies.zino-model] path = "../../zino-model" -version = "0.11.3" +version = "0.11.4" diff --git a/examples/dioxus-desktop/src/service/stargazer.rs b/examples/dioxus-desktop/src/service/stargazer.rs index 29e2c9ab..54f940b8 100644 --- a/examples/dioxus-desktop/src/service/stargazer.rs +++ b/examples/dioxus-desktop/src/service/stargazer.rs @@ -21,7 +21,9 @@ pub async fn list_stargazers(per_page: u8, page: usize) -> Result, Erro }); let mut data: Vec = App::fetch_json(resource, options.as_object()).await?; for d in data.iter_mut() { - if let Some(user) = d.remove("user") && let Some(mut user) = user.into_map_opt() { + if let Some(user) = d.remove("user") + && let Some(mut user) = user.into_map_opt() + { d.append(&mut user); } } diff --git a/zino-core/Cargo.toml b/zino-core/Cargo.toml index 08ba5a61..cb190c2b 100644 --- a/zino-core/Cargo.toml +++ b/zino-core/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "zino-core" description = "Core types and traits for zino." -version = "0.14.3" +version = "0.14.4" rust-version = "1.73" edition = "2021" license = "MIT" @@ -140,7 +140,7 @@ view-minijinja = ["view", "dep:minijinja"] aes-gcm-siv = "0.11.1" apache-avro = "0.16.0" async-trait = "0.1.74" -base64 = "0.21.4" +base64 = "0.21.5" bytes = "1.5.0" cfg-if = "1.0" convert_case = "0.6.0" @@ -166,7 +166,7 @@ multer = "2.1.0" parking_lot = "0.12.1" rand = "0.8.5" regex = "1.10.2" -reqwest-middleware = "0.2.3" +reqwest-middleware = "0.2.4" reqwest-retry = "0.3.0" reqwest-tracing = "0.4.6" rmp-serde = "1.1.2" @@ -175,7 +175,7 @@ serde_qs = "0.12.0" sha2 = "0.10.8" sysinfo = "0.29.10" task-local-extensions = "0.1.4" -toml = "0.8.2" +toml = "0.8.4" tracing = "0.1.40" tracing-appender = "0.2.2" url = "2.4.1" diff --git a/zino-core/README.md b/zino-core/README.md index f4595b71..41a2537f 100644 --- a/zino-core/README.md +++ b/zino-core/README.md @@ -1,3 +1,29 @@ # zino-core Core types and traits for zino. + +## Feature flags + +The following optional features are available: + +| Name | Description | Default? | +|---------------------|--------------------------------------------------------|----------| +| `accessor` | Enables the data access layer built with [`opendal`]. | No | +| `cache` | Enables the cache services. | No | +| `chatbot` | Enables the chatbot services. | No | +| `connector` | Enables the data source connectors. | No | +| `crypto-sm` | Enables China's Standards of Encryption Algorithms. | No | +| `format` | Enables the support for common file formats. | No | +| `orm` | Enables the ORM for MySQL, PostgreSQL or **SQLite**. | No | +| `runtime-async-std` | Enables the [`async-std`] runtime. | No | +| `runtime-tokio` | Enables the [`tokio`] runtime. | Yes | +| `tls-native` | Enables the [`native-tls`] TLS backend. | No | +| `tls-rustls` | Enables the [`rustls`] TLS backend. | Yes | +| `view` | Enables the HTML template rendering. | No | + +[`zino`]: https://github.com/photino/zino +[`opendal`]: https://crates.io/crates/opendal +[`async-std`]: https://crates.io/crates/async-std +[`tokio`]: https://crates.io/crates/tokio +[`native-tls`]: https://crates.io/crates/native-tls +[`rustls`]: https://crates.io/crates/rustls diff --git a/zino-core/src/application/mod.rs b/zino-core/src/application/mod.rs index ebf7de6e..302b0a60 100644 --- a/zino-core/src/application/mod.rs +++ b/zino-core/src/application/mod.rs @@ -69,7 +69,9 @@ pub trait Application { } else { project_dir.join(dir) }; - if !path.exists() && let Err(err) = fs::create_dir_all(&path) { + if !path.exists() + && let Err(err) = fs::create_dir_all(&path) + { let path = path.display(); tracing::error!("fail to create the directory {path}: {err}"); } diff --git a/zino-core/src/auth/authentication.rs b/zino-core/src/auth/authentication.rs index 35c23cd2..7c269284 100644 --- a/zino-core/src/auth/authentication.rs +++ b/zino-core/src/auth/authentication.rs @@ -262,7 +262,9 @@ impl Authentication { let signature = self.signature(); if signature.is_empty() { validation.record("signature", "should be nonempty"); - } else if let Ok(token) = self.sign_with::(secret_access_key) && token != signature { + } else if let Ok(token) = self.sign_with::(secret_access_key) + && token != signature + { validation.record("signature", "invalid signature"); } validation diff --git a/zino-core/src/auth/session_id.rs b/zino-core/src/auth/session_id.rs index b009aa44..270837a3 100644 --- a/zino-core/src/auth/session_id.rs +++ b/zino-core/src/auth/session_id.rs @@ -81,7 +81,9 @@ impl SessionId { } else { domain.strip_suffix(realm) }; - if let Some(s) = remainder && s.ends_with('.') { + if let Some(s) = remainder + && s.ends_with('.') + { true } else { false diff --git a/zino-core/src/auth/user_session.rs b/zino-core/src/auth/user_session.rs index 54c04fc4..84cfbae8 100644 --- a/zino-core/src/auth/user_session.rs +++ b/zino-core/src/auth/user_session.rs @@ -215,7 +215,9 @@ impl UserSession { } else { role.strip_prefix(r.as_str()) }; - if let Some(s) = remainder && s.starts_with(':') { + if let Some(s) = remainder + && s.starts_with(':') + { return true; } } diff --git a/zino-core/src/connector/connector_arrow/arrow_schema.rs b/zino-core/src/connector/connector_arrow/arrow_schema.rs index 576b63d9..36c12b12 100644 --- a/zino-core/src/connector/connector_arrow/arrow_schema.rs +++ b/zino-core/src/connector/connector_arrow/arrow_schema.rs @@ -40,7 +40,9 @@ impl ArrowSchemaExt for Schema { TomlValue::String(value_type) => parse_arrow_data_type(value_type)?, TomlValue::Array(array) => { let length = array.len(); - if length == 1 && let Some(TomlValue::String(value_type)) = array.first() { + if length == 1 + && let Some(TomlValue::String(value_type)) = array.first() + { let item_data_type = parse_arrow_data_type(value_type)?; let field = Field::new("item", item_data_type, true); DataType::List(Arc::new(field)) diff --git a/zino-core/src/file/mod.rs b/zino-core/src/file/mod.rs index 9f5bbb3f..aeb0fe9e 100644 --- a/zino-core/src/file/mod.rs +++ b/zino-core/src/file/mod.rs @@ -143,7 +143,9 @@ impl NamedFile { pub fn encrypt_with(&mut self, key: impl AsRef<[u8]>) -> Result<(), Error> { let suffix = ".encrypted"; let bytes = crypto::encrypt(self.as_ref(), key.as_ref())?; - if let Some(ref mut file_name) = self.file_name && !file_name.ends_with(suffix) { + if let Some(ref mut file_name) = self.file_name + && !file_name.ends_with(suffix) + { file_name.push_str(suffix); } self.bytes = bytes.into(); @@ -155,7 +157,9 @@ impl NamedFile { pub fn decrypt_with(&mut self, key: impl AsRef<[u8]>) -> Result<(), Error> { let suffix = ".encrypted"; let bytes = crypto::decrypt(self.as_ref(), key.as_ref())?; - if let Some(ref mut file_name) = self.file_name && file_name.ends_with(suffix) { + if let Some(ref mut file_name) = self.file_name + && file_name.ends_with(suffix) + { file_name.truncate(file_name.len() - suffix.len()); } self.bytes = bytes.into(); diff --git a/zino-core/src/helper/query.rs b/zino-core/src/helper/query.rs index 62eb9821..22c672de 100644 --- a/zino-core/src/helper/query.rs +++ b/zino-core/src/helper/query.rs @@ -7,7 +7,9 @@ use std::{borrow::Cow, sync::LazyLock}; /// The interpolation parameter is represented as `${param}`, /// in which `param` can only contain restricted chracters `[a-zA-Z]+[\w\.]*`. pub(crate) fn format_query<'a>(query: &'a str, params: Option<&'a Map>) -> Cow<'a, str> { - if let Some(params) = params && query.contains('$') { + if let Some(params) = params + && query.contains('$') + { INTERPOLATION_PATTERN.replace_all(query, |captures: &Captures| { let key = &captures[1]; params diff --git a/zino-core/src/helper/sql_query.rs b/zino-core/src/helper/sql_query.rs index 5799545c..27e5a944 100644 --- a/zino-core/src/helper/sql_query.rs +++ b/zino-core/src/helper/sql_query.rs @@ -13,7 +13,9 @@ pub(crate) fn prepare_sql_query<'a>( placeholder: char, ) -> (Cow<'a, str>, Vec<&'a JsonValue>) { let sql = super::format_query(query, params); - if let Some(params) = params && sql.contains('#') { + if let Some(params) = params + && sql.contains('#') + { let mut values = Vec::new(); let sql = STATEMENT_PATTERN.replace_all(&sql, |captures: &Captures| { let key = &captures[1]; diff --git a/zino-core/src/model/query.rs b/zino-core/src/model/query.rs index 48a06891..231661f8 100644 --- a/zino-core/src/model/query.rs +++ b/zino-core/src/model/query.rs @@ -97,13 +97,15 @@ impl Query { } "timestamp" | "nonce" | "signature" => (), _ => { - if let Some(value) = value.as_str() && value != "all" { - if key.starts_with('$') && - let Some(expr) = value.strip_prefix('(') + if let Some(value) = value.as_str() + && value != "all" + { + if key.starts_with('$') + && let Some(expr) = value.strip_prefix('(') { filters.upsert(key, Self::parse_logical_query(expr)); - } else if value.starts_with('$') && - let Some((operator, value)) = value.split_once('.') + } else if value.starts_with('$') + && let Some((operator, value)) = value.split_once('.') { filters.upsert(key, Map::from_entry(operator, value)); } else { diff --git a/zino-core/src/openapi/model.rs b/zino-core/src/openapi/model.rs index ddf33054..e3a31c42 100644 --- a/zino-core/src/openapi/model.rs +++ b/zino-core/src/openapi/model.rs @@ -11,7 +11,9 @@ pub(crate) fn translate_model_entry(model: &mut Map, model_name: &str) { && let Some(value) = model.get(field) { let translated_field = [field, "translated"].join("_"); - let translated_value = translation.translate(value).unwrap_or_else(|| value.clone()); + let translated_value = translation + .translate(value) + .unwrap_or_else(|| value.clone()); data.upsert(translated_field, translated_value); } } @@ -25,7 +27,9 @@ static MODEL_TRANSLATIONS: LazyLock> = LazyLock::new( for (model_name, fields) in definitions.iter() { for (field, value) in fields { let translation = value.as_table().map(Translation::with_config); - if let Some(translation) = translation && translation.is_ready() { + if let Some(translation) = translation + && translation.is_ready() + { let model_name = model_name.to_case(Case::Snake); let model_key = format!("{model_name}.{field}").leak() as &'static str; model_translations.insert(model_key, translation); diff --git a/zino-core/src/openapi/parser.rs b/zino-core/src/openapi/parser.rs index ecc2f136..d06db547 100644 --- a/zino-core/src/openapi/parser.rs +++ b/zino-core/src/openapi/parser.rs @@ -362,7 +362,9 @@ fn parse_schema_format(format: &str) -> SchemaFormat { fn parse_path_parameters(path: &str) -> Vec { let mut parameters = Vec::new(); for segment in path.split('/') { - if let Some(part) = segment.strip_prefix('{') && let Some(name) = part.strip_suffix('}') { + if let Some(part) = segment.strip_prefix('{') + && let Some(name) = part.strip_suffix('}') + { let schema_name = name.to_case(Case::Camel); let parameter = ParameterBuilder::new() .name(name) diff --git a/zino-core/src/orm/accessor.rs b/zino-core/src/orm/accessor.rs index 81dd6d23..f7e92117 100644 --- a/zino-core/src/orm/accessor.rs +++ b/zino-core/src/orm/accessor.rs @@ -416,8 +416,12 @@ where Self::before_extract().await?; let mut model = Self::try_get_model(id).await?; - if let Some(version) = data.get_u64("version") && model.version() != version { - return Err(Error::new("409 Conflict: there is a version control conflict")); + if let Some(version) = data.get_u64("version") + && model.version() != version + { + return Err(Error::new( + "409 Conflict: there is a version control conflict", + )); } Self::before_validation(data, extension.as_ref()).await?; diff --git a/zino-core/src/orm/helper.rs b/zino-core/src/orm/helper.rs index ab76beda..358235d2 100644 --- a/zino-core/src/orm/helper.rs +++ b/zino-core/src/orm/helper.rs @@ -24,7 +24,9 @@ where fn encrypt_password(passowrd: &str) -> Result { let key = Self::secret_key(); let passowrd = passowrd.as_bytes(); - if let Ok(bytes) = base64::decode(passowrd) && bytes.len() == 256 { + if let Ok(bytes) = base64::decode(passowrd) + && bytes.len() == 256 + { crypto::encrypt_hashed_password(passowrd, key) } else { crypto::encrypt_raw_password(passowrd, key) @@ -36,7 +38,9 @@ where let key = Self::secret_key(); let passowrd = passowrd.as_bytes(); let encrypted_password = encrypted_password.as_bytes(); - if let Ok(bytes) = base64::decode(passowrd) && bytes.len() == 256 { + if let Ok(bytes) = base64::decode(passowrd) + && bytes.len() == 256 + { crypto::verify_hashed_password(passowrd, encrypted_password, key) } else { crypto::verify_raw_password(passowrd, encrypted_password, key) diff --git a/zino-core/src/orm/mutation.rs b/zino-core/src/orm/mutation.rs index d0ad7ec9..952b780d 100644 --- a/zino-core/src/orm/mutation.rs +++ b/zino-core/src/orm/mutation.rs @@ -55,7 +55,7 @@ impl MutationExt for Mutation { let mutation = format!(r#"{key} = GREATEST({key}, {value})"#); mutations.push(mutation); } - _ => () + _ => (), } if operator.starts_with('$') { set_json_object = false; diff --git a/zino-core/src/orm/mysql.rs b/zino-core/src/orm/mysql.rs index 2260f60d..fd97f6bc 100644 --- a/zino-core/src/orm/mysql.rs +++ b/zino-core/src/orm/mysql.rs @@ -203,7 +203,8 @@ impl<'c> EncodeColumn for Column<'c> { if let Some(values) = value.as_array() && let [min_value, max_value, ..] = values.as_slice() { - let condition = format!(r#"{field} BETWEEN {min_value} AND {max_value}"#); + let condition = + format!(r#"{field} BETWEEN {min_value} AND {max_value}"#); conditions.push(condition); } } else if operator == "json_length" { @@ -222,7 +223,9 @@ impl<'c> EncodeColumn for Column<'c> { return conditions.join(" AND "); } } - } else if let Some(range) = value.as_array() && range.len() == 2 { + } else if let Some(range) = value.as_array() + && range.len() == 2 + { let min_value = self.encode_value(range.first()); let max_value = self.encode_value(range.last()); return format!(r#"{field} >= {min_value} AND {field} < {max_value}"#); diff --git a/zino-core/src/orm/postgres.rs b/zino-core/src/orm/postgres.rs index e621e63e..7187bae4 100644 --- a/zino-core/src/orm/postgres.rs +++ b/zino-core/src/orm/postgres.rs @@ -217,7 +217,8 @@ impl<'c> EncodeColumn for Column<'c> { if let Some(values) = value.as_array() && let [min_value, max_value, ..] = values.as_slice() { - let condition = format!(r#"{field} BETWEEN {min_value} AND {max_value}"#); + let condition = + format!(r#"{field} BETWEEN {min_value} AND {max_value}"#); conditions.push(condition); } } else if operator == "array_length" { @@ -236,7 +237,9 @@ impl<'c> EncodeColumn for Column<'c> { return conditions.join(" AND "); } } - } else if let Some(range) = value.as_array() && range.len() == 2 { + } else if let Some(range) = value.as_array() + && range.len() == 2 + { let min_value = self.encode_value(range.first()); let max_value = self.encode_value(range.last()); return format!(r#"{field} >= {min_value} AND {field} < {max_value}"#); diff --git a/zino-core/src/orm/schema.rs b/zino-core/src/orm/schema.rs index 4c8f2fc4..4340cb36 100644 --- a/zino-core/src/orm/schema.rs +++ b/zino-core/src/orm/schema.rs @@ -214,7 +214,8 @@ pub trait Schema: 'static + Send + Sync + ModelHooks { if let Some(d) = column_opt && let Some(data_type) = d.get_str("data_type").or_else(|| d.get_str("DATA_TYPE")) { - let column_default = d.get_str("column_default") + let column_default = d + .get_str("column_default") .or_else(|| d.get_str("COLUMN_DEFAULT")); let is_not_null = if cfg!(any(feature = "orm-mysql", feature = "orm-postgres")) { d.get_str("is_nullable") @@ -717,7 +718,9 @@ pub trait Schema: 'static + Send + Sync + ModelHooks { let mut rows = sqlx::query(&sql).fetch(pool); let mut data = Vec::new(); let mut max_rows = super::MAX_ROWS.load(Relaxed); - while let Some(row) = rows.try_next().await? && max_rows > 0 { + while let Some(row) = rows.try_next().await? + && max_rows > 0 + { data.push(T::decode_row(&row)?); max_rows -= 1; } @@ -826,7 +829,9 @@ pub trait Schema: 'static + Send + Sync + ModelHooks { let mut rows = sqlx::query(&sql).fetch(pool); let mut data = Vec::new(); let mut max_rows = super::MAX_ROWS.load(Relaxed); - while let Some(row) = rows.try_next().await? && max_rows > 0 { + while let Some(row) = rows.try_next().await? + && max_rows > 0 + { data.push(row.try_get_unchecked(0)?); max_rows -= 1; } @@ -896,18 +901,23 @@ pub trait Schema: 'static + Send + Sync + ModelHooks { for row in data { for col in columns { - if let Some(vec) = row.get_array(col) && !vec.is_empty() { + if let Some(vec) = row.get_array(col) + && !vec.is_empty() + { let populated_field = [col, "populated"].join("_"); - let populated_values = vec.iter().map(|key| { - let populated_value = associations - .iter() - .find_map(|(k, v)| (key == k).then_some(v)); - if let Some(value) = populated_value { - value.clone().into() - } else { - key.clone() - } - }).collect::>(); + let populated_values = vec + .iter() + .map(|key| { + let populated_value = associations + .iter() + .find_map(|(k, v)| (key == k).then_some(v)); + if let Some(value) = populated_value { + value.clone().into() + } else { + key.clone() + } + }) + .collect::>(); row.upsert(populated_field, populated_values); } else if let Some(key) = row.get(col) { let populated_value = associations @@ -977,18 +987,23 @@ pub trait Schema: 'static + Send + Sync + ModelHooks { Self::after_query(&ctx).await?; for col in columns { - if let Some(vec) = data.get_array(col) && !vec.is_empty() { + if let Some(vec) = data.get_array(col) + && !vec.is_empty() + { let populated_field = [col, "populated"].join("_"); - let populated_values = vec.iter().map(|key| { - let populated_value = associations - .iter() - .find_map(|(k, v)| (key == k).then_some(v)); - if let Some(value) = populated_value { - value.clone().into() - } else { - key.clone() - } - }).collect::>(); + let populated_values = vec + .iter() + .map(|key| { + let populated_value = associations + .iter() + .find_map(|(k, v)| (key == k).then_some(v)); + if let Some(value) = populated_value { + value.clone().into() + } else { + key.clone() + } + }) + .collect::>(); data.upsert(populated_field, populated_values); } else if let Some(key) = data.get(col) { let populated_value = associations @@ -1043,7 +1058,9 @@ pub trait Schema: 'static + Send + Sync + ModelHooks { let mut rows = sqlx::query(&sql).fetch(pool); let mut data = Vec::new(); let mut max_rows = super::MAX_ROWS.load(Relaxed); - while let Some(row) = rows.try_next().await? && max_rows > 0 { + while let Some(row) = rows.try_next().await? + && max_rows > 0 + { data.push(T::decode_row(&row)?); max_rows -= 1; } @@ -1175,7 +1192,9 @@ pub trait Schema: 'static + Send + Sync + ModelHooks { let mut rows = query.fetch(pool); let mut data = Vec::new(); let mut max_rows = super::MAX_ROWS.load(Relaxed); - while let Some(row) = rows.try_next().await? && max_rows > 0 { + while let Some(row) = rows.try_next().await? + && max_rows > 0 + { data.push(T::decode_row(&row)?); max_rows -= 1; } @@ -1280,7 +1299,9 @@ pub trait Schema: 'static + Send + Sync + ModelHooks { let mut rows = query.fetch(pool); let mut data = Vec::new(); let mut max_rows = super::MAX_ROWS.load(Relaxed); - while let Some(row) = rows.try_next().await? && max_rows > 0 { + while let Some(row) = rows.try_next().await? + && max_rows > 0 + { data.push(row.try_get_unchecked(0)?); max_rows -= 1; } diff --git a/zino-core/src/orm/sqlite.rs b/zino-core/src/orm/sqlite.rs index 56d54620..0a63aaa1 100644 --- a/zino-core/src/orm/sqlite.rs +++ b/zino-core/src/orm/sqlite.rs @@ -141,7 +141,8 @@ impl<'c> EncodeColumn for Column<'c> { for (key, value) in filter { let key = Query::escape_string(key); let value = self.encode_value(Some(value)); - let condition = format!(r#"json_tree.key = {key} AND json_tree.value = {value}"#); + let condition = + format!(r#"json_tree.key = {key} AND json_tree.value = {value}"#); conditions.push(condition); } if conditions.is_empty() { @@ -188,7 +189,8 @@ impl<'c> EncodeColumn for Column<'c> { if let Some(values) = value.as_array() && let [min_value, max_value, ..] = values.as_slice() { - let condition = format!(r#"{field} BETWEEN {min_value} AND {max_value}"#); + let condition = + format!(r#"{field} BETWEEN {min_value} AND {max_value}"#); conditions.push(condition); } } else if operator == "json_array_length" { @@ -207,7 +209,9 @@ impl<'c> EncodeColumn for Column<'c> { return conditions.join(" AND "); } } - } else if let Some(range) = value.as_array() && range.len() == 2 { + } else if let Some(range) = value.as_array() + && range.len() == 2 + { let min_value = self.encode_value(range.first()); let max_value = self.encode_value(range.last()); return format!(r#"{field} >= {min_value} AND {field} < {max_value}"#); diff --git a/zino-core/src/request/mod.rs b/zino-core/src/request/mod.rs index f3afb800..470d8e5e 100644 --- a/zino-core/src/request/mod.rs +++ b/zino-core/src/request/mod.rs @@ -112,12 +112,16 @@ pub trait RequestContext { ctx.set_session_id(session_id); // Set locale. - let supported_locales = i18n::SUPPORTED_LOCALES.as_slice(); - let locale = self - .get_header("accept-language") - .and_then(|languages| i18n::select_language(languages, supported_locales)) - .unwrap_or(&i18n::DEFAULT_LOCALE); - ctx.set_locale(locale); + if let Some(cookie) = self.get_cookie("locale") { + ctx.set_locale(cookie.value()); + } else { + let supported_locales = i18n::SUPPORTED_LOCALES.as_slice(); + let locale = self + .get_header("accept-language") + .and_then(|languages| i18n::select_language(languages, supported_locales)) + .unwrap_or(&i18n::DEFAULT_LOCALE); + ctx.set_locale(locale); + } ctx } @@ -167,6 +171,19 @@ pub trait RequestContext { cookie_builder.build() } + /// Gets a cookie with the given name. + fn get_cookie(&self, name: &str) -> Option> { + self.get_header("cookie")?.split(';').find_map(|cookie| { + if let Some((key, value)) = cookie.split_once('=') + && key == name + { + Some(Cookie::new(key, value)) + } else { + None + } + }) + } + /// Returns the start time. #[inline] fn start_time(&self) -> Instant { @@ -263,7 +280,9 @@ pub trait RequestContext { /// Gets the query value of the URI by name. fn get_query(&self, name: &str) -> Option<&str> { self.original_uri().query()?.split('&').find_map(|param| { - if let Some((key, value)) = param.split_once('=') && key == name { + if let Some((key, value)) = param.split_once('=') + && key == name + { Some(value) } else { None @@ -731,7 +750,9 @@ pub trait RequestContext { /// Creates a new subscription instance. fn subscription(&self) -> Subscription { let mut subscription = self.parse_query::().unwrap_or_default(); - if subscription.session_id().is_none() && let Some(session_id) = self.session_id() { + if subscription.session_id().is_none() + && let Some(session_id) = self.session_id() + { subscription.set_session_id(Some(session_id)); } subscription diff --git a/zino-core/src/response/mod.rs b/zino-core/src/response/mod.rs index 2da7b307..38b78b08 100644 --- a/zino-core/src/response/mod.rs +++ b/zino-core/src/response/mod.rs @@ -10,6 +10,7 @@ use crate::{ JsonValue, SharedString, Uuid, }; use bytes::Bytes; +use cookie::Cookie; use etag::EntityTag; use http::header::{self, HeaderName, HeaderValue}; use http_body::Full; @@ -392,6 +393,12 @@ impl Response { self.start_time = start_time; } + /// Sends a cookie to the user agent. + #[inline] + pub fn set_cookie(&mut self, cookie: &Cookie<'_>) { + self.insert_header("set-cookie", cookie.to_string()); + } + /// Records a server timing metric entry. #[inline] pub fn record_server_timing( @@ -507,7 +514,9 @@ impl Response { let has_json_data = !self.json_data.is_null(); let bytes_opt = if has_bytes_data { Some(self.bytes_data.clone()) - } else if let Some(transformer) = self.data_transformer.as_ref() && has_json_data { + } else if let Some(transformer) = self.data_transformer.as_ref() + && has_json_data + { Some(transformer(&self.json_data)?) } else { None @@ -577,7 +586,9 @@ impl Response { displayed_inline = helper::displayed_inline(content_type); self.set_content_type(content_type.to_string()); } - if let Some(file_name) = file.file_name() && !displayed_inline { + if let Some(file_name) = file.file_name() + && !displayed_inline + { self.insert_header( "content-disposition", format!(r#"attachment; filename="{file_name}""#), diff --git a/zino-derive/Cargo.toml b/zino-derive/Cargo.toml index b6a0f01b..6730043f 100644 --- a/zino-derive/Cargo.toml +++ b/zino-derive/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "zino-derive" description = "Derived traits for zino." -version = "0.11.3" +version = "0.11.4" rust-version = "1.73" edition = "2021" license = "MIT" @@ -21,5 +21,5 @@ syn = "2.0.38" [dependencies.zino-core] path = "../zino-core" -version = "0.14.3" +version = "0.14.4" features = ["orm"] diff --git a/zino-derive/src/lib.rs b/zino-derive/src/lib.rs index bde1969a..c5308d82 100644 --- a/zino-derive/src/lib.rs +++ b/zino-derive/src/lib.rs @@ -76,10 +76,14 @@ pub fn derive_schema(item: TokenStream) -> TokenStream { let mut column_fields = Vec::new(); let mut readonly_fields = Vec::new(); let mut writeonly_fields = Vec::new(); - if let Data::Struct(data) = input.data && let Fields::Named(fields) = data.fields { + if let Data::Struct(data) = input.data + && let Fields::Named(fields) = data.fields + { for field in fields.named.into_iter() { let mut type_name = parser::get_type_name(&field.ty); - if let Some(ident) = field.ident && !type_name.is_empty() { + if let Some(ident) = field.ident + && !type_name.is_empty() + { let mut ignore = false; let mut name = ident.to_string(); let mut not_null = false; @@ -133,10 +137,10 @@ pub fn derive_schema(item: TokenStream) -> TokenStream { primary_key_name = name.clone(); } "readonly" => { - readonly_fields.push(quote!{ #name }); + readonly_fields.push(quote! { #name }); } "writeonly" => { - writeonly_fields.push(quote!{ #name }); + writeonly_fields.push(quote! { #name }); } _ => (), } @@ -407,7 +411,9 @@ pub fn derive_model_accessor(item: TokenStream) -> TokenStream { let mut compound_constraints = Vec::new(); for attr in input.attrs.iter() { for (key, value) in parser::parse_schema_attr(attr).into_iter() { - if let Some(value) = value && key == "unique_on" { + if let Some(value) = value + && key == "unique_on" + { let mut fields = Vec::new(); let column_values = value .trim_start_matches('(') @@ -444,11 +450,15 @@ pub fn derive_model_accessor(item: TokenStream) -> TokenStream { let mut primary_key_type = String::from("Uuid"); let mut primary_key_name = String::from("id"); let mut user_id_type = String::new(); - if let Data::Struct(data) = input.data && let Fields::Named(fields) = data.fields { + if let Data::Struct(data) = input.data + && let Fields::Named(fields) = data.fields + { let mut model_references: Vec<(String, Vec)> = Vec::new(); for field in fields.named.into_iter() { let type_name = parser::get_type_name(&field.ty); - if let Some(ident) = field.ident && !type_name.is_empty() { + if let Some(ident) = field.ident + && !type_name.is_empty() + { let name = ident.to_string(); for attr in field.attrs.iter() { let arguments = parser::parse_schema_attr(attr); @@ -496,7 +506,9 @@ pub fn derive_model_accessor(item: TokenStream) -> TokenStream { validation.record(#name, "it is a nonexistent value"); } }); - } else if type_name == "Option" || type_name == "Option" { + } else if type_name == "Option" + || type_name == "Option" + { field_constraints.push(quote! { if let Some(value) = self.#ident { let values = vec![value.to_string()]; @@ -506,7 +518,8 @@ pub fn derive_model_accessor(item: TokenStream) -> TokenStream { } } }); - } else if type_name == "Vec" || type_name == "Vec" { + } else if type_name == "Vec" || type_name == "Vec" + { field_constraints.push(quote! { let values = self.#ident .iter() @@ -647,9 +660,8 @@ pub fn derive_model_accessor(item: TokenStream) -> TokenStream { } } "length" => { - let length: usize = value - .and_then(|s| s.parse().ok()) - .unwrap_or_default(); + let length: usize = + value.and_then(|s| s.parse().ok()).unwrap_or_default(); if type_name == "String" || parser::check_vec_type(&type_name) { field_constraints.push(quote! { let length = #length; @@ -669,9 +681,8 @@ pub fn derive_model_accessor(item: TokenStream) -> TokenStream { } } "max_length" => { - let length: usize = value - .and_then(|s| s.parse().ok()) - .unwrap_or_default(); + let length: usize = + value.and_then(|s| s.parse().ok()).unwrap_or_default(); if type_name == "String" || parser::check_vec_type(&type_name) { field_constraints.push(quote! { let length = #length; @@ -691,9 +702,8 @@ pub fn derive_model_accessor(item: TokenStream) -> TokenStream { } } "min_length" => { - let length: usize = value - .and_then(|s| s.parse().ok()) - .unwrap_or_default(); + let length: usize = + value.and_then(|s| s.parse().ok()).unwrap_or_default(); if type_name == "String" || parser::check_vec_type(&type_name) { field_constraints.push(quote! { let length = #length; @@ -958,10 +968,14 @@ pub fn derive_decode_row(item: TokenStream) -> TokenStream { let mut decode_model_fields = Vec::new(); let mut mysql_decode_model_fields = Vec::new(); let mut postgres_decode_model_fields = Vec::new(); - if let Data::Struct(data) = input.data && let Fields::Named(fields) = data.fields { + if let Data::Struct(data) = input.data + && let Fields::Named(fields) = data.fields + { for field in fields.named.into_iter() { let type_name = parser::get_type_name(&field.ty); - if let Some(ident) = field.ident && !type_name.is_empty() { + if let Some(ident) = field.ident + && !type_name.is_empty() + { let mut ignore = false; 'inner: for attr in field.attrs.iter() { let arguments = parser::parse_schema_attr(attr); diff --git a/zino-derive/src/parser.rs b/zino-derive/src/parser.rs index da33c606..22d54a4c 100644 --- a/zino-derive/src/parser.rs +++ b/zino-derive/src/parser.rs @@ -20,7 +20,9 @@ pub(super) fn check_option_type(type_name: &str) -> bool { /// Returns the type name as a str. pub(super) fn get_type_name(ty: &Type) -> String { - if let Type::Path(ty) = ty && let Some(segment) = ty.path.segments.last() { + if let Type::Path(ty) = ty + && let Some(segment) = ty.path.segments.last() + { let type_name = segment.ident.to_string(); if let PathArguments::AngleBracketed(ref generics) = segment.arguments { if let Some(GenericArgument::Type(ref ty)) = generics.args.first() { diff --git a/zino-model/Cargo.toml b/zino-model/Cargo.toml index fa1e3f9e..00561da4 100644 --- a/zino-model/Cargo.toml +++ b/zino-model/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "zino-model" description = "Domain models for zino." -version = "0.11.3" +version = "0.11.4" rust-version = "1.73" edition = "2021" license = "MIT" @@ -38,9 +38,9 @@ features = ["derive"] [dependencies.zino-core] path = "../zino-core" -version = "0.14.3" +version = "0.14.4" features = ["orm"] [dependencies.zino-derive] path = "../zino-derive" -version = "0.11.3" +version = "0.11.4" diff --git a/zino-model/src/user/jwt_auth.rs b/zino-model/src/user/jwt_auth.rs index 56d628c4..e470e003 100644 --- a/zino-model/src/user/jwt_auth.rs +++ b/zino-model/src/user/jwt_auth.rs @@ -116,7 +116,9 @@ where let mut claims = JwtClaims::new(user_id); let user_id = user_id.parse()?; - if let Some(role_field) = Self::ROLE_FIELD && user.contains_key(role_field) { + if let Some(role_field) = Self::ROLE_FIELD + && user.contains_key(role_field) + { claims.add_data_entry("roles", user.parse_str_array(role_field)); } if let Some(tenant_id_field) = Self::TENANT_ID_FIELD @@ -173,7 +175,9 @@ where Error::new(message) })?; let mut claims = JwtClaims::new(user_id); - if let Some(role_field) = Self::ROLE_FIELD && user.contains_key(role_field) { + if let Some(role_field) = Self::ROLE_FIELD + && user.contains_key(role_field) + { claims.add_data_entry("roles", user.parse_str_array(role_field)); } if let Some(tenant_id_field) = Self::TENANT_ID_FIELD diff --git a/zino/Cargo.toml b/zino/Cargo.toml index e7757ecf..d6a773d4 100644 --- a/zino/Cargo.toml +++ b/zino/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "zino" description = "Next-generation framework for composable applications in Rust." -version = "0.13.3" +version = "0.13.4" rust-version = "1.73" edition = "2021" license = "MIT" @@ -57,7 +57,6 @@ dioxus-web = [ default = ["orm", "view"] orm = ["zino-core/orm"] view = ["zino-core/view"] -export-pdf = ["zino-core/format-pdf"] [dependencies] cfg-if = "1.0" @@ -173,4 +172,4 @@ optional = true [dependencies.zino-core] path = "../zino-core" -version = "0.14.3" +version = "0.14.4" diff --git a/zino/src/application/actix_cluster.rs b/zino/src/application/actix_cluster.rs index 6a1be379..abd0d194 100644 --- a/zino/src/application/actix_cluster.rs +++ b/zino/src/application/actix_cluster.rs @@ -151,7 +151,8 @@ impl Application for ActixCluster { rapidoc = rapidoc.path("/rapidoc"); } if let Some(custom_html) = config.get_str("custom-html") - && let Ok(html) = fs::read_to_string(project_dir.join(custom_html)) + && let Ok(html) = + fs::read_to_string(project_dir.join(custom_html)) { rapidoc = rapidoc.custom_html(html.leak()); } diff --git a/zino/src/controller/mod.rs b/zino/src/controller/mod.rs index 06aaec2e..c94f4145 100644 --- a/zino/src/controller/mod.rs +++ b/zino/src/controller/mod.rs @@ -317,23 +317,6 @@ where "csv" => res.set_csv_response(models), "jsonlines" => res.set_jsonlines_response(models), "msgpack" => res.set_msgpack_response(models), - #[cfg(feature = "export-pdf")] - "pdf" => { - use zino_core::extension::JsonValueExt; - - res.set_json_data(models); - res.set_content_type("application/pdf"); - res.set_data_transformer(|data| { - let model_name = M::model_name(); - let bytes = zino_core::format::PdfDocument::try_new(model_name, None) - .and_then(|mut doc| { - let data = data.as_map_array().unwrap_or_default(); - doc.add_data_table(data, ["name", "visibility", "status", "version"]); - doc.save_to_bytes() - })?; - Ok(bytes.into()) - }); - } _ => res.set_json_response(models), } Ok(res.into())