diff --git a/zino-core/src/model/context.rs b/zino-core/src/model/context.rs index f82bcc10..fc427a55 100644 --- a/zino-core/src/model/context.rs +++ b/zino-core/src/model/context.rs @@ -26,7 +26,7 @@ impl QueryContext { pub fn new() -> Self { Self { start_time: Instant::now(), - query_id: Uuid::new_v4(), + query_id: Uuid::now_v7(), query: String::new(), arguments: Vec::new(), last_insert_id: None, diff --git a/zino-core/src/openapi/mod.rs b/zino-core/src/openapi/mod.rs index 12431611..727811a1 100644 --- a/zino-core/src/openapi/mod.rs +++ b/zino-core/src/openapi/mod.rs @@ -89,7 +89,7 @@ pub(crate) fn default_components() -> Components { let mut components = OPENAPI_COMPONENTS.get_or_init(Components::new).clone(); // Request ID - let request_id_example = Uuid::new_v4(); + let request_id_example = Uuid::now_v7(); let request_id_schema = ObjectBuilder::new() .schema_type(SchemaType::String) .format(Some(SchemaFormat::KnownFormat(KnownFormat::Uuid))) @@ -142,7 +142,7 @@ pub(crate) fn default_components() -> Components { .insert("default".to_owned(), default_response.into()); // Error response - let model_id_example = Uuid::new_v4(); + let model_id_example = Uuid::now_v7(); let detail_example = format!("404 Not Found: cannot find the model `{model_id_example}`"); let instance_example = format!("/model/{model_id_example}/view"); let status_schema = ObjectBuilder::new() diff --git a/zino-core/src/request/mod.rs b/zino-core/src/request/mod.rs index 79006d29..5d8d07d6 100644 --- a/zino-core/src/request/mod.rs +++ b/zino-core/src/request/mod.rs @@ -95,10 +95,10 @@ pub trait RequestContext { let request_id = self .get_header("x-request-id") .and_then(|s| s.parse().ok()) - .unwrap_or_else(Uuid::new_v4); + .unwrap_or_else(Uuid::now_v7); let trace_id = self .get_trace_context() - .map_or_else(Uuid::new_v4, |t| Uuid::from_u128(t.trace_id())); + .map_or_else(Uuid::now_v7, |t| Uuid::from_u128(t.trace_id())); let session_id = self .get_header("x-session-id") .or_else(|| self.get_header("session_id")) diff --git a/zino-core/src/schedule/mod.rs b/zino-core/src/schedule/mod.rs index 5f2fb2fb..700b4cd0 100644 --- a/zino-core/src/schedule/mod.rs +++ b/zino-core/src/schedule/mod.rs @@ -34,7 +34,7 @@ impl Job { let schedule = Schedule::from_str(cron_expr) .unwrap_or_else(|err| panic!("invalid cron expression `{cron_expr}`: {err}")); Job { - id: Uuid::new_v4(), + id: Uuid::now_v7(), data: Map::new(), schedule, run: ExecutableJob::Fn(exec), @@ -48,7 +48,7 @@ impl Job { let schedule = Schedule::from_str(cron_expr) .unwrap_or_else(|err| panic!("invalid cron expression `{cron_expr}`: {err}")); Job { - id: Uuid::new_v4(), + id: Uuid::now_v7(), data: Map::new(), schedule, run: ExecutableJob::AsyncFn(exec), diff --git a/zino-core/src/trace/trace_context.rs b/zino-core/src/trace/trace_context.rs index 97389cc9..92c76374 100644 --- a/zino-core/src/trace/trace_context.rs +++ b/zino-core/src/trace/trace_context.rs @@ -37,7 +37,7 @@ impl TraceContext { Self { span_id, version: 0, - trace_id: Uuid::new_v4().as_u128(), + trace_id: Uuid::now_v7().as_u128(), parent_id: None, trace_flags: FLAG_SAMPLED | FLAG_RANDOM_TRACE_ID, trace_state: TraceState::new(), diff --git a/zino-core/src/validation/mod.rs b/zino-core/src/validation/mod.rs index 19869738..f2397d63 100644 --- a/zino-core/src/validation/mod.rs +++ b/zino-core/src/validation/mod.rs @@ -4,8 +4,9 @@ use crate::{error::Error, extension::JsonObjectExt, Map, SharedString}; mod validator; pub use validator::{ - DateTimeValidator, DateValidator, EmailValidator, Ipv4AddrValidator, Ipv6AddrValidator, - TimeValidator, UriValidator, UuidValidator, Validator, + DateTimeValidator, DateValidator, EmailValidator, HostValidator, HostnameValidator, + IpAddrValidator, Ipv4AddrValidator, Ipv6AddrValidator, TimeValidator, UriValidator, + UuidValidator, Validator, }; /// A record of validation results. @@ -60,6 +61,21 @@ impl Validation { self.record_fail(key, err); } } + "host" => { + if let Err(err) = HostValidator.validate(value) { + self.record_fail(key, err); + } + } + "hostname" => { + if let Err(err) = HostnameValidator.validate(value) { + self.record_fail(key, err); + } + } + "ip" => { + if let Err(err) = IpAddrValidator.validate(value) { + self.record_fail(key, err); + } + } "ipv4" => { if let Err(err) = Ipv4AddrValidator.validate(value) { self.record_fail(key, err); diff --git a/zino-core/src/validation/validator/email.rs b/zino-core/src/validation/validator/email.rs index 458e98dd..3d93d394 100644 --- a/zino-core/src/validation/validator/email.rs +++ b/zino-core/src/validation/validator/email.rs @@ -3,11 +3,11 @@ use super::Validator; use regex::Regex; use std::{fmt, net::IpAddr, str::FromStr, sync::LazyLock}; -/// A validator for email address. +/// A validator for the email address. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct EmailValidator; -/// An error for email address validation. +/// An error for the email address validation. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum InvalidEmail { /// The value is empty. diff --git a/zino-core/src/validation/validator/host.rs b/zino-core/src/validation/validator/host.rs new file mode 100644 index 00000000..415aa133 --- /dev/null +++ b/zino-core/src/validation/validator/host.rs @@ -0,0 +1,49 @@ +use self::InvalidHost::*; +use super::Validator; +use std::{fmt, num::ParseIntError}; +use url::{Host, ParseError}; + +/// A validator for the host of a URL. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct HostValidator; + +/// An error for the host validation. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum InvalidHost { + /// Invalid port. + InvalidPort(ParseIntError), + /// Invalid hostname. + InvalidHostname(ParseError), +} + +impl fmt::Display for InvalidHost { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + InvalidPort(err) => write!(f, "invalid port: {err}"), + InvalidHostname(err) => write!(f, "invalid hostname: {err}"), + } + } +} + +impl std::error::Error for InvalidHost {} + +impl Validator for HostValidator { + type Error = InvalidHost; + + #[inline] + fn validate(&self, data: &str) -> Result<(), Self::Error> { + if let Some((hostname, port)) = data.rsplit_once(':') { + if let Err(err) = port.parse::() { + Err(InvalidPort(err)) + } else if let Err(err) = Host::parse(hostname) { + Err(InvalidHostname(err)) + } else { + Ok(()) + } + } else if let Err(err) = Host::parse(data) { + Err(InvalidHostname(err)) + } else { + Ok(()) + } + } +} diff --git a/zino-core/src/validation/validator/hostname.rs b/zino-core/src/validation/validator/hostname.rs new file mode 100644 index 00000000..2ad6f220 --- /dev/null +++ b/zino-core/src/validation/validator/hostname.rs @@ -0,0 +1,16 @@ +use super::Validator; +use url::{Host, ParseError}; + +/// A validator for the hostname of a URL. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct HostnameValidator; + +impl Validator for HostnameValidator { + type Error = ParseError; + + #[inline] + fn validate(&self, data: &str) -> Result<(), Self::Error> { + Host::parse(data)?; + Ok(()) + } +} diff --git a/zino-core/src/validation/validator/ip_addr.rs b/zino-core/src/validation/validator/ip_addr.rs new file mode 100644 index 00000000..fa140a53 --- /dev/null +++ b/zino-core/src/validation/validator/ip_addr.rs @@ -0,0 +1,19 @@ +use super::Validator; +use std::{ + net::{AddrParseError, IpAddr}, + str::FromStr, +}; + +/// A validator for [`IpAddr`]. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct IpAddrValidator; + +impl Validator for IpAddrValidator { + type Error = AddrParseError; + + #[inline] + fn validate(&self, data: &str) -> Result<(), Self::Error> { + IpAddr::from_str(data)?; + Ok(()) + } +} diff --git a/zino-core/src/validation/validator/mod.rs b/zino-core/src/validation/validator/mod.rs index 75f339a3..38f83a01 100644 --- a/zino-core/src/validation/validator/mod.rs +++ b/zino-core/src/validation/validator/mod.rs @@ -3,6 +3,9 @@ mod date; mod date_time; mod email; +mod host; +mod hostname; +mod ip_addr; mod ipv4_addr; mod ipv6_addr; mod time; @@ -12,6 +15,9 @@ mod uuid; 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; pub use ipv4_addr::Ipv4AddrValidator; pub use ipv6_addr::Ipv6AddrValidator; pub use time::TimeValidator; diff --git a/zino-derive/docs/model_accessor.md b/zino-derive/docs/model_accessor.md index 626edbb1..813b7723 100644 --- a/zino-derive/docs/model_accessor.md +++ b/zino-derive/docs/model_accessor.md @@ -33,7 +33,8 @@ Derives the [`ModelAccessor`](zino_core::orm::ModelAccessor) trait. - **`#[schema(format = "format")]`**: The `format` attribute specifies the format for a `String` value. Supported values: **`date`** | **`date-time`** - | **`email`** | **`ipv4`** | **`ipv6`** | **`time`** | **`uri`** | **`uuid`**. + | **`email`** | **`host`** | **`hostname`** | **`ip`** | **`ipv4`** | **`ipv6`** + | **`time`** | **`uri`** | **`uuid`**. - **`#[schema(length = N)]`**: The `length` attribute specifies diff --git a/zino-derive/docs/schema.md b/zino-derive/docs/schema.md index 5e6e3779..9b7e163b 100644 --- a/zino-derive/docs/schema.md +++ b/zino-derive/docs/schema.md @@ -26,11 +26,14 @@ Derives the [`Schema`](zino_core::orm::Schema) trait. - **`#[schema(ignore)]`**: The `ignore` annotation is used to skip a particular field such that it maps to no database column. +- **`#[schema(type_name = "name")]`**: The `type_name` attribute is used to + override the Rust data type of the column. + - **`#[schema(column_name = "name")]`**: All column names are assumed to be in **snake-case**. You can override the it by specifying the `column_name` attribute. - **`#[schema(column_type = "type")]`**: The column type is derived automatically - from the mappings of Rust primitive data types for different database drivers. + from the mappings of Rust data types for different database drivers. You can override the it by specifying the `column_type` attribute. - **`#[schema(length = N)]`**: The `length` attribute specifies diff --git a/zino-derive/src/lib.rs b/zino-derive/src/lib.rs index 672bef5c..5d798121 100644 --- a/zino-derive/src/lib.rs +++ b/zino-derive/src/lib.rs @@ -20,8 +20,9 @@ pub fn derive_schema(item: TokenStream) -> TokenStream { ]; // Special attributes - const SPECIAL_ATTRIBUTES: [&str; 7] = [ + const SPECIAL_ATTRIBUTES: [&str; 8] = [ "ignore", + "type_name", "not_null", "default_value", "index_type", @@ -83,7 +84,7 @@ pub fn derive_schema(item: TokenStream) -> TokenStream { && let Fields::Named(fields) = data.fields { for field in fields.named.into_iter() { - let type_name = parser::get_type_name(&field.ty); + let mut type_name = parser::get_type_name(&field.ty); if let Some(ident) = field.ident && !type_name.is_empty() { @@ -124,6 +125,11 @@ pub fn derive_schema(item: TokenStream) -> TokenStream { ignore = true; break 'inner; } + "type_name" => { + if let Some(value) = value { + type_name = value; + } + } "column_type" => { column_type = value; } diff --git a/zino-model/src/user/mod.rs b/zino-model/src/user/mod.rs index c96ee297..561674c2 100644 --- a/zino-model/src/user/mod.rs +++ b/zino-model/src/user/mod.rs @@ -44,10 +44,10 @@ pub struct User { #[schema(default_value = "User::model_namespace", index_type = "hash")] namespace: String, #[cfg(feature = "visibility")] - #[schema(column_type = "String", default_value = "UserVisibility::default")] + #[schema(type_name = "String", default_value = "UserVisibility::default")] visibility: UserVisibility, #[schema( - column_type = "String", + type_name = "String", default_value = "UserStatus::default", index_type = "hash" )] diff --git a/zino/src/channel/axum_channel.rs b/zino/src/channel/axum_channel.rs index c52b47e3..4fe1a5f8 100644 --- a/zino/src/channel/axum_channel.rs +++ b/zino/src/channel/axum_channel.rs @@ -63,7 +63,7 @@ impl MessageChannel { /// Creates a new `MessageChannel`. pub fn new() -> Self { let (sender, receiver) = mpsc::channel(CHANNEL_CAPACITY.load(Relaxed)); - let sender_id = Uuid::new_v4(); + let sender_id = Uuid::now_v7(); let subscriber = Subscriber::new(sender, None); let mut senders = CHANNEL_SUBSCRIBERS.write(); senders.retain(|_, subscriber| !subscriber.emitter().is_closed());