From 33ce419dca84ad729e1f80c03793fa435341b5c7 Mon Sep 17 00:00:00 2001 From: photino Date: Wed, 22 Nov 2023 20:59:01 +0800 Subject: [PATCH] Release version 0.16.0 --- examples/actix-app/Cargo.toml | 12 ++-- examples/axum-app/Cargo.toml | 14 +++-- examples/axum-app/src/model/user.rs | 12 +++- examples/dioxus-desktop/Cargo.toml | 12 ++-- zino-cli/Cargo.toml | 4 +- zino-core/Cargo.toml | 32 ++++++++--- zino-core/src/model/column.rs | 7 ++- zino-core/src/model/hook.rs | 14 +++++ zino-core/src/orm/accessor.rs | 43 +++++++++++--- zino-core/src/orm/column.rs | 54 ++++++++++++++---- zino-core/src/orm/schema.rs | 16 ++++-- zino-core/src/validation/mod.rs | 57 ++++++++++--------- .../src/validation/validator/credit_card.rs | 18 ++++++ zino-core/src/validation/validator/mod.rs | 28 +++++++-- .../src/validation/validator/phone_number.rs | 17 ++++++ zino-derive/Cargo.toml | 4 +- zino-derive/docs/model_accessor.md | 8 ++- zino-derive/docs/schema.md | 13 ++++- zino-derive/src/model_accessor.rs | 39 +++++++++++-- zino-dioxus/Cargo.toml | 4 +- zino-extra/Cargo.toml | 4 +- zino-model/Cargo.toml | 14 +++-- zino-model/src/user/mod.rs | 1 + zino/Cargo.toml | 4 +- 24 files changed, 324 insertions(+), 107 deletions(-) create mode 100644 zino-core/src/validation/validator/credit_card.rs create mode 100644 zino-core/src/validation/validator/phone_number.rs diff --git a/examples/actix-app/Cargo.toml b/examples/actix-app/Cargo.toml index 72c668af..786e828a 100644 --- a/examples/actix-app/Cargo.toml +++ b/examples/actix-app/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "actix-app" description = "An example for actix-web integration." -version = "0.4.0" +version = "0.4.1" rust-version = "1.73" edition = "2021" publish = false @@ -11,23 +11,23 @@ actix-web = "4.4.0" tracing = "0.1.40" [dependencies.serde] -version = "1.0.192" +version = "1.0.193" features = ["derive"] [dependencies.zino] path = "../../zino" -version = "0.14.4" +version = "0.15.0" features = ["actix"] [dependencies.zino-core] path = "../../zino-core" -version = "0.15.4" +version = "0.16.0" features = ["orm-postgres", "view-minijinja"] [dependencies.zino-derive] path = "../../zino-derive" -version = "0.12.4" +version = "0.13.0" [dependencies.zino-model] path = "../../zino-model" -version = "0.12.4" +version = "0.13.0" diff --git a/examples/axum-app/Cargo.toml b/examples/axum-app/Cargo.toml index 7c4d8cda..8833d357 100644 --- a/examples/axum-app/Cargo.toml +++ b/examples/axum-app/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "axum-app" description = "An example for axum integration." -version = "0.10.0" +version = "0.10.1" rust-version = "1.73" edition = "2021" publish = false @@ -11,27 +11,29 @@ axum = "0.6.20" tracing = "0.1.40" [dependencies.serde] -version = "1.0.192" +version = "1.0.193" features = ["derive"] [dependencies.zino] path = "../../zino" -version = "0.14.4" +version = "0.15.0" features = ["axum"] [dependencies.zino-core] path = "../../zino-core" -version = "0.15.4" +version = "0.16.0" features = [ "crypto-sm", "orm-mysql", + "validator-email", + "validator-phone-number", "view-tera", ] [dependencies.zino-derive] path = "../../zino-derive" -version = "0.12.4" +version = "0.13.0" [dependencies.zino-model] path = "../../zino-model" -version = "0.12.4" +version = "0.13.0" diff --git a/examples/axum-app/src/model/user.rs b/examples/axum-app/src/model/user.rs index 3af48201..ce0c50ad 100644 --- a/examples/axum-app/src/model/user.rs +++ b/examples/axum-app/src/model/user.rs @@ -26,7 +26,7 @@ pub struct User { name: String, #[schema( auto_initialized, - enum_values = "Active | Inactive | Locked | Deleted", + enum_values = "Active | Inactive | Locked | Deleted | Archived", default_value = "Inactive", index_type = "hash", comment = "User status" @@ -42,12 +42,20 @@ pub struct User { account: String, #[schema(not_null, write_only, comment = "User password")] password: String, + #[schema(format = "phone_number")] mobile: String, #[schema(format = "email")] email: String, #[schema(format = "uri")] avatar: String, - #[schema(snapshot, nonempty, unique_items, comment = "User roles", example = "admin, worker")] + #[schema( + snapshot, + nonempty, + unique_items, + enum_values = "admin | worker | auditor", + comment = "User roles", + example = "admin, worker" + )] roles: Vec, #[schema(unique_items, reference = "Tag", comment = "User tags")] tags: Vec, diff --git a/examples/dioxus-desktop/Cargo.toml b/examples/dioxus-desktop/Cargo.toml index 318a4d77..6606dfe0 100644 --- a/examples/dioxus-desktop/Cargo.toml +++ b/examples/dioxus-desktop/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "dioxus-desktop" description = "An example for Dioxus desktop integration." -version = "0.1.2" +version = "0.1.3" rust-version = "1.73" edition = "2021" publish = false @@ -21,23 +21,23 @@ features = [ ] [dependencies.serde] -version = "1.0.192" +version = "1.0.193" features = ["derive"] [dependencies.zino] path = "../../zino" -version = "0.14.4" +version = "0.15.0" features = ["dioxus-desktop"] [dependencies.zino-core] path = "../../zino-core" -version = "0.15.4" +version = "0.16.0" features = ["orm-sqlite"] [dependencies.zino-model] path = "../../zino-model" -version = "0.12.4" +version = "0.13.0" [dependencies.zino-dioxus] path = "../../zino-dioxus" -version = "0.1.3" +version = "0.1.4" diff --git a/zino-cli/Cargo.toml b/zino-cli/Cargo.toml index db61c6ff..716dbcea 100644 --- a/zino-cli/Cargo.toml +++ b/zino-cli/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "zino-cli" description = "CLI tools for zino." -version = "0.1.2" +version = "0.1.3" rust-version = "1.73" edition = "2021" license = "MIT" @@ -25,4 +25,4 @@ features = ["derive"] [dependencies.zino-core] path = "../zino-core" -version = "0.15.3" +version = "0.16.0" diff --git a/zino-core/Cargo.toml b/zino-core/Cargo.toml index 389bc8e6..86da3e6c 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.15.4" +version = "0.16.0" rust-version = "1.73" edition = "2021" license = "MIT" @@ -95,6 +95,12 @@ all-connectors = [ "connector-postgres", "connector-sqlite", ] +all-validators = [ + "validator-credit-card", + "validator-email", + "validator-phone-number", + "validator-regex", +] auth-oauth2 = ["dep:oauth2"] auth-oidc = ["dep:openidconnect"] auth-totp = ["dep:totp-rs"] @@ -113,6 +119,7 @@ full = [ "all-auth", "all-chatbots", "all-connectors", + "all-validators", "orm", "view", ] @@ -134,8 +141,12 @@ tls-rustls = [ "opendal?/rustls", "reqwest/rustls-tls", "sqlx?/tls-rustls", - "ureq/rustls", ] +validator = [] +validator-credit-card = ["validator", "dep:card-validate"] +validator-email = ["validator"] +validator-phone-number = ["validator", "dep:phonenumber"] +validator-regex = ["validator"] view = ["dep:minijinja"] view-minijinja = ["view", "dep:minijinja"] view-tera = ["view", "dep:tera"] @@ -192,6 +203,10 @@ features = ["std"] version = "0.16.3" optional = true +[dependencies.card-validate] +version = "2.3.0" +optional = true + [dependencies.chrono] version = "0.4.31" features = ["serde"] @@ -223,6 +238,10 @@ features = ["layers-all"] version = "3.4.0" optional = true +[dependencies.phonenumber] +version = "0.3.3" +optional = true + [dependencies.reqwest] version = "0.11.22" default-features = false @@ -235,7 +254,7 @@ features = [ ] [dependencies.serde] -version = "1.0.192" +version = "1.0.193" features = ["derive"] [dependencies.serde_json] @@ -286,9 +305,8 @@ version = "0.9.1" features = ["macros"] [dependencies.ureq] -version = "2.8.0" -default-features = false -features = ["gzip", "json"] +version = "2.9.0" +features = ["json"] [dependencies.utoipa] version = "4.1.0" @@ -300,7 +318,7 @@ features = [ ] [dependencies.uuid] -version = "1.6.0" +version = "1.6.1" features = [ "fast-rng", "serde", diff --git a/zino-core/src/model/column.rs b/zino-core/src/model/column.rs index 1fe6a5ab..22853bae 100644 --- a/zino-core/src/model/column.rs +++ b/zino-core/src/model/column.rs @@ -1,5 +1,8 @@ use super::Reference; -use crate::{extension::{JsonObjectExt, JsonValueExt}, JsonValue, Map}; +use crate::{ + extension::{JsonObjectExt, JsonValueExt}, + JsonValue, Map, +}; use apache_avro::schema::{Name, RecordField, RecordFieldOrder, Schema, UnionSchema}; use serde::Serialize; use std::{borrow::Cow, collections::BTreeMap}; @@ -162,7 +165,7 @@ impl<'a> Column<'a> { true } - /// Returns `true` if the column is the primary key. + /// Returns `true` if the column is a primary key. #[inline] pub fn is_primary_key(&self) -> bool { self.has_attribute("primary_key") diff --git a/zino-core/src/model/hook.rs b/zino-core/src/model/hook.rs index 2f59f5f7..f848e185 100644 --- a/zino-core/src/model/hook.rs +++ b/zino-core/src/model/hook.rs @@ -130,6 +130,20 @@ pub trait ModelHooks: Model { Ok(()) } + /// A hook running before archiving a model in the table. + #[inline] + async fn before_archive(&mut self) -> Result { + self.before_save().await + } + + /// A hook running after archiving a model in the table. + #[inline] + async fn after_archive(ctx: &QueryContext, data: Self::Data) -> Result<(), Error> { + Self::after_save(ctx, data).await?; + ctx.emit_metrics("archive"); + Ok(()) + } + /// A hook running before updating a model in the table. #[inline] async fn before_update(&mut self) -> Result { diff --git a/zino-core/src/orm/accessor.rs b/zino-core/src/orm/accessor.rs index 33383dd7..41141eb8 100644 --- a/zino-core/src/orm/accessor.rs +++ b/zino-core/src/orm/accessor.rs @@ -192,6 +192,12 @@ where self.status().eq_ignore_ascii_case("Deleted") } + /// Returns `true` if the `status` is `Archived`. + #[inline] + fn is_archived(&self) -> bool { + self.status().eq_ignore_ascii_case("Archived") + } + /// Returns `true` if the `description` is nonempty. #[inline] fn has_description(&self) -> bool { @@ -315,7 +321,7 @@ where mutation } - /// Constructs the `Mutation` for a soft delete of the model. + /// Constructs a `Mutation` for logically deleting the model. fn soft_delete_mutation(&self) -> Mutation { let mut mutation = Self::default_mutation(); let mut updates = self.next_edition_updates(); @@ -324,7 +330,7 @@ where mutation } - /// Constructs the `Mutation` for a lock of the model. + /// Constructs a `Mutation` for locking the model. fn lock_mutation(&self) -> Mutation { let mut mutation = Self::default_mutation(); let mut updates = self.next_edition_updates(); @@ -333,6 +339,15 @@ where mutation } + /// Constructs a `Mutation` for archiving the model. + fn archive_mutation(&self) -> Mutation { + let mut mutation = Self::default_mutation(); + let mut updates = self.next_edition_updates(); + updates.upsert("status", "Archived"); + mutation.append_updates(&mut updates); + mutation + } + /// Constructs a default snapshot `Query` for the model. fn default_snapshot_query() -> Query { let mut query = Self::default_query(); @@ -410,6 +425,18 @@ where Ok(()) } + /// Archives a model of the primary key by setting the status as `Archived`. + async fn archive_by_id(id: &K) -> Result<(), Error> { + let mut model = Self::try_get_model(id).await?; + let model_data = model.before_archive().await?; + + let query = model.current_version_query(); + let mut mutation = model.archive_mutation(); + let ctx = Self::update_one(&query, &mut mutation).await?; + Self::after_archive(&ctx, model_data).await?; + Ok(()) + } + /// Updates a model of the primary key using the json object. async fn update_by_id( id: &K, @@ -423,8 +450,8 @@ where && model.version() != version { bail!( - "409 Conflict: there is a version conflict for `{}`", - version + "409 Conflict: there is a version conflict for the model `{}`", + id ); } Self::before_validation(data, extension.as_ref()).await?; @@ -441,10 +468,12 @@ where if !validation.is_success() { return Ok((validation, model)); } - if model.is_locked() { - data.retain(|key, _value| key == "visibility" || key == "status"); - } else if model.is_deleted() { + if model.is_deleted() { data.retain(|key, _value| key == "status"); + } else if model.is_locked() { + data.retain(|key, _value| key == "visibility" || key == "status"); + } else if model.is_archived() { + bail!("403 Forbidden: archived model `{}` can not be modified", id); } model.after_validation(data).await?; diff --git a/zino-core/src/orm/column.rs b/zino-core/src/orm/column.rs index b130144d..bff5eb9c 100644 --- a/zino-core/src/orm/column.rs +++ b/zino-core/src/orm/column.rs @@ -2,17 +2,35 @@ use crate::{ extension::JsonObjectExt, model::{Column, EncodeColumn}, }; +use convert_case::{Case, Casing}; /// Extension trait for [`Column`](crate::model::Column). pub(super) trait ColumnExt { + /// Returns the type annotation. + fn type_annotation(&self) -> &'static str; + /// Returns the field definition. fn field_definition(&self, primary_key_name: &str) -> String; - /// Returns the type annotation. - fn type_annotation(&self) -> &'static str; + /// Returns the constraints. + fn constraints(&self) -> Vec; } impl<'a> ColumnExt for Column<'a> { + fn type_annotation(&self) -> &'static str { + if cfg!(feature = "orm-postgres") { + match self.column_type() { + "UUID" => "::UUID", + "BIGINT" | "BIGSERIAL" => "::BIGINT", + "INT" | "SERIAL" => "::INT", + "SMALLINT" | "SMALLSERIAL" => "::SMALLINT", + _ => "::TEXT", + } + } else { + "" + } + } + fn field_definition(&self, primary_key_name: &str) -> String { let column_name = self .extra() @@ -56,17 +74,29 @@ impl<'a> ColumnExt for Column<'a> { definition } - fn type_annotation(&self) -> &'static str { - if cfg!(feature = "orm-postgres") { - match self.column_type() { - "UUID" => "::UUID", - "BIGINT" | "BIGSERIAL" => "::BIGINT", - "INT" | "SERIAL" => "::INT", - "SMALLINT" | "SMALLSERIAL" => "::SMALLINT", - _ => "::TEXT", + fn constraints(&self) -> Vec { + let mut constraints = Vec::new(); + let extra = self.extra(); + let column_name = self + .extra() + .get_str("column_name") + .unwrap_or_else(|| self.name()); + if extra.contains_key("foreign_key") + && let Some(reference) = self.reference() + { + let parent_table_name = reference.name(); + let parent_column_name = reference.column_name(); + let mut constraint = format!( + "FOREIGN KEY ({column_name}) REFERENCES {parent_table_name}({parent_column_name})" + ); + if let Some(action) = extra.get_str("on_delete") { + constraint = format!("{constraint} ON DELETE {}", action.to_case(Case::Upper)); } - } else { - "" + if let Some(action) = extra.get_str("on_update") { + constraint = format!("{constraint} ON UPDATE {}", action.to_case(Case::Upper)); + } + constraints.push(constraint); } + constraints } } diff --git a/zino-core/src/orm/schema.rs b/zino-core/src/orm/schema.rs index 199e5f74..a251b930 100644 --- a/zino-core/src/orm/schema.rs +++ b/zino-core/src/orm/schema.rs @@ -177,12 +177,20 @@ pub trait Schema: 'static + Send + Sync + ModelHooks { let primary_key_name = Self::PRIMARY_KEY_NAME; let table_name = Self::table_name(); - let columns = Self::columns() + let columns = Self::columns(); + let mut definitions = columns .iter() .map(|col| col.field_definition(primary_key_name)) - .collect::>() - .join(",\n "); - let sql = format!("CREATE TABLE IF NOT EXISTS {table_name} (\n {columns}\n);"); + .collect::>(); + for col in columns { + let mut constraints = col.constraints(); + if !constraints.is_empty() { + definitions.append(&mut constraints); + } + } + + let definitions = definitions.join(",\n "); + let sql = format!("CREATE TABLE IF NOT EXISTS {table_name} (\n {definitions}\n);"); sqlx::query(&sql).execute(pool).await?; Self::after_create_table().await?; Ok(()) diff --git a/zino-core/src/validation/mod.rs b/zino-core/src/validation/mod.rs index 5b49d489..c4ee50a9 100644 --- a/zino-core/src/validation/mod.rs +++ b/zino-core/src/validation/mod.rs @@ -1,6 +1,5 @@ //! Generic validator and common validation rules. use crate::{error::Error, extension::JsonObjectExt, Map, SharedString}; -use regex::Regex; mod validator; @@ -8,11 +7,23 @@ pub use validator::{ AlphabeticValidator, AlphanumericValidator, AsciiAlphabeticValidator, AsciiAlphanumericValidator, AsciiDigitValidator, AsciiHexdigitValidator, AsciiLowercaseValidator, AsciiUppercaseValidator, AsciiValidator, DateTimeValidator, - DateValidator, EmailValidator, HostValidator, HostnameValidator, IpAddrValidator, - Ipv4AddrValidator, Ipv6AddrValidator, LowercaseValidator, NumericValidator, RegexValidator, - TimeValidator, UppercaseValidator, UriValidator, UuidValidator, Validator, + DateValidator, HostValidator, HostnameValidator, IpAddrValidator, Ipv4AddrValidator, + Ipv6AddrValidator, LowercaseValidator, NumericValidator, TimeValidator, UppercaseValidator, + UriValidator, UuidValidator, Validator, }; +#[cfg(feature = "validator-credit-card")] +pub use validator::CreditCardValidator; + +#[cfg(feature = "validator-email")] +pub use validator::EmailValidator; + +#[cfg(feature = "validator-phone-number")] +pub use validator::PhoneNumberValidator; + +#[cfg(feature = "validator-regex")] +pub use validator::RegexValidator; + /// A record of validation results. #[derive(Debug, Default)] pub struct Validation { @@ -48,15 +59,6 @@ impl Validation { } /// Validates the string value with a specific format. - /// - /// # Supported formats - /// - /// **`alphabetic`** | **`alphanumeric`** | **`ascii-alphabetic`** - /// | **`ascii-alphanumeric`** | **`ascii-digit`** | **`ascii-hexdigit`** - /// | **`ascii-lowercase`** | **`ascii-uppercase`** | **`date`** | **`date-time`** - /// | **`email`** | **`host`** | **`hostname`** | **`ip`** | **`ipv4`** | **`ipv6`** - /// | **`lowercase`** | **`numeric`** | **`regex`** | **`time`** | **`uppercase`** - /// | **`uri`** | **`uuid`** pub fn validate_format(&mut self, key: impl Into, value: &str, format: &str) { match format { "alphabetic" => { @@ -104,6 +106,12 @@ impl Validation { self.record_fail(key, err); } } + #[cfg(feature = "validator-credit-card")] + "credit-card" => { + if let Err(err) = CreditCardValidator.validate(value) { + self.record_fail(key, err); + } + } "date" => { if let Err(err) = DateValidator.validate(value) { self.record_fail(key, err); @@ -114,6 +122,7 @@ impl Validation { self.record_fail(key, err); } } + #[cfg(feature = "validator-email")] "email" => { if let Err(err) = EmailValidator.validate(value) { self.record_fail(key, err); @@ -154,6 +163,13 @@ impl Validation { self.record_fail(key, err); } } + #[cfg(feature = "validator-phone-number")] + "phone_number" => { + if let Err(err) = PhoneNumberValidator.validate(value) { + self.record_fail(key, err); + } + } + #[cfg(feature = "validator-regex")] "regex" => { if let Err(err) = RegexValidator.validate(value) { self.record_fail(key, err); @@ -185,21 +201,6 @@ impl Validation { } } - /// Validates the string value with a regex pattern. - pub fn validate_pattern(&mut self, key: impl Into, value: &str, pattern: &str) { - match Regex::new(pattern) { - Ok(re) => { - if !re.is_match(value) { - self.record(key, "invalid value for the pattern"); - } - } - Err(err) => { - tracing::error!("fail to compile the regex: {err}"); - self.record(key, "fail to compile the regex"); - } - } - } - /// Returns true if the validation contains a value for the specified key. #[inline] pub fn contains_key(&self, key: &str) -> bool { diff --git a/zino-core/src/validation/validator/credit_card.rs b/zino-core/src/validation/validator/credit_card.rs new file mode 100644 index 00000000..200fc936 --- /dev/null +++ b/zino-core/src/validation/validator/credit_card.rs @@ -0,0 +1,18 @@ +use super::Validator; +use crate::{bail, error::Error}; + +/// A validator for a credit card number. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct CreditCardValidator; + +impl Validator for CreditCardValidator { + type Error = Error; + + #[inline] + fn validate(&self, data: &str) -> Result<(), Self::Error> { + if card_validate::Validate::from(data).is_err() { + bail!("invalid credit card number"); + } + Ok(()) + } +} diff --git a/zino-core/src/validation/validator/mod.rs b/zino-core/src/validation/validator/mod.rs index da7ec2d1..1d189593 100644 --- a/zino-core/src/validation/validator/mod.rs +++ b/zino-core/src/validation/validator/mod.rs @@ -11,7 +11,6 @@ mod ascii_lowercase; mod ascii_uppercase; mod date; mod date_time; -mod email; mod host; mod hostname; mod ip_addr; @@ -19,12 +18,23 @@ mod ipv4_addr; mod ipv6_addr; mod lowercase; mod numeric; -mod regex; mod time; mod uppercase; mod uri; mod uuid; +#[cfg(feature = "validator-credit-card")] +mod credit_card; + +#[cfg(feature = "validator-email")] +mod email; + +#[cfg(feature = "validator-phone-number")] +mod phone_number; + +#[cfg(feature = "validator-regex")] +mod regex; + pub use alphabetic::AlphabeticValidator; pub use alphanumeric::AlphanumericValidator; pub use ascii::AsciiValidator; @@ -36,7 +46,6 @@ pub use ascii_lowercase::AsciiLowercaseValidator; pub use ascii_uppercase::AsciiUppercaseValidator; pub use date::DateValidator; pub use date_time::DateTimeValidator; -pub use email::EmailValidator; pub use host::HostValidator; pub use hostname::HostnameValidator; pub use ip_addr::IpAddrValidator; @@ -44,12 +53,23 @@ pub use ipv4_addr::Ipv4AddrValidator; pub use ipv6_addr::Ipv6AddrValidator; pub use lowercase::LowercaseValidator; pub use numeric::NumericValidator; -pub use regex::RegexValidator; pub use time::TimeValidator; pub use uppercase::UppercaseValidator; pub use uri::UriValidator; pub use uuid::UuidValidator; +#[cfg(feature = "validator-credit-card")] +pub use credit_card::CreditCardValidator; + +#[cfg(feature = "validator-email")] +pub use email::EmailValidator; + +#[cfg(feature = "validator-phone-number")] +pub use phone_number::PhoneNumberValidator; + +#[cfg(feature = "validator-regex")] +pub use regex::RegexValidator; + /// A generic validator. pub trait Validator { /// The error type. diff --git a/zino-core/src/validation/validator/phone_number.rs b/zino-core/src/validation/validator/phone_number.rs new file mode 100644 index 00000000..9085d4af --- /dev/null +++ b/zino-core/src/validation/validator/phone_number.rs @@ -0,0 +1,17 @@ +use super::Validator; +use phonenumber::{ParseError, PhoneNumber}; +use std::str::FromStr; + +/// A validator for a phone number. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct PhoneNumberValidator; + +impl Validator for PhoneNumberValidator { + type Error = ParseError; + + #[inline] + fn validate(&self, data: &str) -> Result<(), Self::Error> { + PhoneNumber::from_str(data)?; + Ok(()) + } +} diff --git a/zino-derive/Cargo.toml b/zino-derive/Cargo.toml index 4cd6205f..926a6f2d 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.12.4" +version = "0.13.0" rust-version = "1.73" edition = "2021" license = "MIT" @@ -21,5 +21,5 @@ syn = "2.0.39" [dependencies.zino-core] path = "../zino-core" -version = "0.15.4" +version = "0.16.0" features = ["orm"] diff --git a/zino-derive/docs/model_accessor.md b/zino-derive/docs/model_accessor.md index b5f6f865..5cfbac52 100644 --- a/zino-derive/docs/model_accessor.md +++ b/zino-derive/docs/model_accessor.md @@ -38,11 +38,13 @@ Derives the [`ModelAccessor`](zino_core::orm::ModelAccessor) trait. - **`#[schema(format = "format")]`**: The `format` attribute specifies the format for a `String` value. Supported values: **`alphabetic`** | **`alphanumeric`** | **`ascii`** | **`ascii-alphabetic`** | **`ascii-alphanumeric`** | **`ascii-digit`** - | **`ascii-hexdigit`** | **`ascii-lowercase`** | **`ascii-uppercase`** - | **`date`** | **`date-time`** | **`email`** | **`host`** | **`hostname`** - | **`ip`** | **`ipv4`** | **`ipv6`** | **`lowercase`** | **`numeric`** + | **`ascii-hexdigit`** | **`ascii-lowercase`** | **`ascii-uppercase`** | **`credit-card`** + | **`date`** | **`date-time`** | **`email`** | **`host`** | **`hostname`** | **`ip`** + | **`ipv4`** | **`ipv6`** | **`lowercase`** | **`numeric`** | **`phone_number`** | **`regex`** | **`time`** | **`uppercase`** | **`uri`** | **`uuid`**. +- **`#[schema(enum_values = "value1 | value2 | ...")]`**: The `enum_values` attribute specifies + the enumerated values for a `String` or `Vec` value. - **`#[schema(length = N)]`**: The `length` attribute specifies the fixed length for a `String` value. diff --git a/zino-derive/docs/schema.md b/zino-derive/docs/schema.md index c37cd145..c9da9992 100644 --- a/zino-derive/docs/schema.md +++ b/zino-derive/docs/schema.md @@ -70,8 +70,19 @@ Derives the [`Schema`](zino_core::orm::Schema) trait. - **`#[schema(primary_key)]`**: The `primary_key` annotation is used to mark a column as the primary key. +- **`#[schema(foreign_key)]`**: The `foreign_key` annotation is used to + mark a column as the foreign key. + - **`#[schema(read_only)]`**: The `read_only` annotation is used to indicate that the column is read-only and can not be modified after creation. - **`#[schema(write_only)]`**: The `write_only` annotation is used to indicate that - the column is write-only and can not be seen by frontend users. \ No newline at end of file + the column is write-only and can not be seen by frontend users. + +- **`#[schema(on_delete = "action")]`**: The `on_delete` attribute sepcifies + the referential action for a foreign key when the parent table has a `DELETE` operation. + Supported values: **`cascade`** | **`restrict`**. + +- **`#[schema(on_update = "action")]`**: The `on_update` attribute sepcifies + the referential action for a foreign key when the parent table has an `UPDATE` operation. + Supported values: **`cascade`** | **`restrict`**. diff --git a/zino-derive/src/model_accessor.rs b/zino-derive/src/model_accessor.rs index 6e7cb1b2..e84a9aa2 100644 --- a/zino-derive/src/model_accessor.rs +++ b/zino-derive/src/model_accessor.rs @@ -288,11 +288,42 @@ pub(super) fn parse_token_stream(input: DeriveInput) -> TokenStream { } } "format" if type_name == "String" => { - field_constraints.push(quote! { - if !self.#ident.is_empty() { - validation.validate_format(#name, self.#ident.as_str(), #value); + if let Some(value) = value { + field_constraints.push(quote! { + if !self.#ident.is_empty() { + validation.validate_format(#name, self.#ident.as_str(), #value); + } + }); + } + } + "enum_values" => { + if let Some(value) = value { + let values = + value.split('|').map(|s| s.trim()).collect::>(); + if type_name == "String" { + field_constraints.push(quote! { + if !self.#ident.is_empty() { + let values = [#(#values),*]; + let value = self.#ident.as_str(); + if !values.contains(&value) { + let message = format!("the value `{value}` is not allowed"); + validation.record(#name, message); + } + } + }); + } else if type_name == "Vec" { + field_constraints.push(quote! { + let values = [#(#values),*]; + for value in self.#ident.iter() { + if !values.contains(&value.as_str()) { + let message = format!("the value `{value}` is not allowed"); + validation.record(#name, message); + break; + } + } + }); } - }); + } } "length" => { let length = value diff --git a/zino-dioxus/Cargo.toml b/zino-dioxus/Cargo.toml index 6191c0f8..9bc6f844 100644 --- a/zino-dioxus/Cargo.toml +++ b/zino-dioxus/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "zino-dioxus" description = "Dioxus components for zino." -version = "0.1.3" +version = "0.1.4" rust-version = "1.73" edition = "2021" license = "MIT" @@ -21,4 +21,4 @@ smallvec = "1.11.1" [dependencies.zino-core] path = "../zino-core" -version = "0.15.3" +version = "0.16.0" diff --git a/zino-extra/Cargo.toml b/zino-extra/Cargo.toml index 4d77897c..142938fa 100644 --- a/zino-extra/Cargo.toml +++ b/zino-extra/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "zino-extra" description = "Extra utilities for zino." -version = "0.1.3" +version = "0.1.4" edition = "2021" license = "MIT" categories = ["asynchronous", "network-programming", "web-programming"] @@ -40,4 +40,4 @@ optional = true [dependencies.zino-core] path = "../zino-core" -version = "0.15.3" +version = "0.16.0" diff --git a/zino-model/Cargo.toml b/zino-model/Cargo.toml index c654a100..c09f3182 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.12.4" +version = "0.13.0" rust-version = "1.73" edition = "2021" license = "MIT" @@ -34,7 +34,7 @@ sqlx = "0.7.2" tracing = "0.1.40" [dependencies.serde] -version = "1.0.192" +version = "1.0.193" features = ["derive"] [dependencies.strum] @@ -43,9 +43,13 @@ features = ["derive"] [dependencies.zino-core] path = "../zino-core" -version = "0.15.4" -features = ["orm"] +version = "0.16.0" +features = [ + "orm", + "validator-email", + "validator-phone-number", +] [dependencies.zino-derive] path = "../zino-derive" -version = "0.12.4" +version = "0.13.0" diff --git a/zino-model/src/user/mod.rs b/zino-model/src/user/mod.rs index 561674c2..11be33c6 100644 --- a/zino-model/src/user/mod.rs +++ b/zino-model/src/user/mod.rs @@ -73,6 +73,7 @@ pub struct User { email: String, location: String, locale: String, + #[schema(format = "phone_number")] mobile: String, #[schema(snapshot, nonempty, unique_items, index_type = "gin")] roles: Vec, diff --git a/zino/Cargo.toml b/zino/Cargo.toml index 838b2926..0cd5146d 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.14.4" +version = "0.15.0" rust-version = "1.73" edition = "2021" license = "MIT" @@ -175,4 +175,4 @@ optional = true [dependencies.zino-core] path = "../zino-core" -version = "0.15.4" +version = "0.16.0"