From c8fff9a7e0ce1322f211a2280289744407e596f7 Mon Sep 17 00:00:00 2001 From: RA <70325462+RAprogramm@users.noreply.github.com> Date: Wed, 17 Sep 2025 10:34:15 +0700 Subject: [PATCH 01/25] Add custom error derive --- Cargo.lock | 11 +- Cargo.toml | 6 +- masterror-derive/Cargo.toml | 16 + masterror-derive/src/lib.rs | 702 ++++++++++++++++++++++++++++++++++++ src/app_error.rs | 7 +- src/frontend.rs | 3 +- src/kind.rs | 4 +- src/lib.rs | 22 ++ src/turnkey.rs | 4 +- tests/error_derive.rs | 79 ++++ 10 files changed, 842 insertions(+), 12 deletions(-) create mode 100644 masterror-derive/Cargo.toml create mode 100644 masterror-derive/src/lib.rs create mode 100644 tests/error_derive.rs diff --git a/Cargo.lock b/Cargo.lock index 72bb530..aa1a5b2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1498,6 +1498,7 @@ dependencies = [ "config", "http 1.3.1", "js-sys", + "masterror-derive", "redis", "reqwest", "serde", @@ -1506,7 +1507,6 @@ dependencies = [ "sqlx", "telegram-webapp-sdk", "teloxide-core", - "thiserror", "tokio", "tracing", "utoipa", @@ -1514,6 +1514,15 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "masterror-derive" +version = "0.1.0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "matchit" version = "0.8.4" diff --git a/Cargo.toml b/Cargo.toml index 65a33e7..f8c3339 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,6 +12,10 @@ categories = ["rust-patterns", "web-programming"] keywords = ["error", "api", "framework"] +[workspace] +members = ["masterror-derive"] + + [features] default = [] axum = ["dep:axum", "dep:serde_json"] # IntoResponse + JSON body @@ -32,7 +36,7 @@ turnkey = [] openapi = ["dep:utoipa"] [dependencies] -thiserror = "2" +masterror-derive = { path = "masterror-derive" } tracing = "0.1" serde = { version = "1", features = ["derive"] } diff --git a/masterror-derive/Cargo.toml b/masterror-derive/Cargo.toml new file mode 100644 index 0000000..31d1fb5 --- /dev/null +++ b/masterror-derive/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "masterror-derive" +version = "0.1.0" +edition = "2024" +rust-version = "1.89" +description = "Derive macro for masterror" +license = "MIT OR Apache-2.0" +authors = ["masterror maintainers"] + +[lib] +proc-macro = true + +[dependencies] +proc-macro2 = "1" +quote = "1" +syn = { version = "2", features = ["full", "extra-traits"] } diff --git a/masterror-derive/src/lib.rs b/masterror-derive/src/lib.rs new file mode 100644 index 0000000..111b0c2 --- /dev/null +++ b/masterror-derive/src/lib.rs @@ -0,0 +1,702 @@ +#![forbid(unsafe_code)] +#![warn(missing_docs, clippy::all, rust_2018_idioms)] + +//! Derive macro implementing [`std::error::Error`] with `Display` formatting. +//! +//! The macro mirrors the essential functionality relied upon by `masterror` and +//! consumers of the crate: display strings with named or positional fields and +//! a configurable error source via `#[source]` field attributes. + +use std::collections::BTreeSet; + +use proc_macro::TokenStream; +use proc_macro2::{Ident, Span, TokenStream as TokenStream2}; +use quote::{format_ident, quote}; +use syn::{ + Attribute, Data, DataEnum, DataStruct, DeriveInput, Field, Fields, GenericArgument, Generics, + LitStr, Member, Meta, PathArguments, Type, spanned::Spanned +}; + +/// Derive [`std::error::Error`] and [`core::fmt::Display`] for structs and +/// enums. +/// +/// ``` +/// use masterror::Error; +/// +/// #[derive(Debug, Error)] +/// #[error("{code}: {message}")] +/// struct MiniError { +/// code: u16, +/// message: &'static str +/// } +/// +/// let err = MiniError { +/// code: 500, +/// message: "boom" +/// }; +/// assert_eq!(err.to_string(), "500: boom"); +/// assert!(err.source().is_none()); +/// ``` +#[proc_macro_derive(Error, attributes(error, source))] +pub fn derive_error(input: TokenStream) -> TokenStream { + match derive_error_impl(syn::parse_macro_input!(input as DeriveInput)) { + Ok(tokens) => tokens.into(), + Err(err) => err.to_compile_error().into() + } +} + +fn derive_error_impl(input: DeriveInput) -> syn::Result { + let ident = input.ident; + let generics = input.generics; + + let display_impl; + let error_impl; + + match input.data { + Data::Struct(data) => { + let fields = parse_fields(&data)?; + let display_attr = parse_display_attr(&input.attrs)?; + display_impl = build_struct_display(&ident, &generics, &fields, &display_attr)?; + error_impl = build_struct_error(&ident, &generics, &fields)?; + } + Data::Enum(data) => { + let variants = parse_enum(&data)?; + display_impl = build_enum_display(&ident, &generics, &variants)?; + error_impl = build_enum_error(&ident, &generics, &variants)?; + } + Data::Union(_) => { + return Err(syn::Error::new( + ident.span(), + "#[derive(Error)] does not support unions" + )); + } + } + + Ok(quote! { + #display_impl + #error_impl + }) +} + +#[derive(Clone, Copy)] +enum FieldsStyle { + Unit, + Named, + Unnamed +} + +#[derive(Clone)] +struct FieldSpec { + member: Member, + ident: Option, + binding: Ident +} + +#[derive(Clone, Copy)] +enum SourceKind { + Direct { needs_deref: bool }, + Optional { needs_deref: bool } +} + +#[derive(Clone, Copy)] +struct SourceField { + index: usize, + kind: SourceKind +} + +struct ParsedFields { + style: FieldsStyle, + fields: Vec, + source: Option +} + +struct VariantInfo { + ident: Ident, + fields: ParsedFields, + display: LitStr +} + +struct RewriteResult { + literal: LitStr, + positional_indices: BTreeSet +} + +fn parse_fields(data: &DataStruct) -> syn::Result { + parse_fields_internal(&data.fields) +} + +fn parse_enum(data: &DataEnum) -> syn::Result> { + let mut variants = Vec::with_capacity(data.variants.len()); + for variant in &data.variants { + let display = parse_display_attr(&variant.attrs)?; + let fields = parse_fields_internal(&variant.fields)?; + variants.push(VariantInfo { + ident: variant.ident.clone(), + fields, + display + }); + } + Ok(variants) +} + +fn parse_fields_internal(fields: &Fields) -> syn::Result { + match fields { + Fields::Unit => Ok(ParsedFields { + style: FieldsStyle::Unit, + fields: Vec::new(), + source: None + }), + Fields::Named(named) => { + let mut specs = Vec::with_capacity(named.named.len()); + let mut source = None; + for (index, field) in named.named.iter().enumerate() { + let ident = field.ident.clone().ok_or_else(|| { + syn::Error::new(field.span(), "named field missing identifier") + })?; + let member = Member::Named(ident.clone()); + let binding = ident.clone(); + if has_source_attr(field)? { + let kind = detect_source_kind(&field.ty)?; + if source.is_some() { + return Err(syn::Error::new( + field.span(), + "only a single #[source] field is supported" + )); + } + source = Some(SourceField { + index, + kind + }); + } + specs.push(FieldSpec { + member, + ident: Some(ident), + binding + }); + } + Ok(ParsedFields { + style: FieldsStyle::Named, + fields: specs, + source + }) + } + Fields::Unnamed(unnamed) => { + let mut specs = Vec::with_capacity(unnamed.unnamed.len()); + let mut source = None; + for (index, field) in unnamed.unnamed.iter().enumerate() { + let member = Member::Unnamed(index.into()); + let binding = format_ident!("__masterror_{index}"); + if has_source_attr(field)? { + let kind = detect_source_kind(&field.ty)?; + if source.is_some() { + return Err(syn::Error::new( + field.span(), + "only a single #[source] field is supported" + )); + } + source = Some(SourceField { + index, + kind + }); + } + specs.push(FieldSpec { + member, + ident: None, + binding + }); + } + Ok(ParsedFields { + style: FieldsStyle::Unnamed, + fields: specs, + source + }) + } + } +} + +fn parse_display_attr(attrs: &[Attribute]) -> syn::Result { + let mut result = None; + for attr in attrs.iter().filter(|attr| attr.path().is_ident("error")) { + if result.is_some() { + return Err(syn::Error::new( + attr.span(), + "multiple #[error(...)] attributes found" + )); + } + match &attr.meta { + Meta::List(_) => { + let lit: LitStr = attr.parse_args()?; + result = Some(lit); + } + _ => { + return Err(syn::Error::new( + attr.span(), + r#"expected #[error("format")]"# + )); + } + } + } + result + .ok_or_else(|| syn::Error::new(Span::call_site(), r#"missing #[error("...")] attribute"#)) +} + +fn has_source_attr(field: &Field) -> syn::Result { + let mut found = false; + for attr in &field.attrs { + if attr.path().is_ident("source") { + if found { + return Err(syn::Error::new( + attr.span(), + "duplicate #[source] attribute" + )); + } + found = true; + } + } + Ok(found) +} + +fn detect_source_kind(ty: &Type) -> syn::Result { + if let Some(inner) = option_inner_type(ty) { + Ok(SourceKind::Optional { + needs_deref: needs_deref(inner) + }) + } else { + Ok(SourceKind::Direct { + needs_deref: needs_deref(ty) + }) + } +} + +fn option_inner_type(ty: &Type) -> Option<&Type> { + if let Type::Path(type_path) = ty + && type_path.qself.is_none() + && let Some(segment) = type_path.path.segments.last() + && segment.ident == "Option" + && let PathArguments::AngleBracketed(args) = &segment.arguments + && let Some(GenericArgument::Type(inner)) = args.args.first() + { + return Some(inner); + } + None +} + +fn needs_deref(ty: &Type) -> bool { + if let Type::Path(type_path) = ty { + if type_path.qself.is_some() { + return false; + } + if let Some(segment) = type_path.path.segments.last() { + let ident = segment.ident.to_string(); + return matches!(ident.as_str(), "Box" | "Rc" | "Arc"); + } + } + false +} + +fn build_struct_display( + ident: &Ident, + generics: &Generics, + fields: &ParsedFields, + display: &LitStr +) -> syn::Result { + let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); + let RewriteResult { + literal, + positional_indices + } = rewrite_format_string(display, fields.fields.len())?; + + let body = match fields.style { + FieldsStyle::Unit => quote! { + ::core::write!(formatter, #literal) + }, + FieldsStyle::Named => { + let field_idents: Vec<_> = fields + .fields + .iter() + .map(|f| f.ident.clone().expect("named fields must have identifiers")) + .collect(); + let positional_bindings = positional_indices.iter().map(|index| { + let binding_ident = format_ident!("__masterror_{index}"); + let field_ident = field_idents[*index].clone(); + quote! { + #[allow(unused_variables)] + let #binding_ident = &*#field_ident; + } + }); + quote! { + let Self { #( ref #field_idents ),* } = *self; + #[allow(unused_variables)] + let _ = (#(&#field_idents),*); + #(#positional_bindings)* + ::core::write!(formatter, #literal) + } + } + FieldsStyle::Unnamed => { + let bindings: Vec<_> = fields.fields.iter().map(|f| f.binding.clone()).collect(); + quote! { + let Self( #( ref #bindings ),* ) = *self; + #[allow(unused_variables)] + let _ = (#(&#bindings),*); + ::core::write!(formatter, #literal) + } + } + }; + + Ok(quote! { + impl #impl_generics ::core::fmt::Display for #ident #ty_generics #where_clause { + fn fmt(&self, formatter: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result { + #body + } + } + }) +} + +fn build_struct_error( + ident: &Ident, + generics: &Generics, + fields: &ParsedFields +) -> syn::Result { + let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); + let source_expr = if let Some(source) = fields.source { + let field = fields + .fields + .get(source.index) + .ok_or_else(|| syn::Error::new(Span::call_site(), "invalid source field index"))?; + let member = &field.member; + match source.kind { + SourceKind::Direct { + needs_deref: false + } => quote! { + ::core::option::Option::Some(&self.#member as &(dyn ::std::error::Error + 'static)) + }, + SourceKind::Direct { + needs_deref: true + } => quote! { + ::core::option::Option::Some(self.#member.as_ref() as &(dyn ::std::error::Error + 'static)) + }, + SourceKind::Optional { + needs_deref: false + } => quote! { + self.#member + .as_ref() + .map(|source| source as &(dyn ::std::error::Error + 'static)) + }, + SourceKind::Optional { + needs_deref: true + } => quote! { + self.#member + .as_ref() + .map(|source| source.as_ref() as &(dyn ::std::error::Error + 'static)) + } + } + } else { + quote! { ::core::option::Option::None } + }; + + Ok(quote! { + impl #impl_generics ::std::error::Error for #ident #ty_generics #where_clause { + fn source(&self) -> ::core::option::Option<&(dyn ::std::error::Error + 'static)> { + #source_expr + } + } + }) +} + +fn build_enum_display( + ident: &Ident, + generics: &Generics, + variants: &[VariantInfo] +) -> syn::Result { + let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); + let mut arms = Vec::with_capacity(variants.len()); + for variant in variants { + let variant_ident = &variant.ident; + let RewriteResult { + literal, + positional_indices + } = rewrite_format_string(&variant.display, variant.fields.fields.len())?; + let arm = match variant.fields.style { + FieldsStyle::Unit => quote! { + Self::#variant_ident => ::core::write!(formatter, #literal) + }, + FieldsStyle::Named => { + let bindings: Vec<_> = variant + .fields + .fields + .iter() + .map(|f| { + f.ident + .clone() + .expect("named variant field requires identifier") + }) + .collect(); + let positional_bindings = positional_indices.iter().map(|index| { + let binding_ident = format_ident!("__masterror_{index}"); + let field_ident = bindings[*index].clone(); + quote! { + #[allow(unused_variables)] + let #binding_ident = &*#field_ident; + } + }); + quote! { + Self::#variant_ident { #( #bindings ),* } => { + #[allow(unused_variables)] + let _ = (#(&#bindings),*); + #(#positional_bindings)* + ::core::write!(formatter, #literal) + } + } + } + FieldsStyle::Unnamed => { + let bindings: Vec<_> = variant + .fields + .fields + .iter() + .map(|f| f.binding.clone()) + .collect(); + quote! { + Self::#variant_ident( #( #bindings ),* ) => { + #[allow(unused_variables)] + let _ = (#(&#bindings),*); + ::core::write!(formatter, #literal) + } + } + } + }; + arms.push(arm); + } + + Ok(quote! { + impl #impl_generics ::core::fmt::Display for #ident #ty_generics #where_clause { + fn fmt(&self, formatter: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result { + match self { + #(#arms),* + } + } + } + }) +} + +fn build_enum_error( + ident: &Ident, + generics: &Generics, + variants: &[VariantInfo] +) -> syn::Result { + let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); + let mut arms = Vec::with_capacity(variants.len()); + for variant in variants { + let variant_ident = &variant.ident; + let arm = match variant.fields.style { + FieldsStyle::Unit => quote! { + Self::#variant_ident => ::core::option::Option::None + }, + FieldsStyle::Named => { + let bindings: Vec<_> = variant + .fields + .fields + .iter() + .map(|f| { + f.ident + .clone() + .expect("named variant field requires identifier") + }) + .collect(); + let source_expr = if let Some(source) = variant.fields.source { + let binding = bindings[source.index].clone(); + match source.kind { + SourceKind::Direct { + needs_deref: false + } => quote! { + ::core::option::Option::Some(#binding as &(dyn ::std::error::Error + 'static)) + }, + SourceKind::Direct { + needs_deref: true + } => quote! { + ::core::option::Option::Some(#binding.as_ref() as &(dyn ::std::error::Error + 'static)) + }, + SourceKind::Optional { + needs_deref: false + } => quote! { + #binding + .as_ref() + .map(|source| source as &(dyn ::std::error::Error + 'static)) + }, + SourceKind::Optional { + needs_deref: true + } => quote! { + #binding + .as_ref() + .map(|source| source.as_ref() as &(dyn ::std::error::Error + 'static)) + } + } + } else { + quote! { ::core::option::Option::None } + }; + quote! { + Self::#variant_ident { #( #bindings ),* } => { + #source_expr + } + } + } + FieldsStyle::Unnamed => { + let bindings: Vec<_> = variant + .fields + .fields + .iter() + .map(|f| f.binding.clone()) + .collect(); + let source_expr = if let Some(source) = variant.fields.source { + let binding = bindings[source.index].clone(); + match source.kind { + SourceKind::Direct { + needs_deref: false + } => quote! { + ::core::option::Option::Some(#binding as &(dyn ::std::error::Error + 'static)) + }, + SourceKind::Direct { + needs_deref: true + } => quote! { + ::core::option::Option::Some(#binding.as_ref() as &(dyn ::std::error::Error + 'static)) + }, + SourceKind::Optional { + needs_deref: false + } => quote! { + #binding + .as_ref() + .map(|source| source as &(dyn ::std::error::Error + 'static)) + }, + SourceKind::Optional { + needs_deref: true + } => quote! { + #binding + .as_ref() + .map(|source| source.as_ref() as &(dyn ::std::error::Error + 'static)) + } + } + } else { + quote! { ::core::option::Option::None } + }; + quote! { + Self::#variant_ident( #( #bindings ),* ) => { + #source_expr + } + } + } + }; + arms.push(arm); + } + + Ok(quote! { + impl #impl_generics ::std::error::Error for #ident #ty_generics #where_clause { + fn source(&self) -> ::core::option::Option<&(dyn ::std::error::Error + 'static)> { + match self { + #(#arms),* + } + } + } + }) +} + +fn rewrite_format_string(original: &LitStr, field_count: usize) -> syn::Result { + let src = original.value(); + let mut result = String::with_capacity(src.len()); + let mut positional_indices = BTreeSet::new(); + let bytes = src.as_bytes(); + let mut i = 0; + let len = bytes.len(); + let mut next_implicit = 0usize; + + while i < len { + match bytes[i] { + b'{' => { + if i + 1 < len && bytes[i + 1] == b'{' { + result.push_str("{{"); + i += 2; + continue; + } + let start = i + 1; + let mut j = start; + while j < len { + if bytes[j] == b'}' { + break; + } + if bytes[j] == b'{' { + return Err(syn::Error::new( + original.span(), + "nested '{' inside format string is not supported" + )); + } + j += 1; + } + if j == len { + return Err(syn::Error::new( + original.span(), + "unmatched '{' in format string" + )); + } + let content = &src[start..j]; + let (arg, rest) = if let Some(pos) = content.find(':') { + (&content[..pos], Some(&content[pos + 1..])) + } else { + (content, None) + }; + let trimmed = arg.trim(); + let mut used_index = None; + if trimmed.is_empty() { + used_index = Some(next_implicit); + next_implicit += 1; + } else if trimmed.chars().all(|ch| ch.is_ascii_digit()) { + let idx: usize = trimmed.parse().map_err(|_| { + syn::Error::new(original.span(), "invalid positional index") + })?; + used_index = Some(idx); + } + result.push('{'); + if let Some(idx) = used_index { + if idx >= field_count { + return Err(syn::Error::new( + original.span(), + "format index exceeds field count" + )); + } + positional_indices.insert(idx); + let ident = format!("__masterror_{}", idx); + result.push_str(&ident); + } else { + result.push_str(arg); + } + if let Some(rest) = rest { + result.push(':'); + result.push_str(rest); + } + result.push('}'); + i = j + 1; + } + b'}' => { + if i + 1 < len && bytes[i + 1] == b'}' { + result.push_str("}}"); + i += 2; + } else { + return Err(syn::Error::new( + original.span(), + "unmatched '}' in format string" + )); + } + } + _ => { + let start = i; + i += 1; + while i < len && bytes[i] != b'{' && bytes[i] != b'}' { + i += 1; + } + result.push_str(&src[start..i]); + } + } + } + + Ok(RewriteResult { + literal: LitStr::new(&result, original.span()), + positional_indices + }) +} diff --git a/src/app_error.rs b/src/app_error.rs index 9db9fad..260d3b4 100644 --- a/src/app_error.rs +++ b/src/app_error.rs @@ -59,10 +59,9 @@ use std::borrow::Cow; -use thiserror::Error; use tracing::error; -use crate::{RetryAdvice, code::AppCode, kind::AppErrorKind}; +use crate::{Error, RetryAdvice, code::AppCode, kind::AppErrorKind}; /// Thin error wrapper: kind + optional message. /// @@ -347,8 +346,8 @@ mod tests { // AppError's Display is "{kind}", message must not appear. let e = AppError::new(AppErrorKind::Validation, "email invalid"); let shown = format!("{}", e); - // AppErrorKind::Validation Display text is defined on the enum via - // `thiserror::Error`. We only assert that message is not leaked. + // AppErrorKind::Validation Display text is defined on the enum via our + // `#[derive(Error)]`. We only assert that message is not leaked. assert!( !shown.contains("email invalid"), "Display must not include the public message" diff --git a/src/frontend.rs b/src/frontend.rs index 44ac396..403216c 100644 --- a/src/frontend.rs +++ b/src/frontend.rs @@ -37,12 +37,11 @@ use js_sys::{Function, Reflect}; #[cfg(target_arch = "wasm32")] use serde_wasm_bindgen::to_value; -use thiserror::Error; #[cfg(target_arch = "wasm32")] use wasm_bindgen::JsCast; use wasm_bindgen::JsValue; -use crate::{AppError, AppResult, ErrorResponse}; +use crate::{AppError, AppResult, Error, ErrorResponse}; /// Error returned when emitting to the browser console fails or is unsupported. #[derive(Debug, Error, PartialEq, Eq)] diff --git a/src/kind.rs b/src/kind.rs index 55662e9..e025012 100644 --- a/src/kind.rs +++ b/src/kind.rs @@ -38,11 +38,13 @@ #[cfg(feature = "axum")] use axum::http::StatusCode; +use crate::Error; + /// Canonical application error taxonomy. /// /// Keep it small, stable, and framework-agnostic. Each variant has a clear, /// documented meaning and a predictable mapping to an HTTP status code. -#[derive(Debug, thiserror::Error, Clone, Copy, PartialEq, Eq)] +#[derive(Debug, Error, Clone, Copy, PartialEq, Eq)] pub enum AppErrorKind { // ── Generic, client-visible failures (4xx/5xx) ──────────────────────────── /// Resource does not exist or is not visible to the caller. diff --git a/src/lib.rs b/src/lib.rs index 1e0fa0f..6ac640c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -202,4 +202,26 @@ pub mod prelude; pub use app_error::{AppError, AppResult}; pub use code::AppCode; pub use kind::AppErrorKind; +/// Derive macro replicating the ergonomics of `thiserror::Error`. +/// +/// ``` +/// use std::error::Error as StdError; +/// +/// use masterror::Error; +/// +/// #[derive(Debug, Error)] +/// #[error("{code}: {message}")] +/// struct MiniError { +/// code: u16, +/// message: &'static str +/// } +/// +/// let err = MiniError { +/// code: 500, +/// message: "boom" +/// }; +/// assert_eq!(err.to_string(), "500: boom"); +/// assert!(StdError::source(&err).is_none()); +/// ``` +pub use masterror_derive::Error; pub use response::{ErrorResponse, RetryAdvice}; diff --git a/src/turnkey.rs b/src/turnkey.rs index 9cb9573..1173151 100644 --- a/src/turnkey.rs +++ b/src/turnkey.rs @@ -28,9 +28,7 @@ //! assert!(matches!(k, TurnkeyErrorKind::UniqueLabel)); //! ``` -use thiserror::Error; - -use crate::{AppError, AppErrorKind}; +use crate::{AppError, AppErrorKind, Error}; /// High-level, stable Turnkey error categories. /// diff --git a/tests/error_derive.rs b/tests/error_derive.rs new file mode 100644 index 0000000..b1171d0 --- /dev/null +++ b/tests/error_derive.rs @@ -0,0 +1,79 @@ +use std::error::Error as StdError; + +use masterror::Error; + +#[derive(Debug, Error)] +#[error("{kind}: {message}")] +struct NamedError { + kind: &'static str, + message: &'static str, + #[source] + cause: Option +} + +#[derive(Debug, Error)] +#[error("leaf failure")] +struct LeafError; + +#[derive(Debug, Error)] +#[error("{0} -> {1:?}")] +struct TupleError(&'static str, u8); + +#[derive(Debug, Error)] +enum EnumError { + #[error("unit failure")] + Unit, + #[error("{_code}")] + Code { + _code: u16, + #[source] + cause: LeafError + }, + #[error("{0}: {1}")] + Pair(String, #[source] LeafError) +} + +#[test] +fn named_struct_display_and_source() { + let err = NamedError { + kind: "validation", + message: "invalid email", + cause: Some(LeafError) + }; + assert_eq!(err.to_string(), "validation: invalid email"); + let source = StdError::source(&err).expect("source"); + assert_eq!(source.to_string(), "leaf failure"); +} + +#[test] +fn tuple_struct_supports_positional_formatting() { + let err = TupleError("alpha", 42); + assert_eq!(err.to_string(), "alpha -> 42"); + assert!(StdError::source(&err).is_none()); +} + +#[test] +fn enum_variants_forward_source() { + let err = EnumError::Code { + _code: 503, + cause: LeafError + }; + assert_eq!(err.to_string(), "503"); + if let EnumError::Code { + _code, .. + } = &err + { + assert_eq!(*_code, 503); + } else { + panic!("unexpected variant"); + } + assert_eq!(StdError::source(&err).unwrap().to_string(), "leaf failure"); +} + +#[test] +fn tuple_variant_with_source() { + let err = EnumError::Pair("left".into(), LeafError); + let _unit = EnumError::Unit; + assert!(err.to_string().starts_with("left")); + assert_eq!(StdError::source(&err).unwrap().to_string(), "leaf failure"); +} From d59cf4552bd3759a81b20205afdc235c08a69851 Mon Sep 17 00:00:00 2001 From: RA <70325462+RAprogramm@users.noreply.github.com> Date: Wed, 17 Sep 2025 10:52:18 +0700 Subject: [PATCH 02/25] Automate README generation from metadata --- Cargo.lock | 57 +++++- Cargo.toml | 85 ++++++++ README.md | 125 ++++-------- README.template.md | 231 ++++++++++++++++++++++ build.rs | 21 ++ build/readme.rs | 378 ++++++++++++++++++++++++++++++++++++ masterror-derive/src/lib.rs | 2 +- tests/readme_sync.rs | 22 +++ 8 files changed, 833 insertions(+), 88 deletions(-) create mode 100644 README.template.md create mode 100644 build.rs create mode 100644 build/readme.rs create mode 100644 tests/readme_sync.rs diff --git a/Cargo.lock b/Cargo.lock index aa1a5b2..171a391 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -449,7 +449,7 @@ dependencies = [ "serde", "serde-untagged", "serde_json", - "toml", + "toml 0.9.5", "winnow", "yaml-rust2", ] @@ -1508,6 +1508,7 @@ dependencies = [ "telegram-webapp-sdk", "teloxide-core", "tokio", + "toml 0.8.23", "tracing", "utoipa", "validator", @@ -2280,6 +2281,15 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_spanned" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +dependencies = [ + "serde", +] + [[package]] name = "serde_spanned" version = "1.0.0" @@ -2883,6 +2893,18 @@ dependencies = [ "tokio", ] +[[package]] +name = "toml" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" +dependencies = [ + "serde", + "serde_spanned 0.6.9", + "toml_datetime 0.6.11", + "toml_edit", +] + [[package]] name = "toml" version = "0.9.5" @@ -2890,12 +2912,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "75129e1dc5000bfbaa9fee9d1b21f974f9fbad9daec557a521ee6e080825f6e8" dependencies = [ "serde", - "serde_spanned", - "toml_datetime", + "serde_spanned 1.0.0", + "toml_datetime 0.7.0", "toml_parser", "winnow", ] +[[package]] +name = "toml_datetime" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" +dependencies = [ + "serde", +] + [[package]] name = "toml_datetime" version = "0.7.0" @@ -2905,6 +2936,20 @@ dependencies = [ "serde", ] +[[package]] +name = "toml_edit" +version = "0.22.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" +dependencies = [ + "indexmap 2.11.1", + "serde", + "serde_spanned 0.6.9", + "toml_datetime 0.6.11", + "toml_write", + "winnow", +] + [[package]] name = "toml_parser" version = "1.0.2" @@ -2914,6 +2959,12 @@ dependencies = [ "winnow", ] +[[package]] +name = "toml_write" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" + [[package]] name = "tower" version = "0.5.2" diff --git a/Cargo.toml b/Cargo.toml index f8c3339..84afc47 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,6 +8,7 @@ license = "MIT OR Apache-2.0" documentation = "https://docs.rs/masterror" repository = "https://github.com/RAprogramm/masterror" readme = "README.md" +build = "build.rs" categories = ["rust-patterns", "web-programming"] keywords = ["error", "api", "framework"] @@ -76,5 +77,89 @@ tokio = { version = "1", features = [ "time", ], default-features = false } +toml = "0.8" + +[build-dependencies] +serde = { version = "1", features = ["derive"] } +toml = "0.8" + +[package.metadata.masterror.readme] +feature_order = [ + "axum", + "actix", + "openapi", + "serde_json", + "sqlx", + "reqwest", + "redis", + "validator", + "config", + "tokio", + "multipart", + "teloxide", + "telegram-webapp-sdk", + "frontend", + "turnkey", +] +feature_snippet_group = 4 +conversion_lines = [ + "`std::io::Error` → Internal", + "`String` → BadRequest", + "`sqlx::Error` → NotFound/Database", + "`redis::RedisError` → Cache", + "`reqwest::Error` → Timeout/Network/ExternalApi", + "`axum::extract::multipart::MultipartError` → BadRequest", + "`validator::ValidationErrors` → Validation", + "`config::ConfigError` → Config", + "`tokio::time::error::Elapsed` → Timeout", + "`teloxide_core::RequestError` → RateLimited/Network/ExternalApi/Deserialization/Internal", + "`telegram_webapp_sdk::utils::validate_init_data::ValidationError` → TelegramAuth", +] + +[package.metadata.masterror.readme.features.axum] +description = "IntoResponse integration with structured JSON bodies" + +[package.metadata.masterror.readme.features.actix] +description = "Actix Web ResponseError and Responder implementations" + +[package.metadata.masterror.readme.features.openapi] +description = "Generate utoipa OpenAPI schema for ErrorResponse" + +[package.metadata.masterror.readme.features.serde_json] +description = "Attach structured JSON details to AppError" + +[package.metadata.masterror.readme.features.sqlx] +description = "Classify sqlx::Error variants into AppError kinds" + +[package.metadata.masterror.readme.features.redis] +description = "Map redis::RedisError into cache-aware AppError" + +[package.metadata.masterror.readme.features.validator] +description = "Convert validator::ValidationErrors into validation failures" + +[package.metadata.masterror.readme.features.config] +description = "Propagate config::ConfigError as configuration issues" + +[package.metadata.masterror.readme.features.multipart] +description = "Handle axum multipart extraction errors" + +[package.metadata.masterror.readme.features.tokio] +description = "Classify tokio::time::error::Elapsed as timeout" + +[package.metadata.masterror.readme.features.reqwest] +description = "Classify reqwest::Error as timeout/network/external API" + +[package.metadata.masterror.readme.features.teloxide] +description = "Convert teloxide_core::RequestError into domain errors" + +[package.metadata.masterror.readme.features."telegram-webapp-sdk"] +description = "Surface Telegram WebApp validation failures" + +[package.metadata.masterror.readme.features.frontend] +description = "Log to the browser console and convert to JsValue on WASM" + +[package.metadata.masterror.readme.features.turnkey] +description = "Ship Turnkey-specific error taxonomy and conversions" + [lib] crate-type = ["cdylib", "rlib"] diff --git a/README.md b/README.md index a920457..ff14d8f 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ # masterror · Framework-agnostic application error types + + [![Crates.io](https://img.shields.io/crates/v/masterror)](https://crates.io/crates/masterror) [![docs.rs](https://img.shields.io/docsrs/masterror)](https://docs.rs/masterror) [![Downloads](https://img.shields.io/crates/d/masterror)](https://crates.io/crates/masterror) @@ -7,8 +9,8 @@ ![License](https://img.shields.io/badge/License-MIT%20or%20Apache--2.0-informational) [![CI](https://github.com/RAprogramm/masterror/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/RAprogramm/masterror/actions/workflows/ci.yml?query=branch%3Amain) -Small, pragmatic error model for API-heavy Rust services. -Core is framework-agnostic; integrations are opt-in via feature flags. +Small, pragmatic error model for API-heavy Rust services. +Core is framework-agnostic; integrations are opt-in via feature flags. Stable categories, conservative HTTP mapping, no `unsafe`. - Core types: `AppError`, `AppErrorKind`, `AppResult`, `AppCode`, `ErrorResponse` @@ -22,11 +24,13 @@ Stable categories, conservative HTTP mapping, no `unsafe`. ~~~toml [dependencies] -masterror = { version = "0.4", default-features = false } +masterror = { version = "0.4.0", default-features = false } # or with features: -# masterror = { version = "0.4", features = [ -# "axum", "actix", "serde_json", "openapi", -# "sqlx", "reqwest", "redis", "validator", "config", "tokio", "teloxide" +# masterror = { version = "0.4.0", features = [ +# "axum", "actix", "openapi", "serde_json", +# "sqlx", "reqwest", "redis", "validator", +# "config", "tokio", "multipart", "teloxide", +# "telegram-webapp-sdk", "frontend", "turnkey" # ] } ~~~ @@ -54,16 +58,18 @@ masterror = { version = "0.4", default-features = false } ~~~toml [dependencies] # lean core -masterror = { version = "0.4", default-features = false } +masterror = { version = "0.4.0", default-features = false } # with Axum/Actix + JSON + integrations -# masterror = { version = "0.4", features = [ -# "axum", "actix", "serde_json", "openapi", -# "sqlx", "reqwest", "redis", "validator", "config", "tokio", "teloxide" +# masterror = { version = "0.4.0", features = [ +# "axum", "actix", "openapi", "serde_json", +# "sqlx", "reqwest", "redis", "validator", +# "config", "tokio", "multipart", "teloxide", +# "telegram-webapp-sdk", "frontend", "turnkey" # ] } ~~~ -**MSRV:** 1.89 +**MSRV:** 1.89 **No unsafe:** forbidden by crate. @@ -120,64 +126,7 @@ assert_eq!(resp.status, 401); ~~~rust // features = ["axum", "serde_json"] -use masterror::{AppError, AppResult}; -use axum::{routing::get, Router}; - -async fn handler() -> AppResult<&'static str> { - Err(AppError::forbidden("No access")) -} - -let app = Router::new().route("/demo", get(handler)); -~~~ - - - -
- Actix - -~~~rust -// features = ["actix", "serde_json"] -use actix_web::{get, App, HttpServer, Responder}; -use masterror::prelude::*; - -#[get("/err")] -async fn err() -> AppResult<&'static str> { - Err(AppError::forbidden("No access")) -} - -#[get("/payload")] -async fn payload() -> impl Responder { - ErrorResponse::new(422, AppCode::Validation, "Validation failed") - .expect("status") -} -~~~ - -
- - - -
- OpenAPI - -~~~toml -[dependencies] -masterror = { version = "0.4", features = ["openapi", "serde_json"] } -utoipa = "5" -~~~ - -
- -
- Browser (WASM) - -~~~rust -// features = ["frontend"] -use masterror::{AppError, AppErrorKind, AppResult}; -use masterror::frontend::{BrowserConsoleError, BrowserConsoleExt}; - -fn report() -> AppResult<(), BrowserConsoleError> { - let err = AppError::bad_request("missing field"); - let payload = err.to_js_value()?; +... assert!(payload.is_object()); #[cfg(target_arch = "wasm32")] @@ -195,13 +144,21 @@ fn report() -> AppResult<(), BrowserConsoleError> {
Feature flags -- `axum` — IntoResponse -- `actix` — ResponseError/Responder -- `openapi` — utoipa schema -- `serde_json` — JSON details -- `sqlx`, `redis`, `reqwest`, `validator`, `config`, `tokio`, `multipart`, `teloxide`, `telegram-webapp-sdk` -- `frontend` — convert errors into `JsValue` and log via `console.error` (WASM) -- `turnkey` — domain taxonomy and conversions for Turnkey errors +- `axum` — IntoResponse integration with structured JSON bodies +- `actix` — Actix Web ResponseError and Responder implementations +- `openapi` — Generate utoipa OpenAPI schema for ErrorResponse +- `serde_json` — Attach structured JSON details to AppError +- `sqlx` — Classify sqlx::Error variants into AppError kinds +- `reqwest` — Classify reqwest::Error as timeout/network/external API +- `redis` — Map redis::RedisError into cache-aware AppError +- `validator` — Convert validator::ValidationErrors into validation failures +- `config` — Propagate config::ConfigError as configuration issues +- `tokio` — Classify tokio::time::error::Elapsed as timeout +- `multipart` — Handle axum multipart extraction errors +- `teloxide` — Convert teloxide_core::RequestError into domain errors +- `telegram-webapp-sdk` — Surface Telegram WebApp validation failures +- `frontend` — Log to the browser console and convert to JsValue on WASM +- `turnkey` — Ship Turnkey-specific error taxonomy and conversions
@@ -228,13 +185,13 @@ fn report() -> AppResult<(), BrowserConsoleError> { Minimal core: ~~~toml -masterror = { version = "0.4", default-features = false } +masterror = { version = "0.4.0", default-features = false } ~~~ API (Axum + JSON + deps): ~~~toml -masterror = { version = "0.4", features = [ +masterror = { version = "0.4.0", features = [ "axum", "serde_json", "openapi", "sqlx", "reqwest", "redis", "validator", "config", "tokio" ] } @@ -243,7 +200,7 @@ masterror = { version = "0.4", features = [ API (Actix + JSON + deps): ~~~toml -masterror = { version = "0.4", features = [ +masterror = { version = "0.4.0", features = [ "actix", "serde_json", "openapi", "sqlx", "reqwest", "redis", "validator", "config", "tokio" ] } @@ -274,16 +231,16 @@ assert_eq!(app.kind, AppErrorKind::RateLimited);
Migration 0.2 → 0.3 -- Use `ErrorResponse::new(status, AppCode::..., "msg")` instead of legacy +- Use `ErrorResponse::new(status, AppCode::..., "msg")` instead of legacy - New helpers: `.with_retry_after_secs`, `.with_retry_after_duration`, `.with_www_authenticate` -- `ErrorResponse::new_legacy` is temporary shim +- `ErrorResponse::new_legacy` is temporary shim
Versioning & MSRV -Semantic versioning. Breaking API/wire contract → major bump. +Semantic versioning. Breaking API/wire contract → major bump. MSRV = 1.89 (may raise in minor, never in patch).
@@ -291,8 +248,8 @@ MSRV = 1.89 (may raise in minor, never in patch).
Non-goals -- Not a general-purpose error aggregator like `anyhow` -- Not a replacement for your domain errors +- Not a general-purpose error aggregator like `anyhow` +- Not a replacement for your domain errors
diff --git a/README.template.md b/README.template.md new file mode 100644 index 0000000..9342f58 --- /dev/null +++ b/README.template.md @@ -0,0 +1,231 @@ +# masterror · Framework-agnostic application error types + + + +[![Crates.io](https://img.shields.io/crates/v/masterror)](https://crates.io/crates/masterror) +[![docs.rs](https://img.shields.io/docsrs/masterror)](https://docs.rs/masterror) +[![Downloads](https://img.shields.io/crates/d/masterror)](https://crates.io/crates/masterror) +![MSRV](https://img.shields.io/badge/MSRV-{{MSRV}}-blue) +![License](https://img.shields.io/badge/License-MIT%20or%20Apache--2.0-informational) +[![CI](https://github.com/RAprogramm/masterror/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/RAprogramm/masterror/actions/workflows/ci.yml?query=branch%3Amain) + +Small, pragmatic error model for API-heavy Rust services. +Core is framework-agnostic; integrations are opt-in via feature flags. +Stable categories, conservative HTTP mapping, no `unsafe`. + +- Core types: `AppError`, `AppErrorKind`, `AppResult`, `AppCode`, `ErrorResponse` +- Optional Axum/Actix integration +- Optional OpenAPI schema (via `utoipa`) +- Conversions from `sqlx`, `reqwest`, `redis`, `validator`, `config`, `tokio` + +--- + +### TL;DR + +~~~toml +[dependencies] +masterror = { version = "{{CRATE_VERSION}}", default-features = false } +# or with features: +# masterror = { version = "{{CRATE_VERSION}}", features = [ +{{FEATURE_SNIPPET}} +# ] } +~~~ + +*Since v0.4.0: optional `frontend` feature for WASM/browser console logging.* +*Since v0.3.0: stable `AppCode` enum and extended `ErrorResponse` with retry/authentication metadata.* + +--- + +
+ Why this crate? + +- **Stable taxonomy.** Small set of `AppErrorKind` categories mapping conservatively to HTTP. +- **Framework-agnostic.** No assumptions, no `unsafe`, MSRV pinned. +- **Opt-in integrations.** Zero default features; you enable what you need. +- **Clean wire contract.** `ErrorResponse { status, code, message, details?, retry?, www_authenticate? }`. +- **One log at boundary.** Log once with `tracing`. +- **Less boilerplate.** Built-in conversions, compact prelude. +- **Consistent workspace.** Same error surface across crates. + +
+ +
+ Installation + +~~~toml +[dependencies] +# lean core +masterror = { version = "{{CRATE_VERSION}}", default-features = false } + +# with Axum/Actix + JSON + integrations +# masterror = { version = "{{CRATE_VERSION}}", features = [ +{{FEATURE_SNIPPET}} +# ] } +~~~ + +**MSRV:** {{MSRV}} +**No unsafe:** forbidden by crate. + +
+ +
+ Quick start + +Create an error: + +~~~rust +use masterror::{AppError, AppErrorKind}; + +let err = AppError::new(AppErrorKind::BadRequest, "Flag must be set"); +assert!(matches!(err.kind, AppErrorKind::BadRequest)); +~~~ + +With prelude: + +~~~rust +use masterror::prelude::*; + +fn do_work(flag: bool) -> AppResult<()> { + if !flag { + return Err(AppError::bad_request("Flag must be set")); + } + Ok(()) +} +~~~ + +
+ +
+ Error response payload + +~~~rust +use masterror::{AppError, AppErrorKind, AppCode, ErrorResponse}; +use std::time::Duration; + +let app_err = AppError::new(AppErrorKind::Unauthorized, "Token expired"); +let resp: ErrorResponse = (&app_err).into() + .with_retry_after_duration(Duration::from_secs(30)) + .with_www_authenticate(r#"Bearer realm="api", error="invalid_token""#); + +assert_eq!(resp.status, 401); +~~~ + +
+ +
+ Web framework integrations + +
+ Axum + +~~~rust +// features = ["axum", "serde_json"] +... + assert!(payload.is_object()); + + #[cfg(target_arch = "wasm32")] + err.log_to_browser_console()?; + + Ok(()) +} +~~~ + +- On non-WASM targets `log_to_browser_console` returns + `BrowserConsoleError::UnsupportedTarget`. + +
+ +
+ Feature flags + +{{FEATURE_BULLETS}} + +
+ +
+ Conversions + +{{CONVERSION_BULLETS}} + +
+ +
+ Typical setups + +Minimal core: + +~~~toml +masterror = { version = "{{CRATE_VERSION}}", default-features = false } +~~~ + +API (Axum + JSON + deps): + +~~~toml +masterror = { version = "{{CRATE_VERSION}}", features = [ + "axum", "serde_json", "openapi", + "sqlx", "reqwest", "redis", "validator", "config", "tokio" +] } +~~~ + +API (Actix + JSON + deps): + +~~~toml +masterror = { version = "{{CRATE_VERSION}}", features = [ + "actix", "serde_json", "openapi", + "sqlx", "reqwest", "redis", "validator", "config", "tokio" +] } +~~~ + +
+ +
+ Turnkey + +~~~rust +// features = ["turnkey"] +use masterror::turnkey::{classify_turnkey_error, TurnkeyError, TurnkeyErrorKind}; +use masterror::{AppError, AppErrorKind}; + +// Classify a raw SDK/provider error +let kind = classify_turnkey_error("429 Too Many Requests"); +assert!(matches!(kind, TurnkeyErrorKind::RateLimited)); + +// Wrap into AppError +let e = TurnkeyError::new(TurnkeyErrorKind::RateLimited, "throttled upstream"); +let app: AppError = e.into(); +assert_eq!(app.kind, AppErrorKind::RateLimited); +~~~ + +
+ +
+ Migration 0.2 → 0.3 + +- Use `ErrorResponse::new(status, AppCode::..., "msg")` instead of legacy +- New helpers: `.with_retry_after_secs`, `.with_retry_after_duration`, `.with_www_authenticate` +- `ErrorResponse::new_legacy` is temporary shim + +
+ +
+ Versioning & MSRV + +Semantic versioning. Breaking API/wire contract → major bump. +MSRV = {{MSRV}} (may raise in minor, never in patch). + +
+ +
+ Non-goals + +- Not a general-purpose error aggregator like `anyhow` +- Not a replacement for your domain errors + +
+ +
+ License + +Apache-2.0 OR MIT, at your option. + +
diff --git a/build.rs b/build.rs new file mode 100644 index 0000000..aa96760 --- /dev/null +++ b/build.rs @@ -0,0 +1,21 @@ +use std::{env, error::Error, path::PathBuf, process}; + +#[path = "build/readme.rs"] +mod readme; + +fn main() { + if let Err(err) = run() { + eprintln!("error: {err}"); + process::exit(1); + } +} + +fn run() -> Result<(), Box> { + println!("cargo:rerun-if-changed=Cargo.toml"); + println!("cargo:rerun-if-changed=README.template.md"); + println!("cargo:rerun-if-changed=build/readme.rs"); + + let manifest_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR")?); + readme::sync_readme(&manifest_dir)?; + Ok(()) +} diff --git a/build/readme.rs b/build/readme.rs new file mode 100644 index 0000000..607be8d --- /dev/null +++ b/build/readme.rs @@ -0,0 +1,378 @@ +use std::{ + collections::{BTreeMap, BTreeSet}, + fs, io, + path::Path +}; + +use serde::Deserialize; + +/// Error type describing issues while generating the README file. +#[derive(Debug)] +pub enum ReadmeError { + /// Wrapper for IO errors. + Io(io::Error), + /// Wrapper for TOML deserialization errors. + Toml(toml::de::Error), + /// Required metadata section is missing. + MissingMetadata(&'static str), + /// One or more crate features do not have documentation metadata. + MissingFeatureMetadata(Vec), + /// The feature ordering references an unknown feature. + UnknownFeatureInOrder(String), + /// The feature ordering lists the same feature more than once. + DuplicateFeatureInOrder(String), + /// Metadata is defined for features that are not part of the manifest. + UnknownMetadataFeature(Vec), + /// Feature snippet group must be greater than zero. + InvalidSnippetGroup, + /// Placeholder in the template was not substituted. + UnresolvedPlaceholder(String) +} + +impl std::fmt::Display for ReadmeError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Io(err) => write!(f, "IO error: {err}"), + Self::Toml(err) => write!(f, "Failed to parse Cargo.toml: {err}"), + Self::MissingMetadata(path) => write!(f, "Missing metadata section {path}"), + Self::MissingFeatureMetadata(features) => { + write!(f, "Missing metadata for features: {}", features.join(", ")) + } + Self::UnknownFeatureInOrder(feature) => { + write!(f, "Feature order references unknown feature '{feature}'") + } + Self::DuplicateFeatureInOrder(feature) => { + write!( + f, + "Feature '{feature}' listed multiple times in feature_order" + ) + } + Self::UnknownMetadataFeature(features) => { + write!( + f, + "Metadata defined for unknown features: {}", + features.join(", ") + ) + } + Self::InvalidSnippetGroup => { + write!(f, "feature_snippet_group must be greater than zero") + } + Self::UnresolvedPlaceholder(name) => { + write!( + f, + "Template placeholder '{{{{{name}}}}}' was not substituted" + ) + } + } + } +} + +impl std::error::Error for ReadmeError {} + +impl From for ReadmeError { + fn from(value: io::Error) -> Self { + Self::Io(value) + } +} + +impl From for ReadmeError { + fn from(value: toml::de::Error) -> Self { + Self::Toml(value) + } +} + +#[derive(Debug, Deserialize)] +struct Manifest { + package: Package, + #[serde(default)] + features: BTreeMap> +} + +#[derive(Debug, Deserialize)] +struct Package { + version: String, + #[serde(rename = "rust-version")] + rust_version: Option, + #[serde(default)] + metadata: Option +} + +#[derive(Debug, Deserialize)] +struct PackageMetadata { + #[serde(default)] + masterror: Option +} + +#[derive(Debug, Deserialize)] +struct MasterrorMetadata { + #[serde(default)] + readme: Option +} + +#[derive(Clone, Debug, Deserialize)] +struct ReadmeMetadata { + #[serde(default)] + feature_order: Vec, + #[serde(default)] + feature_snippet_group: Option, + #[serde(default)] + conversion_lines: Vec, + #[serde(default)] + features: BTreeMap +} + +#[derive(Clone, Debug, Deserialize)] +struct FeatureMetadata { + description: String, + #[serde(default)] + extra: Vec +} + +#[derive(Clone, Debug)] +struct FeatureDoc { + name: String, + description: String, + extra: Vec +} + +/// Generate README.md from Cargo metadata and a template. +/// +/// # Errors +/// +/// Returns an error if Cargo.toml, the template, or metadata are invalid. +/// +/// # Examples +/// +/// ```ignore +/// use std::path::PathBuf; +/// +/// let manifest = PathBuf::from("Cargo.toml"); +/// let template = PathBuf::from("README.template.md"); +/// let readme = build::readme::generate_readme(&manifest, &template)?; +/// ``` +pub fn generate_readme(manifest_path: &Path, template_path: &Path) -> Result { + let manifest_raw = fs::read_to_string(manifest_path)?; + let manifest: Manifest = toml::from_str(&manifest_raw)?; + let Manifest { + package, + features + } = manifest; + let Package { + version, + rust_version, + metadata + } = package; + + let readme_meta = metadata + .and_then(|meta| meta.masterror) + .and_then(|meta| meta.readme) + .ok_or(ReadmeError::MissingMetadata( + "package.metadata.masterror.readme" + ))?; + + let feature_docs = collect_feature_docs(&features, &readme_meta)?; + let snippet_group = readme_meta.feature_snippet_group.unwrap_or(4); + if snippet_group == 0 { + return Err(ReadmeError::InvalidSnippetGroup); + } + + let template_raw = fs::read_to_string(template_path)?; + render_readme( + &template_raw, + &version, + rust_version.as_deref().unwrap_or("unknown"), + &feature_docs, + snippet_group, + &readme_meta.conversion_lines + ) +} + +/// Synchronize README.md on disk with the generated output. +/// +/// # Errors +/// +/// Returns an error if reading or writing files fails or metadata is invalid. +/// +/// # Examples +/// +/// ```ignore +/// use std::path::PathBuf; +/// +/// let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")); +/// build::readme::sync_readme(&manifest_dir)?; +/// ``` +#[cfg_attr(test, allow(dead_code))] +pub fn sync_readme(manifest_dir: &Path) -> Result<(), ReadmeError> { + let manifest_path = manifest_dir.join("Cargo.toml"); + let template_path = manifest_dir.join("README.template.md"); + let output_path = manifest_dir.join("README.md"); + let readme = generate_readme(&manifest_path, &template_path)?; + write_if_changed(&output_path, &readme) +} + +fn collect_feature_docs( + feature_table: &BTreeMap>, + readme_meta: &ReadmeMetadata +) -> Result, ReadmeError> { + let feature_names: BTreeSet = feature_table + .keys() + .filter(|name| name.as_str() != "default") + .cloned() + .collect(); + + let mut missing_docs = Vec::new(); + let mut docs_map = BTreeMap::new(); + for name in &feature_names { + if let Some(meta) = readme_meta.features.get(name) { + docs_map.insert( + name.clone(), + FeatureDoc { + name: name.clone(), + description: meta.description.clone(), + extra: meta.extra.clone() + } + ); + } else { + missing_docs.push(name.clone()); + } + } + + if !missing_docs.is_empty() { + return Err(ReadmeError::MissingFeatureMetadata(missing_docs)); + } + + let unknown_metadata: Vec = readme_meta + .features + .keys() + .filter(|name| name.as_str() != "default" && !feature_names.contains(*name)) + .cloned() + .collect(); + + if !unknown_metadata.is_empty() { + return Err(ReadmeError::UnknownMetadataFeature(unknown_metadata)); + } + + let mut ordered = Vec::new(); + for name in &readme_meta.feature_order { + if name == "default" { + continue; + } + if !feature_names.contains(name) { + return Err(ReadmeError::UnknownFeatureInOrder(name.clone())); + } + if let Some(doc) = docs_map.remove(name) { + ordered.push(doc); + } else { + return Err(ReadmeError::DuplicateFeatureInOrder(name.clone())); + } + } + + ordered.extend(docs_map.into_values()); + Ok(ordered) +} + +fn render_readme( + template: &str, + version: &str, + rust_version: &str, + features: &[FeatureDoc], + snippet_group: usize, + conversions: &[String] +) -> Result { + let feature_bullets = render_feature_bullets(features); + let feature_snippet = render_feature_snippet(features, snippet_group); + let conversion_bullets = render_conversion_bullets(conversions); + + let mut rendered = template.replace("{{CRATE_VERSION}}", version); + rendered = rendered.replace("{{MSRV}}", rust_version); + rendered = rendered.replace("{{FEATURE_BULLETS}}", &feature_bullets); + rendered = rendered.replace("{{FEATURE_SNIPPET}}", &feature_snippet); + rendered = rendered.replace("{{CONVERSION_BULLETS}}", &conversion_bullets); + + if let Some(name) = find_placeholder(&rendered) { + return Err(ReadmeError::UnresolvedPlaceholder(name)); + } + + Ok(rendered) +} + +fn render_feature_bullets(features: &[FeatureDoc]) -> String { + let mut lines = Vec::new(); + for feature in features { + lines.push(format!("- `{}` — {}", feature.name, feature.description)); + if !feature.extra.is_empty() { + for note in &feature.extra { + lines.push(format!(" - {note}")); + } + } + } + lines.join("\n") +} + +fn render_conversion_bullets(conversions: &[String]) -> String { + conversions + .iter() + .map(|entry| format!("- {entry}")) + .collect::>() + .join("\n") +} + +fn render_feature_snippet(features: &[FeatureDoc], group_size: usize) -> String { + if features.is_empty() { + return String::new(); + } + + let mut items = Vec::with_capacity(features.len()); + for feature in features { + items.push(format!("\"{}\"", feature.name)); + } + + let chunk_size = group_size; + let chunk_count = items.len().div_ceil(chunk_size); + let mut lines = Vec::with_capacity(chunk_count); + for (index, chunk) in items.chunks(chunk_size).enumerate() { + let mut line = String::from("# "); + line.push_str(&chunk.join(", ")); + if index + 1 != chunk_count { + line.push(','); + } + lines.push(line); + } + + lines.join("\n") +} + +fn find_placeholder(rendered: &str) -> Option { + let start = rendered.find("{{")?; + let after = &rendered[start + 2..]; + if let Some(end_offset) = after.find("}}") { + let name = after[..end_offset].trim(); + if name.is_empty() { + Some(String::from("")) + } else { + Some(name.to_string()) + } + } else { + let snippet: String = after.chars().take(32).collect(); + Some(snippet) + } +} + +#[cfg_attr(test, allow(dead_code))] +fn write_if_changed(path: &Path, contents: &str) -> Result<(), ReadmeError> { + match fs::read_to_string(path) { + Ok(existing) => { + if existing == contents { + return Ok(()); + } + } + Err(err) => { + if err.kind() != io::ErrorKind::NotFound { + return Err(ReadmeError::Io(err)); + } + } + } + + fs::write(path, contents)?; + Ok(()) +} diff --git a/masterror-derive/src/lib.rs b/masterror-derive/src/lib.rs index 111b0c2..2d00622 100644 --- a/masterror-derive/src/lib.rs +++ b/masterror-derive/src/lib.rs @@ -20,7 +20,7 @@ use syn::{ /// Derive [`std::error::Error`] and [`core::fmt::Display`] for structs and /// enums. /// -/// ``` +/// ```ignore /// use masterror::Error; /// /// #[derive(Debug, Error)] diff --git a/tests/readme_sync.rs b/tests/readme_sync.rs new file mode 100644 index 0000000..06ce3fb --- /dev/null +++ b/tests/readme_sync.rs @@ -0,0 +1,22 @@ +#[path = "../build/readme.rs"] +mod readme; + +use std::{error::Error, fs, io, path::PathBuf}; + +#[test] +fn readme_is_in_sync() -> Result<(), Box> { + let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + let manifest_path = manifest_dir.join("Cargo.toml"); + let template_path = manifest_dir.join("README.template.md"); + let readme_path = manifest_dir.join("README.md"); + + let generated = readme::generate_readme(&manifest_path, &template_path)?; + let actual = fs::read_to_string(&readme_path)?; + + if actual != generated { + let message = "README.md is out of date; run `cargo build` to regenerate"; + return Err(io::Error::new(io::ErrorKind::Other, message).into()); + } + + Ok(()) +} From 9b5d35140d5cc7263706167790babfdc1a060712 Mon Sep 17 00:00:00 2001 From: RA <70325462+RAprogramm@users.noreply.github.com> Date: Wed, 17 Sep 2025 10:59:20 +0700 Subject: [PATCH 03/25] feat(derive): support #[from] conversions --- Cargo.lock | 54 ++++ Cargo.toml | 1 + masterror-derive/src/lib.rs | 276 +++++++++++++++++-- tests/error_derive.rs | 86 ++++++ tests/error_derive_from_trybuild.rs | 7 + tests/ui/from/struct_multiple_fields.rs | 15 + tests/ui/from/struct_multiple_fields.stderr | 5 + tests/ui/from/variant_multiple_fields.rs | 14 + tests/ui/from/variant_multiple_fields.stderr | 5 + 9 files changed, 443 insertions(+), 20 deletions(-) create mode 100644 tests/error_derive_from_trybuild.rs create mode 100644 tests/ui/from/struct_multiple_fields.rs create mode 100644 tests/ui/from/struct_multiple_fields.stderr create mode 100644 tests/ui/from/variant_multiple_fields.rs create mode 100644 tests/ui/from/variant_multiple_fields.stderr diff --git a/Cargo.lock b/Cargo.lock index aa1a5b2..a27f05c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -992,6 +992,12 @@ version = "0.31.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" +[[package]] +name = "glob" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" + [[package]] name = "hashbrown" version = "0.12.3" @@ -1509,6 +1515,7 @@ dependencies = [ "teloxide-core", "tokio", "tracing", + "trybuild", "utoipa", "validator", "wasm-bindgen", @@ -2699,6 +2706,12 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "20f34339676cdcab560c9a82300c4c2581f68b9369aedf0fae86f2ff9565ff3e" +[[package]] +name = "target-triple" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ac9aa371f599d22256307c24a9d748c041e548cbf599f35d890f9d365361790" + [[package]] name = "telegram-webapp-sdk" version = "0.1.1" @@ -2755,6 +2768,15 @@ dependencies = [ "uuid", ] +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + [[package]] name = "thiserror" version = "2.0.16" @@ -2889,10 +2911,12 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "75129e1dc5000bfbaa9fee9d1b21f974f9fbad9daec557a521ee6e080825f6e8" dependencies = [ + "indexmap 2.11.1", "serde", "serde_spanned", "toml_datetime", "toml_parser", + "toml_writer", "winnow", ] @@ -2914,6 +2938,12 @@ dependencies = [ "winnow", ] +[[package]] +name = "toml_writer" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcc842091f2def52017664b53082ecbbeb5c7731092bad69d2c63050401dfd64" + [[package]] name = "tower" version = "0.5.2" @@ -2997,6 +3027,21 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" +[[package]] +name = "trybuild" +version = "1.0.111" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ded9fdb81f30a5708920310bfcd9ea7482ff9cba5f54601f7a19a877d5c2392" +dependencies = [ + "glob", + "serde", + "serde_derive", + "serde_json", + "target-triple", + "termcolor", + "toml", +] + [[package]] name = "typeid" version = "1.0.3" @@ -3298,6 +3343,15 @@ dependencies = [ "wasite", ] +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.59.0", +] + [[package]] name = "windows-core" version = "0.61.2" diff --git a/Cargo.toml b/Cargo.toml index f8c3339..5308876 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -75,6 +75,7 @@ tokio = { version = "1", features = [ "net", "time", ], default-features = false } +trybuild = "1" [lib] crate-type = ["cdylib", "rlib"] diff --git a/masterror-derive/src/lib.rs b/masterror-derive/src/lib.rs index 111b0c2..51c4a41 100644 --- a/masterror-derive/src/lib.rs +++ b/masterror-derive/src/lib.rs @@ -4,8 +4,9 @@ //! Derive macro implementing [`std::error::Error`] with `Display` formatting. //! //! The macro mirrors the essential functionality relied upon by `masterror` and -//! consumers of the crate: display strings with named or positional fields and -//! a configurable error source via `#[source]` field attributes. +//! consumers of the crate: display strings with named or positional fields, +//! `#[from]` conversions for wrapper types, and a configurable error source via +//! `#[source]` field attributes. use std::collections::BTreeSet; @@ -21,7 +22,7 @@ use syn::{ /// enums. /// /// ``` -/// use masterror::Error; +/// use masterror_derive::Error; /// /// #[derive(Debug, Error)] /// #[error("{code}: {message}")] @@ -35,9 +36,9 @@ use syn::{ /// message: "boom" /// }; /// assert_eq!(err.to_string(), "500: boom"); -/// assert!(err.source().is_none()); +/// assert!(std::error::Error::source(&err).is_none()); /// ``` -#[proc_macro_derive(Error, attributes(error, source))] +#[proc_macro_derive(Error, attributes(error, source, from))] pub fn derive_error(input: TokenStream) -> TokenStream { match derive_error_impl(syn::parse_macro_input!(input as DeriveInput)) { Ok(tokens) => tokens.into(), @@ -51,6 +52,7 @@ fn derive_error_impl(input: DeriveInput) -> syn::Result { let display_impl; let error_impl; + let from_impls; match input.data { Data::Struct(data) => { @@ -58,11 +60,13 @@ fn derive_error_impl(input: DeriveInput) -> syn::Result { let display_attr = parse_display_attr(&input.attrs)?; display_impl = build_struct_display(&ident, &generics, &fields, &display_attr)?; error_impl = build_struct_error(&ident, &generics, &fields)?; + from_impls = build_struct_from_impl(&ident, &generics, &fields)?; } Data::Enum(data) => { let variants = parse_enum(&data)?; display_impl = build_enum_display(&ident, &generics, &variants)?; error_impl = build_enum_error(&ident, &generics, &variants)?; + from_impls = build_enum_from_impls(&ident, &generics, &variants)?; } Data::Union(_) => { return Err(syn::Error::new( @@ -75,6 +79,7 @@ fn derive_error_impl(input: DeriveInput) -> syn::Result { Ok(quote! { #display_impl #error_impl + #from_impls }) } @@ -89,7 +94,45 @@ enum FieldsStyle { struct FieldSpec { member: Member, ident: Option, - binding: Ident + binding: Ident, + ty: Type, + attrs: FieldAttributes +} + +#[derive(Clone, Default)] +struct FieldAttributes { + from: Option, + source: Option +} + +impl FieldAttributes { + fn mark_from(&mut self, span: Span) -> syn::Result<()> { + if self.from.is_some() { + return Err(syn::Error::new(span, "duplicate #[from] attribute")); + } + self.from = Some(span); + Ok(()) + } + + fn mark_source(&mut self, span: Span) -> syn::Result<()> { + if self.source.is_some() { + return Err(syn::Error::new(span, "duplicate #[source] attribute")); + } + self.source = Some(span); + Ok(()) + } + + fn has_source(&self) -> bool { + self.source.is_some() + } + + fn span_of_from_attribute(&self) -> Option { + self.from + } + + fn source_span(&self) -> Option { + self.source + } } #[derive(Clone, Copy)] @@ -116,6 +159,11 @@ struct VariantInfo { display: LitStr } +struct FromFieldInfo<'a> { + field: &'a FieldSpec, + span: Span +} + struct RewriteResult { literal: LitStr, positional_indices: BTreeSet @@ -129,7 +177,10 @@ fn parse_enum(data: &DataEnum) -> syn::Result> { let mut variants = Vec::with_capacity(data.variants.len()); for variant in &data.variants { let display = parse_display_attr(&variant.attrs)?; - let fields = parse_fields_internal(&variant.fields)?; + let mut fields = parse_fields_internal(&variant.fields)?; + if let Some(span) = parse_variant_from_attr(&variant.attrs)? { + apply_variant_from_attr(&mut fields, span, &variant.ident)?; + } variants.push(VariantInfo { ident: variant.ident.clone(), fields, @@ -150,16 +201,17 @@ fn parse_fields_internal(fields: &Fields) -> syn::Result { let mut specs = Vec::with_capacity(named.named.len()); let mut source = None; for (index, field) in named.named.iter().enumerate() { + let attrs = parse_field_attributes(field)?; let ident = field.ident.clone().ok_or_else(|| { syn::Error::new(field.span(), "named field missing identifier") })?; let member = Member::Named(ident.clone()); let binding = ident.clone(); - if has_source_attr(field)? { + if attrs.has_source() { let kind = detect_source_kind(&field.ty)?; if source.is_some() { return Err(syn::Error::new( - field.span(), + attrs.source_span().unwrap_or_else(|| field.span()), "only a single #[source] field is supported" )); } @@ -171,7 +223,9 @@ fn parse_fields_internal(fields: &Fields) -> syn::Result { specs.push(FieldSpec { member, ident: Some(ident), - binding + binding, + ty: field.ty.clone(), + attrs }); } Ok(ParsedFields { @@ -184,13 +238,14 @@ fn parse_fields_internal(fields: &Fields) -> syn::Result { let mut specs = Vec::with_capacity(unnamed.unnamed.len()); let mut source = None; for (index, field) in unnamed.unnamed.iter().enumerate() { + let attrs = parse_field_attributes(field)?; let member = Member::Unnamed(index.into()); let binding = format_ident!("__masterror_{index}"); - if has_source_attr(field)? { + if attrs.has_source() { let kind = detect_source_kind(&field.ty)?; if source.is_some() { return Err(syn::Error::new( - field.span(), + attrs.source_span().unwrap_or_else(|| field.span()), "only a single #[source] field is supported" )); } @@ -202,7 +257,9 @@ fn parse_fields_internal(fields: &Fields) -> syn::Result { specs.push(FieldSpec { member, ident: None, - binding + binding, + ty: field.ty.clone(), + attrs }); } Ok(ParsedFields { @@ -240,20 +297,101 @@ fn parse_display_attr(attrs: &[Attribute]) -> syn::Result { .ok_or_else(|| syn::Error::new(Span::call_site(), r#"missing #[error("...")] attribute"#)) } -fn has_source_attr(field: &Field) -> syn::Result { - let mut found = false; +fn parse_field_attributes(field: &Field) -> syn::Result { + let mut attrs = FieldAttributes::default(); for attr in &field.attrs { if attr.path().is_ident("source") { - if found { + ensure_path_only(attr, "source")?; + attrs.mark_source(attr.span())?; + } else if attr.path().is_ident("from") { + ensure_path_only(attr, "from")?; + attrs.mark_from(attr.span())?; + } + } + Ok(attrs) +} + +fn ensure_path_only(attr: &Attribute, name: &str) -> syn::Result<()> { + if !matches!(&attr.meta, Meta::Path(_)) { + return Err(syn::Error::new( + attr.span(), + format!("#[{name}] attribute does not accept arguments") + )); + } + Ok(()) +} + +fn parse_variant_from_attr(attrs: &[Attribute]) -> syn::Result> { + let mut span = None; + for attr in attrs.iter().filter(|attr| attr.path().is_ident("from")) { + ensure_path_only(attr, "from")?; + if span.is_some() { + return Err(syn::Error::new(attr.span(), "duplicate #[from] attribute")); + } + span = Some(attr.span()); + } + Ok(span) +} + +fn apply_variant_from_attr( + fields: &mut ParsedFields, + span: Span, + variant_ident: &Ident +) -> syn::Result<()> { + if fields.fields.is_empty() { + return Err(syn::Error::new( + span, + format!( + "variant `{variant_ident}` marked with #[from] must contain exactly one field" + ) + )); + } + if fields.fields.len() != 1 { + return Err(syn::Error::new( + span, + format!( + "variant `{variant_ident}` marked with #[from] must contain exactly one field" + ) + )); + } + let field = fields + .fields + .get_mut(0) + .ok_or_else(|| syn::Error::new(span, "invalid #[from] field index"))?; + field.attrs.mark_from(span) +} + +fn find_from_field<'a>( + fields: &'a ParsedFields, + context: &str +) -> syn::Result>> { + let mut info = None; + for field in &fields.fields { + if let Some(span) = field.attrs.span_of_from_attribute() { + if info.is_some() { return Err(syn::Error::new( - attr.span(), - "duplicate #[source] attribute" + span, + format!( + "multiple #[from] attributes in {context}; only one field may use #[from]" + ) )); } - found = true; + info = Some(FromFieldInfo { + field, + span + }); } } - Ok(found) + let Some(info) = info else { + return Ok(None); + }; + if fields.fields.len() != 1 { + return Err(syn::Error::new( + info.span, + format!("using #[from] in {context} requires exactly one field") + )); + } + Ok(Some(info)) } fn detect_source_kind(ty: &Type) -> syn::Result { @@ -403,6 +541,52 @@ fn build_struct_error( }) } +fn build_struct_from_impl( + ident: &Ident, + generics: &Generics, + fields: &ParsedFields +) -> syn::Result { + let context = format!("struct `{ident}`"); + let Some(from_info) = find_from_field(fields, &context)? else { + return Ok(TokenStream2::new()); + }; + let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); + let field_ty = &from_info.field.ty; + let arg_ident = format_ident!("__masterror_from_value"); + + let construct = match fields.style { + FieldsStyle::Named => { + let field_ident = from_info.field.ident.clone().ok_or_else(|| { + syn::Error::new(from_info.span, "named field missing identifier") + })?; + quote! { Self { #field_ident: #arg_ident } } + } + FieldsStyle::Unnamed => { + if fields.fields.len() != 1 { + return Err(syn::Error::new( + from_info.span, + format!("using #[from] in {context} requires exactly one field") + )); + } + quote! { Self(#arg_ident) } + } + FieldsStyle::Unit => { + return Err(syn::Error::new( + from_info.span, + format!("using #[from] in {context} requires at least one field") + )); + } + }; + + Ok(quote! { + impl #impl_generics ::core::convert::From<#field_ty> for #ident #ty_generics #where_clause { + fn from(#arg_ident: #field_ty) -> Self { + #construct + } + } + }) +} + fn build_enum_display( ident: &Ident, generics: &Generics, @@ -598,6 +782,58 @@ fn build_enum_error( }) } +fn build_enum_from_impls( + ident: &Ident, + generics: &Generics, + variants: &[VariantInfo] +) -> syn::Result { + let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); + let mut impls = Vec::new(); + + for variant in variants { + let context = format!("variant `{}`", variant.ident); + if let Some(from_info) = find_from_field(&variant.fields, &context)? { + let field_ty = &from_info.field.ty; + let variant_ident = &variant.ident; + let arg_ident = format_ident!("__masterror_from_value"); + + let body = match variant.fields.style { + FieldsStyle::Named => { + let field_ident = from_info.field.ident.clone().ok_or_else(|| { + syn::Error::new(from_info.span, "named field missing identifier") + })?; + quote! { Self::#variant_ident { #field_ident: #arg_ident } } + } + FieldsStyle::Unnamed => { + if variant.fields.fields.len() != 1 { + return Err(syn::Error::new( + from_info.span, + format!("using #[from] in {context} requires exactly one field") + )); + } + quote! { Self::#variant_ident(#arg_ident) } + } + FieldsStyle::Unit => { + return Err(syn::Error::new( + from_info.span, + format!("{context} cannot be unit-like when using #[from]") + )); + } + }; + + impls.push(quote! { + impl #impl_generics ::core::convert::From<#field_ty> for #ident #ty_generics #where_clause { + fn from(#arg_ident: #field_ty) -> Self { + #body + } + } + }); + } + } + + Ok(quote! { #(#impls)* }) +} + fn rewrite_format_string(original: &LitStr, field_count: usize) -> syn::Result { let src = original.value(); let mut result = String::with_capacity(src.len()); diff --git a/tests/error_derive.rs b/tests/error_derive.rs index b1171d0..3df72ab 100644 --- a/tests/error_derive.rs +++ b/tests/error_derive.rs @@ -33,6 +33,48 @@ enum EnumError { Pair(String, #[source] LeafError) } +#[derive(Debug, Error)] +#[error("primary failure")] +struct PrimaryError; + +#[derive(Debug, Error)] +#[error("secondary failure")] +struct SecondaryError; + +#[derive(Debug, Error)] +#[error("tuple wrapper -> {0}")] +struct TupleWrapper( + #[from] + #[source] + LeafError +); + +#[derive(Debug, Error)] +#[error("message: {message}")] +struct MessageWrapper { + #[from] + message: String +} + +#[derive(Debug, Error)] +enum MixedFromError { + #[error("tuple variant {0}")] + Tuple( + #[from] + #[source] + LeafError + ), + #[error("variant attr {0}")] + #[from] + VariantAttr(#[source] PrimaryError), + #[error("named variant {source:?}")] + Named { + #[from] + #[source] + source: SecondaryError + } +} + #[test] fn named_struct_display_and_source() { let err = NamedError { @@ -77,3 +119,47 @@ fn tuple_variant_with_source() { assert!(err.to_string().starts_with("left")); assert_eq!(StdError::source(&err).unwrap().to_string(), "leaf failure"); } + +#[test] +fn tuple_struct_from_wraps_source() { + let err = TupleWrapper::from(LeafError); + assert_eq!(err.to_string(), "tuple wrapper -> leaf failure"); + let source = StdError::source(&err).expect("source present"); + assert_eq!(source.to_string(), "leaf failure"); +} + +#[test] +fn named_struct_from_without_source() { + let err = MessageWrapper::from(String::from("payload")); + assert_eq!(err.to_string(), "message: payload"); + assert!(StdError::source(&err).is_none()); +} + +#[test] +fn enum_from_variants_generate_impls() { + let tuple = MixedFromError::from(LeafError); + assert!(matches!(&tuple, MixedFromError::Tuple(_))); + assert_eq!( + StdError::source(&tuple).unwrap().to_string(), + "leaf failure" + ); + + let variant_attr = MixedFromError::from(PrimaryError); + assert!(matches!(&variant_attr, MixedFromError::VariantAttr(_))); + assert_eq!( + StdError::source(&variant_attr).unwrap().to_string(), + "primary failure" + ); + + let named = MixedFromError::from(SecondaryError); + assert!(matches!( + &named, + MixedFromError::Named { + source: SecondaryError + } + )); + assert_eq!( + StdError::source(&named).unwrap().to_string(), + "secondary failure" + ); +} diff --git a/tests/error_derive_from_trybuild.rs b/tests/error_derive_from_trybuild.rs new file mode 100644 index 0000000..a878025 --- /dev/null +++ b/tests/error_derive_from_trybuild.rs @@ -0,0 +1,7 @@ +use trybuild::TestCases; + +#[test] +fn from_attribute_compile_failures() { + let t = TestCases::new(); + t.compile_fail("tests/ui/from/*.rs"); +} diff --git a/tests/ui/from/struct_multiple_fields.rs b/tests/ui/from/struct_multiple_fields.rs new file mode 100644 index 0000000..a545ce7 --- /dev/null +++ b/tests/ui/from/struct_multiple_fields.rs @@ -0,0 +1,15 @@ +use masterror::Error; + +#[derive(Debug, Error)] +#[error("{left:?} - {right:?}")] +struct BadStruct { + #[from] + left: DummyError, + right: DummyError, +} + +#[derive(Debug, Error)] +#[error("dummy")] +struct DummyError; + +fn main() {} diff --git a/tests/ui/from/struct_multiple_fields.stderr b/tests/ui/from/struct_multiple_fields.stderr new file mode 100644 index 0000000..c529748 --- /dev/null +++ b/tests/ui/from/struct_multiple_fields.stderr @@ -0,0 +1,5 @@ +error: using #[from] in struct `BadStruct` requires exactly one field + --> tests/ui/from/struct_multiple_fields.rs:6:5 + | +6 | #[from] + | ^ diff --git a/tests/ui/from/variant_multiple_fields.rs b/tests/ui/from/variant_multiple_fields.rs new file mode 100644 index 0000000..3d35295 --- /dev/null +++ b/tests/ui/from/variant_multiple_fields.rs @@ -0,0 +1,14 @@ +use masterror::Error; + +#[derive(Debug, Error)] +enum BadEnum { + #[error("{0} - {1}")] + #[from] + Two(#[source] DummyError, DummyError), +} + +#[derive(Debug, Error)] +#[error("dummy")] +struct DummyError; + +fn main() {} diff --git a/tests/ui/from/variant_multiple_fields.stderr b/tests/ui/from/variant_multiple_fields.stderr new file mode 100644 index 0000000..0f07dd2 --- /dev/null +++ b/tests/ui/from/variant_multiple_fields.stderr @@ -0,0 +1,5 @@ +error: variant `Two` marked with #[from] must contain exactly one field + --> tests/ui/from/variant_multiple_fields.rs:6:5 + | +6 | #[from] + | ^ From 076164c5c025d5d533814406097a1a871e49e6d8 Mon Sep 17 00:00:00 2001 From: RAprogramm Date: Wed, 17 Sep 2025 11:10:04 +0700 Subject: [PATCH 04/25] increase version --- Cargo.lock | 217 +++++++++++++++++++++++-------------------- Cargo.toml | 8 +- src/frontend.rs | 2 + tests/readme_sync.rs | 5 +- 4 files changed, 127 insertions(+), 105 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 171a391..6a99d68 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -380,18 +380,18 @@ checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" [[package]] name = "bytestring" -version = "1.4.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e465647ae23b2823b0753f50decb2d5a86d2bb2cac04788fafd1f80e45378e5f" +checksum = "113b4343b5f6617e7ad401ced8de3cc8b012e73a594347c307b90db3e9271289" dependencies = [ "bytes", ] [[package]] name = "cc" -version = "1.2.36" +version = "1.2.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5252b3d2648e5eedbc1a6f501e3c795e07025c1e93bbf8bbdd6eef7f447a6d54" +checksum = "65193589c6404eb80b450d618eaf9a2cafaaafd57ecce47370519ef674a7bd44" dependencies = [ "find-msvc-tools", "shlex", @@ -412,7 +412,7 @@ dependencies = [ "iana-time-zone", "num-traits", "serde", - "windows-link 0.2.0", + "windows-link", ] [[package]] @@ -436,9 +436,9 @@ dependencies = [ [[package]] name = "config" -version = "0.15.15" +version = "0.15.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0faa974509d38b33ff89282db9c3295707ccf031727c0de9772038ec526852ba" +checksum = "cef036f0ecf99baef11555578630e2cca559909b4c50822dbba828c252d21c49" dependencies = [ "async-trait", "convert_case", @@ -446,10 +446,10 @@ dependencies = [ "pathdiff", "ron", "rust-ini", - "serde", "serde-untagged", + "serde_core", "serde_json", - "toml 0.9.5", + "toml 0.9.6", "winnow", "yaml-rust2", ] @@ -779,11 +779,12 @@ dependencies = [ [[package]] name = "erased-serde" -version = "0.4.6" +version = "0.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e004d887f51fcb9fef17317a2f3525c887d8aa3f4f50fed920816a688284a5b7" +checksum = "259d404d09818dec19332e31d94558aeb442fea04c817006456c24b5460bbd4b" dependencies = [ "serde", + "serde_core", "typeid", ] @@ -983,7 +984,7 @@ dependencies = [ "cfg-if", "libc", "r-efi", - "wasi 0.14.5+wasi-0.2.4", + "wasi 0.14.7+wasi-0.2.4", ] [[package]] @@ -1149,9 +1150,9 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.16" +version = "0.1.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d9b05277c7e8da2c93a568989bb6207bef0112e8d17df7a6eda4a3cf143bc5e" +checksum = "3c6995591a8f1380fcb4ba966a252a4b29188d51d2b89e3a252f5305be65aea8" dependencies = [ "base64 0.22.1", "bytes", @@ -1173,9 +1174,9 @@ dependencies = [ [[package]] name = "iana-time-zone" -version = "0.1.63" +version = "0.1.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8" +checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" dependencies = [ "android_system_properties", "core-foundation-sys", @@ -1327,13 +1328,14 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.11.1" +version = "2.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "206a8042aec68fa4a62e8d3f7aa4ceb508177d9324faf261e1959e495b7a1921" +checksum = "92119844f513ffa41556430369ab02c295a3578af21cf945caa3e9e0c2481ac3" dependencies = [ "equivalent", "hashbrown 0.15.5", "serde", + "serde_core", ] [[package]] @@ -1371,9 +1373,9 @@ checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" [[package]] name = "js-sys" -version = "0.3.78" +version = "0.3.79" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c0b063578492ceec17683ef2f8c5e89121fbd0b172cbc280635ab7567db2738" +checksum = "6247da8b8658ad4e73a186e747fcc5fc2a29f979d6fe6269127fdb5fd08298d0" dependencies = [ "once_cell", "wasm-bindgen", @@ -1419,9 +1421,9 @@ checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" [[package]] name = "libredox" -version = "0.1.9" +version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "391290121bad3d37fbddad76d8f5d1c1c314cfc646d143d7e07a3086ddff0ce3" +checksum = "416f7e718bdb06000964960ffa43b4335ad4012ae8b99060261aa4a8088d5ccb" dependencies = [ "bitflags", "libc", @@ -1479,9 +1481,9 @@ checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" [[package]] name = "masterror" -version = "0.3.3" +version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e7c3a243a6f697e05d0b971c22d0ac029b9080c20b2bbc5f4a3f43ea6024a60" +checksum = "3d38aeb53944762378aa5219b9929f2f3346a25fdd9b61266c24a487200c87fd" dependencies = [ "http 1.3.1", "serde", @@ -1509,6 +1511,7 @@ dependencies = [ "teloxide-core", "tokio", "toml 0.8.23", + "toml 0.9.6", "tracing", "utoipa", "validator", @@ -1740,9 +1743,9 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "pest" -version = "2.8.1" +version = "2.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1db05f56d34358a8b1066f67cbb203ee3e7ed2ba674a6263a1d5ec6db2204323" +checksum = "21e0a3a33733faeaf8651dfee72dd0f388f0c8e5ad496a3478fa5a922f49cfa8" dependencies = [ "memchr", "thiserror", @@ -1751,9 +1754,9 @@ dependencies = [ [[package]] name = "pest_derive" -version = "2.8.1" +version = "2.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb056d9e8ea77922845ec74a1c4e8fb17e7c218cc4fc11a15c5d25e189aa40bc" +checksum = "bc58706f770acb1dbd0973e6530a3cff4746fb721207feb3a8a6064cd0b6c663" dependencies = [ "pest", "pest_generator", @@ -1761,9 +1764,9 @@ dependencies = [ [[package]] name = "pest_generator" -version = "2.8.1" +version = "2.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87e404e638f781eb3202dc82db6760c8ae8a1eeef7fb3fa8264b2ef280504966" +checksum = "6d4f36811dfe07f7b8573462465d5cb8965fffc2e71ae377a33aecf14c2c9a2f" dependencies = [ "pest", "pest_meta", @@ -1774,9 +1777,9 @@ dependencies = [ [[package]] name = "pest_meta" -version = "2.8.1" +version = "2.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edd1101f170f5903fde0914f899bb503d9ff5271d7ba76bbb70bea63690cc0d5" +checksum = "42919b05089acbd0a5dcd5405fb304d17d1053847b81163d09c4ad18ce8e8420" dependencies = [ "pest", "sha2", @@ -2213,27 +2216,29 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "semver" -version = "1.0.26" +version = "1.0.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" [[package]] name = "serde" -version = "1.0.219" +version = "1.0.225" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +checksum = "fd6c24dee235d0da097043389623fb913daddf92c76e9f5a1db88607a0bcbd1d" dependencies = [ + "serde_core", "serde_derive", ] [[package]] name = "serde-untagged" -version = "0.1.8" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34836a629bcbc6f1afdf0907a744870039b1e14c0561cb26094fa683b158eff3" +checksum = "f9faf48a4a2d2693be24c6289dbe26552776eb7737074e6722891fadbe6c5058" dependencies = [ "erased-serde", "serde", + "serde_core", "typeid", ] @@ -2248,11 +2253,20 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "serde_core" +version = "1.0.225" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "659356f9a0cb1e529b24c01e43ad2bdf520ec4ceaf83047b83ddcc2251f96383" +dependencies = [ + "serde_derive", +] + [[package]] name = "serde_derive" -version = "1.0.219" +version = "1.0.225" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +checksum = "0ea936adf78b1f766949a4977b91d2f5595825bd6ec079aa9543ad2685fc4516" dependencies = [ "proc-macro2", "quote", @@ -2261,24 +2275,26 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.143" +version = "1.0.145" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d401abef1d108fbd9cbaebc3e46611f4b1021f714a0597a71f41ee463f5f4a5a" +checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" dependencies = [ "itoa", "memchr", "ryu", "serde", + "serde_core", ] [[package]] name = "serde_path_to_error" -version = "0.1.17" +version = "0.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59fab13f937fa393d08645bf3a84bdfe86e296747b506ada67bb15f10f218b2a" +checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457" dependencies = [ "itoa", "serde", + "serde_core", ] [[package]] @@ -2292,11 +2308,11 @@ dependencies = [ [[package]] name = "serde_spanned" -version = "1.0.0" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40734c41988f7306bb04f0ecf60ec0f3f1caa34290e4e8ea471dcd3346483b83" +checksum = "2789234a13a53fc4be1b51ea1bab45a3c338bdb884862a257d10e5a74ae009e6" dependencies = [ - "serde", + "serde_core", ] [[package]] @@ -2321,7 +2337,7 @@ dependencies = [ "chrono", "hex", "indexmap 1.9.3", - "indexmap 2.11.1", + "indexmap 2.11.3", "schemars 0.9.0", "schemars 1.0.4", "serde", @@ -2475,7 +2491,7 @@ dependencies = [ "futures-util", "hashbrown 0.15.5", "hashlink", - "indexmap 2.11.1", + "indexmap 2.11.3", "log", "memchr", "once_cell", @@ -2711,16 +2727,16 @@ checksum = "20f34339676cdcab560c9a82300c4c2581f68b9369aedf0fae86f2ff9565ff3e" [[package]] name = "telegram-webapp-sdk" -version = "0.1.1" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80632ebd5e273b42ddff5f444be5d6e2d7976a283008853a3b3af6a183f936e3" +checksum = "fb72a0fd8e65f7c9279d164e9b00661e6685f74c0ba63e7f17b30662d5aed21b" dependencies = [ "base64 0.22.1", "ed25519-dalek", "hex", "hmac-sha256", "js-sys", - "masterror 0.3.3", + "masterror 0.3.5", "once_cell", "percent-encoding", "serde", @@ -2728,6 +2744,7 @@ dependencies = [ "serde_json", "serde_urlencoded", "thiserror", + "toml 0.8.23", "wasm-bindgen", "wasm-bindgen-futures", "web-sys", @@ -2907,14 +2924,16 @@ dependencies = [ [[package]] name = "toml" -version = "0.9.5" +version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75129e1dc5000bfbaa9fee9d1b21f974f9fbad9daec557a521ee6e080825f6e8" +checksum = "ae2a4cf385da23d1d53bc15cdfa5c2109e93d8d362393c801e87da2f72f0e201" dependencies = [ - "serde", - "serde_spanned 1.0.0", - "toml_datetime 0.7.0", + "indexmap 2.11.3", + "serde_core", + "serde_spanned 1.0.1", + "toml_datetime 0.7.1", "toml_parser", + "toml_writer", "winnow", ] @@ -2929,11 +2948,11 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "0.7.0" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bade1c3e902f58d73d3f294cd7f20391c1cb2fbcb643b73566bc773971df91e3" +checksum = "a197c0ec7d131bfc6f7e82c8442ba1595aeab35da7adbf05b6b73cd06a16b6be" dependencies = [ - "serde", + "serde_core", ] [[package]] @@ -2942,7 +2961,7 @@ version = "0.22.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" dependencies = [ - "indexmap 2.11.1", + "indexmap 2.11.3", "serde", "serde_spanned 0.6.9", "toml_datetime 0.6.11", @@ -2965,6 +2984,12 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" +[[package]] +name = "toml_writer" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcc842091f2def52017664b53082ecbbeb5c7731092bad69d2c63050401dfd64" + [[package]] name = "tower" version = "0.5.2" @@ -3135,7 +3160,7 @@ version = "5.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2fcc29c80c21c31608227e0912b2d7fddba57ad76b606890627ba8ee7964e993" dependencies = [ - "indexmap 2.11.1", + "indexmap 2.11.3", "serde", "serde_json", "utoipa-gen", @@ -3222,18 +3247,18 @@ checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasi" -version = "0.14.5+wasi-0.2.4" +version = "0.14.7+wasi-0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4494f6290a82f5fe584817a676a34b9d6763e8d9d18204009fb31dceca98fd4" +checksum = "883478de20367e224c0090af9cf5f9fa85bed63a95c1abf3afc5c083ebc06e8c" dependencies = [ "wasip2", ] [[package]] name = "wasip2" -version = "1.0.0+wasi-0.2.4" +version = "1.0.1+wasi-0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03fa2761397e5bd52002cd7e73110c71af2109aca4e521a9f40473fe685b0a24" +checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" dependencies = [ "wit-bindgen", ] @@ -3246,9 +3271,9 @@ checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" [[package]] name = "wasm-bindgen" -version = "0.2.101" +version = "0.2.102" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e14915cadd45b529bb8d1f343c4ed0ac1de926144b746e2710f9cd05df6603b" +checksum = "4ad224d2776649cfb4f4471124f8176e54c1cca67a88108e30a0cd98b90e7ad3" dependencies = [ "cfg-if", "once_cell", @@ -3259,9 +3284,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.101" +version = "0.2.102" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e28d1ba982ca7923fd01448d5c30c6864d0a14109560296a162f80f305fb93bb" +checksum = "3a1364104bdcd3c03f22b16a3b1c9620891469f5e9f09bc38b2db121e593e732" dependencies = [ "bumpalo", "log", @@ -3273,9 +3298,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.51" +version = "0.4.52" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ca85039a9b469b38336411d6d6ced91f3fc87109a2a27b0c197663f5144dffe" +checksum = "9c0a08ecf5d99d5604a6666a70b3cde6ab7cc6142f5e641a8ef48fc744ce8854" dependencies = [ "cfg-if", "js-sys", @@ -3286,9 +3311,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.101" +version = "0.2.102" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c3d463ae3eff775b0c45df9da45d68837702ac35af998361e2c84e7c5ec1b0d" +checksum = "0d7ab4ca3e367bb1ed84ddbd83cc6e41e115f8337ed047239578210214e36c76" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -3296,9 +3321,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.101" +version = "0.2.102" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7bb4ce89b08211f923caf51d527662b75bdc9c9c7aab40f86dcb9fb85ac552aa" +checksum = "4a518014843a19e2dbbd0ed5dfb6b99b23fb886b14e6192a00803a3e14c552b0" dependencies = [ "proc-macro2", "quote", @@ -3309,9 +3334,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.101" +version = "0.2.102" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f143854a3b13752c6950862c906306adb27c7e839f7414cec8fea35beab624c1" +checksum = "255eb0aa4cc2eea3662a00c2bbd66e93911b7361d5e0fcd62385acfd7e15dcee" dependencies = [ "unicode-ident", ] @@ -3331,9 +3356,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.78" +version = "0.3.79" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77e4b637749ff0d92b8fad63aa1f7cff3cbe125fd49c175cd6345e7272638b12" +checksum = "50462a022f46851b81d5441d1a6f5bac0b21a1d72d64bd4906fbdd4bf7230ec7" dependencies = [ "js-sys", "wasm-bindgen", @@ -3351,13 +3376,13 @@ dependencies = [ [[package]] name = "windows-core" -version = "0.61.2" +version = "0.62.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" +checksum = "57fe7168f7de578d2d8a05b07fd61870d2e73b4020e9f49aa00da8471723497c" dependencies = [ "windows-implement", "windows-interface", - "windows-link 0.1.3", + "windows-link", "windows-result", "windows-strings", ] @@ -3384,12 +3409,6 @@ dependencies = [ "syn", ] -[[package]] -name = "windows-link" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" - [[package]] name = "windows-link" version = "0.2.0" @@ -3398,20 +3417,20 @@ checksum = "45e46c0661abb7180e7b9c281db115305d49ca1709ab8242adf09666d2173c65" [[package]] name = "windows-result" -version = "0.3.4" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" +checksum = "7084dcc306f89883455a206237404d3eaf961e5bd7e0f312f7c91f57eb44167f" dependencies = [ - "windows-link 0.1.3", + "windows-link", ] [[package]] name = "windows-strings" -version = "0.4.2" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" +checksum = "7218c655a553b0bed4426cf54b20d7ba363ef543b52d515b3e48d7fd55318dda" dependencies = [ - "windows-link 0.1.3", + "windows-link", ] [[package]] @@ -3573,9 +3592,9 @@ dependencies = [ [[package]] name = "wit-bindgen" -version = "0.45.1" +version = "0.46.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c573471f125075647d03df72e026074b7203790d41351cd6edc96f46bcccd36" +checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" [[package]] name = "writeable" @@ -3585,9 +3604,9 @@ checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" [[package]] name = "yaml-rust2" -version = "0.10.3" +version = "0.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ce2a4ff45552406d02501cea6c18d8a7e50228e7736a872951fe2fe75c91be7" +checksum = "2462ea039c445496d8793d052e13787f2b90e750b833afee748e601c17621ed9" dependencies = [ "arraydeque", "encoding_rs", diff --git a/Cargo.toml b/Cargo.toml index 84afc47..eb65f6e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,13 +19,13 @@ members = ["masterror-derive"] [features] default = [] -axum = ["dep:axum", "dep:serde_json"] # IntoResponse + JSON body +axum = ["dep:axum", "dep:serde_json"] # IntoResponse + JSON body actix = ["dep:actix-web", "dep:serde_json"] sqlx = ["dep:sqlx"] redis = ["dep:redis"] validator = ["dep:validator"] serde_json = ["dep:serde_json"] -config = ["dep:config"] # config::ConfigError -> AppError +config = ["dep:config"] # config::ConfigError -> AppError multipart = ["axum"] tokio = ["dep:tokio"] reqwest = ["dep:reqwest"] @@ -63,7 +63,7 @@ utoipa = { version = "5.4", optional = true } tokio = { version = "1", optional = true, features = ["time"] } reqwest = { version = "0.12", optional = true, default-features = false } teloxide-core = { version = "0.13", optional = true, default-features = false } -telegram-webapp-sdk = { version = "0.1", optional = true } +telegram-webapp-sdk = { version = "0.2", optional = true } wasm-bindgen = { version = "0.2", optional = true } js-sys = { version = "0.3", optional = true } serde-wasm-bindgen = { version = "0.6", optional = true } @@ -77,7 +77,7 @@ tokio = { version = "1", features = [ "time", ], default-features = false } -toml = "0.8" +toml = "0.9" [build-dependencies] serde = { version = "1", features = ["derive"] } diff --git a/src/frontend.rs b/src/frontend.rs index 403216c..3a64293 100644 --- a/src/frontend.rs +++ b/src/frontend.rs @@ -1,3 +1,5 @@ +#![allow(unused_variables)] + //! Browser/WASM helpers for converting application errors into JavaScript //! values. //! diff --git a/tests/readme_sync.rs b/tests/readme_sync.rs index 06ce3fb..37dde59 100644 --- a/tests/readme_sync.rs +++ b/tests/readme_sync.rs @@ -14,8 +14,9 @@ fn readme_is_in_sync() -> Result<(), Box> { let actual = fs::read_to_string(&readme_path)?; if actual != generated { - let message = "README.md is out of date; run `cargo build` to regenerate"; - return Err(io::Error::new(io::ErrorKind::Other, message).into()); + // Use std::io::Error::other to satisfy clippy::io-other-error + let msg = "README.md is out of date; run `cargo build` to regenerate"; + return Err(io::Error::other(msg).into()); } Ok(()) From 7c7444d95f7afdf525c41cb63582255cdbd58a39 Mon Sep 17 00:00:00 2001 From: RA <70325462+RAprogramm@users.noreply.github.com> Date: Wed, 17 Sep 2025 11:10:43 +0700 Subject: [PATCH 05/25] Add BrowserConsoleError context accessor --- Cargo.lock | 8 ++++++- src/frontend.rs | 61 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 68 insertions(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index 23efdfa..5b1a057 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2989,6 +2989,12 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" +[[package]] +name = "toml_writer" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcc842091f2def52017664b53082ecbbeb5c7731092bad69d2c63050401dfd64" + [[package]] name = "tower" version = "0.5.2" @@ -3084,7 +3090,7 @@ dependencies = [ "serde_json", "target-triple", "termcolor", - "toml", + "toml 0.9.5", ] [[package]] diff --git a/src/frontend.rs b/src/frontend.rs index 403216c..cc8058a 100644 --- a/src/frontend.rs +++ b/src/frontend.rs @@ -79,6 +79,48 @@ pub enum BrowserConsoleError { UnsupportedTarget } +impl BrowserConsoleError { + /// Returns the contextual message associated with the error, when + /// available. + /// + /// This is primarily useful for surfacing browser-provided diagnostics in + /// higher-level logs or telemetry. + /// + /// # Examples + /// + /// ``` + /// # #[cfg(feature = "frontend")] + /// # { + /// use masterror::frontend::BrowserConsoleError; + /// + /// let err = BrowserConsoleError::ConsoleUnavailable { + /// message: "console missing".to_owned() + /// }; + /// assert_eq!(err.context(), Some("console missing")); + /// + /// let err = BrowserConsoleError::ConsoleMethodNotCallable; + /// assert_eq!(err.context(), None); + /// # } + /// ``` + pub fn context(&self) -> Option<&str> { + match self { + Self::Serialization { + message + } + | Self::ConsoleUnavailable { + message + } + | Self::ConsoleErrorUnavailable { + message + } + | Self::ConsoleInvocation { + message + } => Some(message.as_str()), + Self::ConsoleMethodNotCallable | Self::UnsupportedTarget => None + } + } +} + /// Extensions for serializing errors to JavaScript and logging to the browser /// console. #[cfg_attr(docsrs, doc(cfg(feature = "frontend")))] @@ -181,6 +223,25 @@ mod tests { use super::*; use crate::AppCode; + #[test] + fn context_returns_optional_message() { + let serialization = BrowserConsoleError::Serialization { + message: "encode failed".to_owned() + }; + assert_eq!(serialization.context(), Some("encode failed")); + + let invocation = BrowserConsoleError::ConsoleInvocation { + message: "js error".to_owned() + }; + assert_eq!(invocation.context(), Some("js error")); + + assert_eq!( + BrowserConsoleError::ConsoleMethodNotCallable.context(), + None + ); + assert_eq!(BrowserConsoleError::UnsupportedTarget.context(), None); + } + #[cfg(not(target_arch = "wasm32"))] mod native { use super::*; From 79e43e070ba333c6344a0b183a4f6ef080b89727 Mon Sep 17 00:00:00 2001 From: RAprogramm Date: Wed, 17 Sep 2025 11:15:17 +0700 Subject: [PATCH 06/25] increase version --- Cargo.lock | 2261 +++++++++++++++++++++++++++++++++------------------- 1 file changed, 1447 insertions(+), 814 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f5fb1de..ae55d42 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8,15 +8,15 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f7b0a21988c1bf877cf4759ef5ddaac04c1c9fe808c9142ecb78ba97d97a28a" dependencies = [ - "bitflags", - "bytes", - "futures-core", - "futures-sink", - "memchr", - "pin-project-lite", - "tokio", - "tokio-util", - "tracing", + "bitflags", + "bytes", + "futures-core", + "futures-sink", + "memchr", + "pin-project-lite", + "tokio", + "tokio-util", + "tracing", ] [[package]] @@ -25,33 +25,33 @@ version = "3.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "44cceded2fb55f3c4b67068fa64962e2ca59614edc5b03167de9ff82ae803da0" dependencies = [ - "actix-codec", - "actix-rt", - "actix-service", - "actix-utils", - "base64 0.22.1", - "bitflags", - "bytes", - "bytestring", - "derive_more 2.0.1", - "encoding_rs", - "foldhash", - "futures-core", - "http 0.2.12", - "httparse", - "httpdate", - "itoa", - "language-tags", - "local-channel", - "mime", - "percent-encoding", - "pin-project-lite", - "rand 0.9.2", - "sha1", - "smallvec", - "tokio", - "tokio-util", - "tracing", + "actix-codec", + "actix-rt", + "actix-service", + "actix-utils", + "base64 0.22.1", + "bitflags", + "bytes", + "bytestring", + "derive_more 2.0.1", + "encoding_rs", + "foldhash", + "futures-core", + "http 0.2.12", + "httparse", + "httpdate", + "itoa", + "language-tags", + "local-channel", + "mime", + "percent-encoding", + "pin-project-lite", + "rand 0.9.2", + "sha1", + "smallvec", + "tokio", + "tokio-util", + "tracing", ] [[package]] @@ -59,7 +59,10 @@ name = "actix-macros" version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e01ed3140b2f8d422c68afa1ed2e85d996ea619c988ac834d255db32138655cb" -dependencies = ["quote", "syn"] +dependencies = [ + "quote", + "syn", +] [[package]] name = "actix-router" @@ -67,12 +70,12 @@ version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13d324164c51f63867b57e73ba5936ea151b8a41a1d23d1031eeb9f70d0236f8" dependencies = [ - "bytestring", - "cfg-if", - "http 0.2.12", - "regex-lite", - "serde", - "tracing", + "bytestring", + "cfg-if", + "http 0.2.12", + "regex-lite", + "serde", + "tracing", ] [[package]] @@ -80,7 +83,10 @@ name = "actix-rt" version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92589714878ca59a7626ea19734f0e07a6a875197eec751bb5d3f99e64998c63" -dependencies = ["futures-core", "tokio"] +dependencies = [ + "futures-core", + "tokio", +] [[package]] name = "actix-server" @@ -88,15 +94,15 @@ version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a65064ea4a457eaf07f2fba30b4c695bf43b721790e9530d26cb6f9019ff7502" dependencies = [ - "actix-rt", - "actix-service", - "actix-utils", - "futures-core", - "futures-util", - "mio", - "socket2 0.5.10", - "tokio", - "tracing", + "actix-rt", + "actix-service", + "actix-utils", + "futures-core", + "futures-util", + "mio", + "socket2 0.5.10", + "tokio", + "tracing", ] [[package]] @@ -104,14 +110,20 @@ name = "actix-service" version = "2.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9e46f36bf0e5af44bdc4bdb36fbbd421aa98c79a9bce724e1edeb3894e10dc7f" -dependencies = ["futures-core", "pin-project-lite"] +dependencies = [ + "futures-core", + "pin-project-lite", +] [[package]] name = "actix-utils" version = "3.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "88a1dcdff1466e3c2488e1cb5c36a71822750ad43839937f85d2f4d9f8b705d8" -dependencies = ["local-waker", "pin-project-lite"] +dependencies = [ + "local-waker", + "pin-project-lite", +] [[package]] name = "actix-web" @@ -119,39 +131,39 @@ version = "4.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a597b77b5c6d6a1e1097fddde329a83665e25c5437c696a3a9a4aa514a614dea" dependencies = [ - "actix-codec", - "actix-http", - "actix-macros", - "actix-router", - "actix-rt", - "actix-server", - "actix-service", - "actix-utils", - "actix-web-codegen", - "bytes", - "bytestring", - "cfg-if", - "derive_more 2.0.1", - "encoding_rs", - "foldhash", - "futures-core", - "futures-util", - "impl-more", - "itoa", - "language-tags", - "log", - "mime", - "once_cell", - "pin-project-lite", - "regex-lite", - "serde", - "serde_json", - "serde_urlencoded", - "smallvec", - "socket2 0.5.10", - "time", - "tracing", - "url", + "actix-codec", + "actix-http", + "actix-macros", + "actix-router", + "actix-rt", + "actix-server", + "actix-service", + "actix-utils", + "actix-web-codegen", + "bytes", + "bytestring", + "cfg-if", + "derive_more 2.0.1", + "encoding_rs", + "foldhash", + "futures-core", + "futures-util", + "impl-more", + "itoa", + "language-tags", + "log", + "mime", + "once_cell", + "pin-project-lite", + "regex-lite", + "serde", + "serde_json", + "serde_urlencoded", + "smallvec", + "socket2 0.5.10", + "time", + "tracing", + "url", ] [[package]] @@ -159,14 +171,21 @@ name = "actix-web-codegen" version = "4.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f591380e2e68490b5dfaf1dd1aa0ebe78d84ba7067078512b4ea6e4492d622b8" -dependencies = ["actix-router", "proc-macro2", "quote", "syn"] +dependencies = [ + "actix-router", + "proc-macro2", + "quote", + "syn", +] [[package]] name = "addr2line" version = "0.24.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" -dependencies = ["gimli"] +dependencies = [ + "gimli", +] [[package]] name = "adler2" @@ -179,7 +198,9 @@ name = "aho-corasick" version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" -dependencies = ["memchr"] +dependencies = [ + "memchr", +] [[package]] name = "allocator-api2" @@ -192,7 +213,9 @@ name = "android_system_properties" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" -dependencies = ["libc"] +dependencies = [ + "libc", +] [[package]] name = "arraydeque" @@ -205,14 +228,20 @@ name = "async-trait" version = "0.1.89" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" -dependencies = ["proc-macro2", "quote", "syn"] +dependencies = [ + "proc-macro2", + "quote", + "syn", +] [[package]] name = "atoi" version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f28d99ec8bfea296261ca1af174f24225171fea9664ba9003cbebee704810528" -dependencies = ["num-traits"] +dependencies = [ + "num-traits", +] [[package]] name = "atomic-waker" @@ -232,27 +261,27 @@ version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "021e862c184ae977658b36c4500f7feac3221ca5da43e3f25bd04ab6c79a29b5" dependencies = [ - "axum-core", - "bytes", - "futures-util", - "http 1.3.1", - "http-body", - "http-body-util", - "itoa", - "matchit", - "memchr", - "mime", - "multer", - "percent-encoding", - "pin-project-lite", - "rustversion", - "serde", - "serde_json", - "serde_path_to_error", - "sync_wrapper", - "tower", - "tower-layer", - "tower-service", + "axum-core", + "bytes", + "futures-util", + "http 1.3.1", + "http-body", + "http-body-util", + "itoa", + "matchit", + "memchr", + "mime", + "multer", + "percent-encoding", + "pin-project-lite", + "rustversion", + "serde", + "serde_json", + "serde_path_to_error", + "sync_wrapper", + "tower", + "tower-layer", + "tower-service", ] [[package]] @@ -261,17 +290,17 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68464cd0412f486726fb3373129ef5d2993f90c34bc2bc1c1e9943b2f4fc7ca6" dependencies = [ - "bytes", - "futures-core", - "http 1.3.1", - "http-body", - "http-body-util", - "mime", - "pin-project-lite", - "rustversion", - "sync_wrapper", - "tower-layer", - "tower-service", + "bytes", + "futures-core", + "http 1.3.1", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "rustversion", + "sync_wrapper", + "tower-layer", + "tower-service", ] [[package]] @@ -280,13 +309,13 @@ version = "0.3.75" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6806a6321ec58106fea15becdad98371e28d92ccbc7c8f1b3b6dd724fe8f1002" dependencies = [ - "addr2line", - "cfg-if", - "libc", - "miniz_oxide", - "object", - "rustc-demangle", - "windows-targets 0.52.6", + "addr2line", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", + "windows-targets 0.52.6", ] [[package]] @@ -312,14 +341,18 @@ name = "bitflags" version = "2.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2261d10cca569e4643e526d8dc2e62e433cc8aba21ab764233731f8d369bf394" -dependencies = ["serde"] +dependencies = [ + "serde", +] [[package]] name = "block-buffer" version = "0.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" -dependencies = ["generic-array"] +dependencies = [ + "generic-array", +] [[package]] name = "bumpalo" @@ -350,14 +383,19 @@ name = "bytestring" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "113b4343b5f6617e7ad401ced8de3cc8b012e73a594347c307b90db3e9271289" -dependencies = ["bytes"] +dependencies = [ + "bytes", +] [[package]] name = "cc" version = "1.2.37" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "65193589c6404eb80b450d618eaf9a2cafaaafd57ecce47370519ef674a7bd44" -dependencies = ["find-msvc-tools", "shlex"] +dependencies = [ + "find-msvc-tools", + "shlex", +] [[package]] name = "cfg-if" @@ -370,21 +408,31 @@ name = "chrono" version = "0.4.42" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" -dependencies = ["iana-time-zone", "num-traits", "serde", "windows-link"] +dependencies = [ + "iana-time-zone", + "num-traits", + "serde", + "windows-link", +] [[package]] name = "combine" version = "4.6.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" -dependencies = ["bytes", "memchr"] +dependencies = [ + "bytes", + "memchr", +] [[package]] name = "concurrent-queue" version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" -dependencies = ["crossbeam-utils"] +dependencies = [ + "crossbeam-utils", +] [[package]] name = "config" @@ -392,18 +440,18 @@ version = "0.15.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cef036f0ecf99baef11555578630e2cca559909b4c50822dbba828c252d21c49" dependencies = [ - "async-trait", - "convert_case", - "json5", - "pathdiff", - "ron", - "rust-ini", - "serde-untagged", - "serde_core", - "serde_json", - "toml 0.9.6", - "winnow", - "yaml-rust2", + "async-trait", + "convert_case", + "json5", + "pathdiff", + "ron", + "rust-ini", + "serde-untagged", + "serde_core", + "serde_json", + "toml 0.9.6", + "winnow", + "yaml-rust2", ] [[package]] @@ -417,21 +465,29 @@ name = "const-random" version = "0.1.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "87e00182fe74b066627d63b85fd550ac2998d4b0bd86bfed477a0ae4c7c71359" -dependencies = ["const-random-macro"] +dependencies = [ + "const-random-macro", +] [[package]] name = "const-random-macro" version = "0.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e" -dependencies = ["getrandom 0.2.16", "once_cell", "tiny-keccak"] +dependencies = [ + "getrandom 0.2.16", + "once_cell", + "tiny-keccak", +] [[package]] name = "convert_case" version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca" -dependencies = ["unicode-segmentation"] +dependencies = [ + "unicode-segmentation", +] [[package]] name = "core-foundation-sys" @@ -444,14 +500,18 @@ name = "cpufeatures" version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" -dependencies = ["libc"] +dependencies = [ + "libc", +] [[package]] name = "crc" version = "3.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9710d3b3739c2e349eb44fe848ad0b7c8cb1e42bd87ee49371df2f7acaf3e675" -dependencies = ["crc-catalog"] +dependencies = [ + "crc-catalog", +] [[package]] name = "crc-catalog" @@ -464,7 +524,9 @@ name = "crossbeam-queue" version = "0.3.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115" -dependencies = ["crossbeam-utils"] +dependencies = [ + "crossbeam-utils", +] [[package]] name = "crossbeam-utils" @@ -483,7 +545,10 @@ name = "crypto-common" version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" -dependencies = ["generic-array", "typenum"] +dependencies = [ + "generic-array", + "typenum", +] [[package]] name = "curve25519-dalek" @@ -491,14 +556,14 @@ version = "4.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" dependencies = [ - "cfg-if", - "cpufeatures", - "curve25519-dalek-derive", - "digest", - "fiat-crypto", - "rustc_version", - "subtle", - "zeroize", + "cfg-if", + "cpufeatures", + "curve25519-dalek-derive", + "digest", + "fiat-crypto", + "rustc_version", + "subtle", + "zeroize", ] [[package]] @@ -506,91 +571,141 @@ name = "curve25519-dalek-derive" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" -dependencies = ["proc-macro2", "quote", "syn"] +dependencies = [ + "proc-macro2", + "quote", + "syn", +] [[package]] name = "darling" version = "0.20.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" -dependencies = ["darling_core", "darling_macro"] +dependencies = [ + "darling_core", + "darling_macro", +] [[package]] name = "darling_core" version = "0.20.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" -dependencies = ["fnv", "ident_case", "proc-macro2", "quote", "strsim", "syn"] +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn", +] [[package]] name = "darling_macro" version = "0.20.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" -dependencies = ["darling_core", "quote", "syn"] +dependencies = [ + "darling_core", + "quote", + "syn", +] [[package]] name = "der" version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" -dependencies = ["const-oid", "pem-rfc7468", "zeroize"] +dependencies = [ + "const-oid", + "pem-rfc7468", + "zeroize", +] [[package]] name = "deranged" version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d630bccd429a5bb5a64b5e94f693bfc48c9f8566418fda4c494cc94f911f87cc" -dependencies = ["powerfmt", "serde"] +dependencies = [ + "powerfmt", + "serde", +] [[package]] name = "derive_more" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4a9b99b9cbbe49445b21764dc0625032a89b145a2642e67603e1c936f5458d05" -dependencies = ["derive_more-impl 1.0.0"] +dependencies = [ + "derive_more-impl 1.0.0", +] [[package]] name = "derive_more" version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "093242cf7570c207c83073cf82f79706fe7b8317e98620a47d5be7c3d8497678" -dependencies = ["derive_more-impl 2.0.1"] +dependencies = [ + "derive_more-impl 2.0.1", +] [[package]] name = "derive_more-impl" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cb7330aeadfbe296029522e6c40f315320aba36fc43a5b3632f3795348f3bd22" -dependencies = ["proc-macro2", "quote", "syn", "unicode-xid"] +dependencies = [ + "proc-macro2", + "quote", + "syn", + "unicode-xid", +] [[package]] name = "derive_more-impl" version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3" -dependencies = ["proc-macro2", "quote", "syn", "unicode-xid"] +dependencies = [ + "proc-macro2", + "quote", + "syn", + "unicode-xid", +] [[package]] name = "digest" version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" -dependencies = ["block-buffer", "const-oid", "crypto-common", "subtle"] +dependencies = [ + "block-buffer", + "const-oid", + "crypto-common", + "subtle", +] [[package]] name = "displaydoc" version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" -dependencies = ["proc-macro2", "quote", "syn"] +dependencies = [ + "proc-macro2", + "quote", + "syn", +] [[package]] name = "dlv-list" version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "442039f5147480ba31067cb00ada1adae6892028e40e45fc5de7b7df6dcc1b5f" -dependencies = ["const-random"] +dependencies = [ + "const-random", +] [[package]] name = "dotenvy" @@ -609,7 +724,10 @@ name = "ed25519" version = "2.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" -dependencies = ["pkcs8", "signature"] +dependencies = [ + "pkcs8", + "signature", +] [[package]] name = "ed25519-dalek" @@ -617,12 +735,12 @@ version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9" dependencies = [ - "curve25519-dalek", - "ed25519", - "serde", - "sha2", - "subtle", - "zeroize", + "curve25519-dalek", + "ed25519", + "serde", + "sha2", + "subtle", + "zeroize", ] [[package]] @@ -630,14 +748,18 @@ name = "either" version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" -dependencies = ["serde"] +dependencies = [ + "serde", +] [[package]] name = "encoding_rs" version = "0.8.35" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" -dependencies = ["cfg-if"] +dependencies = [ + "cfg-if", +] [[package]] name = "equivalent" @@ -650,28 +772,43 @@ name = "erasable" version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "437cfb75878119ed8265685c41a115724eae43fb7cc5a0bf0e4ecc3b803af1c4" -dependencies = ["autocfg", "scopeguard"] +dependencies = [ + "autocfg", + "scopeguard", +] [[package]] name = "erased-serde" version = "0.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "259d404d09818dec19332e31d94558aeb442fea04c817006456c24b5460bbd4b" -dependencies = ["serde", "serde_core", "typeid"] +dependencies = [ + "serde", + "serde_core", + "typeid", +] [[package]] name = "etcetera" version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "136d1b5283a1ab77bd9257427ffd09d8667ced0570b6f938942bc7568ed5b943" -dependencies = ["cfg-if", "home", "windows-sys 0.48.0"] +dependencies = [ + "cfg-if", + "home", + "windows-sys 0.48.0", +] [[package]] name = "event-listener" version = "5.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" -dependencies = ["concurrent-queue", "parking", "pin-project-lite"] +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] [[package]] name = "fiat-crypto" @@ -690,7 +827,11 @@ name = "flume" version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "da0e4dd2a88388a1f4ccc7c9ce104604dab68d9f408dc34cd45823d5a9069095" -dependencies = ["futures-core", "futures-sink", "spin"] +dependencies = [ + "futures-core", + "futures-sink", + "spin", +] [[package]] name = "fnv" @@ -709,7 +850,9 @@ name = "form_urlencoded" version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" -dependencies = ["percent-encoding"] +dependencies = [ + "percent-encoding", +] [[package]] name = "futures" @@ -717,13 +860,13 @@ version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" dependencies = [ - "futures-channel", - "futures-core", - "futures-executor", - "futures-io", - "futures-sink", - "futures-task", - "futures-util", + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", ] [[package]] @@ -731,7 +874,10 @@ name = "futures-channel" version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" -dependencies = ["futures-core", "futures-sink"] +dependencies = [ + "futures-core", + "futures-sink", +] [[package]] name = "futures-core" @@ -744,14 +890,22 @@ name = "futures-executor" version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" -dependencies = ["futures-core", "futures-task", "futures-util"] +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] [[package]] name = "futures-intrusive" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d930c203dd0b6ff06e0201a4a2fe9149b43c684fd4420555b26d21b1a02956f" -dependencies = ["futures-core", "lock_api", "parking_lot"] +dependencies = [ + "futures-core", + "lock_api", + "parking_lot", +] [[package]] name = "futures-io" @@ -764,7 +918,11 @@ name = "futures-macro" version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" -dependencies = ["proc-macro2", "quote", "syn"] +dependencies = [ + "proc-macro2", + "quote", + "syn", +] [[package]] name = "futures-sink" @@ -784,16 +942,16 @@ version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" dependencies = [ - "futures-channel", - "futures-core", - "futures-io", - "futures-macro", - "futures-sink", - "futures-task", - "memchr", - "pin-project-lite", - "pin-utils", - "slab", + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", ] [[package]] @@ -801,21 +959,33 @@ name = "generic-array" version = "0.14.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" -dependencies = ["typenum", "version_check"] +dependencies = [ + "typenum", + "version_check", +] [[package]] name = "getrandom" version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" -dependencies = ["cfg-if", "libc", "wasi 0.11.1+wasi-snapshot-preview1"] +dependencies = [ + "cfg-if", + "libc", + "wasi 0.11.1+wasi-snapshot-preview1", +] [[package]] name = "getrandom" version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" -dependencies = ["cfg-if", "libc", "r-efi", "wasi 0.14.7+wasi-0.2.4"] +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasi 0.14.7+wasi-0.2.4", +] [[package]] name = "gimli" @@ -846,14 +1016,20 @@ name = "hashbrown" version = "0.15.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" -dependencies = ["allocator-api2", "equivalent", "foldhash"] +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash", +] [[package]] name = "hashlink" version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" -dependencies = ["hashbrown 0.15.5"] +dependencies = [ + "hashbrown 0.15.5", +] [[package]] name = "heck" @@ -872,14 +1048,18 @@ name = "hkdf" version = "0.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" -dependencies = ["hmac"] +dependencies = [ + "hmac", +] [[package]] name = "hmac" version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" -dependencies = ["digest"] +dependencies = [ + "digest", +] [[package]] name = "hmac-sha256" @@ -892,28 +1072,41 @@ name = "home" version = "0.5.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589533453244b0995c858700322199b2becb13b627df2851f64a2775d024abcf" -dependencies = ["windows-sys 0.59.0"] +dependencies = [ + "windows-sys 0.59.0", +] [[package]] name = "http" version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" -dependencies = ["bytes", "fnv", "itoa"] +dependencies = [ + "bytes", + "fnv", + "itoa", +] [[package]] name = "http" version = "1.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" -dependencies = ["bytes", "fnv", "itoa"] +dependencies = [ + "bytes", + "fnv", + "itoa", +] [[package]] name = "http-body" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" -dependencies = ["bytes", "http 1.3.1"] +dependencies = [ + "bytes", + "http 1.3.1", +] [[package]] name = "http-body-util" @@ -921,11 +1114,11 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" dependencies = [ - "bytes", - "futures-core", - "http 1.3.1", - "http-body", - "pin-project-lite", + "bytes", + "futures-core", + "http 1.3.1", + "http-body", + "pin-project-lite", ] [[package]] @@ -946,19 +1139,19 @@ version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eb3aa54a13a0dfe7fbe3a59e0c76093041720fdc77b110cc0fc260fafb4dc51e" dependencies = [ - "atomic-waker", - "bytes", - "futures-channel", - "futures-core", - "http 1.3.1", - "http-body", - "httparse", - "itoa", - "pin-project-lite", - "pin-utils", - "smallvec", - "tokio", - "want", + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "http 1.3.1", + "http-body", + "httparse", + "itoa", + "pin-project-lite", + "pin-utils", + "smallvec", + "tokio", + "want", ] [[package]] @@ -967,22 +1160,22 @@ version = "0.1.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c6995591a8f1380fcb4ba966a252a4b29188d51d2b89e3a252f5305be65aea8" dependencies = [ - "base64 0.22.1", - "bytes", - "futures-channel", - "futures-core", - "futures-util", - "http 1.3.1", - "http-body", - "hyper", - "ipnet", - "libc", - "percent-encoding", - "pin-project-lite", - "socket2 0.6.0", - "tokio", - "tower-service", - "tracing", + "base64 0.22.1", + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "http 1.3.1", + "http-body", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2 0.6.0", + "tokio", + "tower-service", + "tracing", ] [[package]] @@ -991,13 +1184,13 @@ version = "0.1.64" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" dependencies = [ - "android_system_properties", - "core-foundation-sys", - "iana-time-zone-haiku", - "js-sys", - "log", - "wasm-bindgen", - "windows-core", + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", ] [[package]] @@ -1005,21 +1198,35 @@ name = "iana-time-zone-haiku" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" -dependencies = ["cc"] +dependencies = [ + "cc", +] [[package]] name = "icu_collections" version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47" -dependencies = ["displaydoc", "potential_utf", "yoke", "zerofrom", "zerovec"] +dependencies = [ + "displaydoc", + "potential_utf", + "yoke", + "zerofrom", + "zerovec", +] [[package]] name = "icu_locale_core" version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a" -dependencies = ["displaydoc", "litemap", "tinystr", "writeable", "zerovec"] +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] [[package]] name = "icu_normalizer" @@ -1027,13 +1234,13 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979" dependencies = [ - "displaydoc", - "icu_collections", - "icu_normalizer_data", - "icu_properties", - "icu_provider", - "smallvec", - "zerovec", + "displaydoc", + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", ] [[package]] @@ -1048,14 +1255,14 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b" dependencies = [ - "displaydoc", - "icu_collections", - "icu_locale_core", - "icu_properties_data", - "icu_provider", - "potential_utf", - "zerotrie", - "zerovec", + "displaydoc", + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "potential_utf", + "zerotrie", + "zerovec", ] [[package]] @@ -1070,15 +1277,15 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af" dependencies = [ - "displaydoc", - "icu_locale_core", - "stable_deref_trait", - "tinystr", - "writeable", - "yoke", - "zerofrom", - "zerotrie", - "zerovec", + "displaydoc", + "icu_locale_core", + "stable_deref_trait", + "tinystr", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", ] [[package]] @@ -1092,14 +1299,21 @@ name = "idna" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" -dependencies = ["idna_adapter", "smallvec", "utf8_iter"] +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] [[package]] name = "idna_adapter" version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" -dependencies = ["icu_normalizer", "icu_properties"] +dependencies = [ + "icu_normalizer", + "icu_properties", +] [[package]] name = "impl-more" @@ -1112,21 +1326,34 @@ name = "indexmap" version = "1.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" -dependencies = ["autocfg", "hashbrown 0.12.3", "serde"] +dependencies = [ + "autocfg", + "hashbrown 0.12.3", + "serde", +] [[package]] name = "indexmap" version = "2.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92119844f513ffa41556430369ab02c295a3578af21cf945caa3e9e0c2481ac3" -dependencies = ["equivalent", "hashbrown 0.15.5", "serde", "serde_core"] +dependencies = [ + "equivalent", + "hashbrown 0.15.5", + "serde", + "serde_core", +] [[package]] name = "io-uring" version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "046fa2d4d00aea763528b4950358d0ead425372445dc8ff86312b3c69ff7727b" -dependencies = ["bitflags", "cfg-if", "libc"] +dependencies = [ + "bitflags", + "cfg-if", + "libc", +] [[package]] name = "ipnet" @@ -1139,7 +1366,10 @@ name = "iri-string" version = "0.7.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dbc5ebe9c3a1a7a5127f920a418f7585e9e758e911d0466ed004f393b0e380b2" -dependencies = ["memchr", "serde"] +dependencies = [ + "memchr", + "serde", +] [[package]] name = "itoa" @@ -1152,14 +1382,21 @@ name = "js-sys" version = "0.3.79" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6247da8b8658ad4e73a186e747fcc5fc2a29f979d6fe6269127fdb5fd08298d0" -dependencies = ["once_cell", "wasm-bindgen"] +dependencies = [ + "once_cell", + "wasm-bindgen", +] [[package]] name = "json5" version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96b0db21af676c1ce64250b5f40f3ce2cf27e4e47cb91ed91eb6fe9350b430c1" -dependencies = ["pest", "pest_derive", "serde"] +dependencies = [ + "pest", + "pest_derive", + "serde", +] [[package]] name = "language-tags" @@ -1172,7 +1409,9 @@ name = "lazy_static" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" -dependencies = ["spin"] +dependencies = [ + "spin", +] [[package]] name = "libc" @@ -1191,14 +1430,21 @@ name = "libredox" version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "416f7e718bdb06000964960ffa43b4335ad4012ae8b99060261aa4a8088d5ccb" -dependencies = ["bitflags", "libc", "redox_syscall"] +dependencies = [ + "bitflags", + "libc", + "redox_syscall", +] [[package]] name = "libsqlite3-sys" version = "0.30.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149" -dependencies = ["pkg-config", "vcpkg"] +dependencies = [ + "pkg-config", + "vcpkg", +] [[package]] name = "litemap" @@ -1211,7 +1457,11 @@ name = "local-channel" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6cbc85e69b8df4b8bb8b89ec634e7189099cea8927a276b7384ce5488e53ec8" -dependencies = ["futures-core", "futures-sink", "local-waker"] +dependencies = [ + "futures-core", + "futures-sink", + "local-waker", +] [[package]] name = "local-waker" @@ -1224,7 +1474,10 @@ name = "lock_api" version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765" -dependencies = ["autocfg", "scopeguard"] +dependencies = [ + "autocfg", + "scopeguard", +] [[package]] name = "log" @@ -1237,40 +1490,49 @@ name = "masterror" version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d38aeb53944762378aa5219b9929f2f3346a25fdd9b61266c24a487200c87fd" -dependencies = ["http 1.3.1", "serde", "thiserror", "tracing"] +dependencies = [ + "http 1.3.1", + "serde", + "thiserror", + "tracing", +] [[package]] name = "masterror" version = "0.4.0" dependencies = [ - "actix-web", - "axum", - "config", - "http 1.3.1", - "js-sys", - "masterror-derive", - "redis", - "reqwest", - "serde", - "serde-wasm-bindgen", - "serde_json", - "sqlx", - "telegram-webapp-sdk", - "teloxide-core", - "tokio", - "toml 0.8.23", - "toml 0.9.6", - "tracing", - "trybuild", - "utoipa", - "validator", - "wasm-bindgen", + "actix-web", + "axum", + "config", + "http 1.3.1", + "js-sys", + "masterror-derive", + "redis", + "reqwest", + "serde", + "serde-wasm-bindgen", + "serde_json", + "sqlx", + "telegram-webapp-sdk", + "teloxide-core", + "tokio", + "toml 0.8.23", + "toml 0.9.6", + "tracing", + "trybuild", + "utoipa", + "validator", + "wasm-bindgen", ] [[package]] name = "masterror-derive" version = "0.1.0" -dependencies = ["proc-macro2", "quote", "syn"] +dependencies = [ + "proc-macro2", + "quote", + "syn", +] [[package]] name = "matchit" @@ -1283,7 +1545,10 @@ name = "md-5" version = "0.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" -dependencies = ["cfg-if", "digest"] +dependencies = [ + "cfg-if", + "digest", +] [[package]] name = "memchr" @@ -1302,14 +1567,19 @@ name = "mime_guess" version = "2.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" -dependencies = ["mime", "unicase"] +dependencies = [ + "mime", + "unicase", +] [[package]] name = "miniz_oxide" version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" -dependencies = ["adler2"] +dependencies = [ + "adler2", +] [[package]] name = "mio" @@ -1317,10 +1587,10 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" dependencies = [ - "libc", - "log", - "wasi 0.11.1+wasi-snapshot-preview1", - "windows-sys 0.59.0", + "libc", + "log", + "wasi 0.11.1+wasi-snapshot-preview1", + "windows-sys 0.59.0", ] [[package]] @@ -1329,15 +1599,15 @@ version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "83e87776546dc87511aa5ee218730c92b666d7264ab6ed41f9d215af9cd5224b" dependencies = [ - "bytes", - "encoding_rs", - "futures-util", - "http 1.3.1", - "httparse", - "memchr", - "mime", - "spin", - "version_check", + "bytes", + "encoding_rs", + "futures-util", + "http 1.3.1", + "httparse", + "memchr", + "mime", + "spin", + "version_check", ] [[package]] @@ -1345,7 +1615,10 @@ name = "num-bigint" version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" -dependencies = ["num-integer", "num-traits"] +dependencies = [ + "num-integer", + "num-traits", +] [[package]] name = "num-bigint-dig" @@ -1353,15 +1626,15 @@ version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc84195820f291c7697304f3cbdadd1cb7199c0efc917ff5eafd71225c136151" dependencies = [ - "byteorder", - "lazy_static", - "libm", - "num-integer", - "num-iter", - "num-traits", - "rand 0.8.5", - "smallvec", - "zeroize", + "byteorder", + "lazy_static", + "libm", + "num-integer", + "num-iter", + "num-traits", + "rand 0.8.5", + "smallvec", + "zeroize", ] [[package]] @@ -1375,28 +1648,39 @@ name = "num-integer" version = "0.1.46" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" -dependencies = ["num-traits"] +dependencies = [ + "num-traits", +] [[package]] name = "num-iter" version = "0.1.45" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" -dependencies = ["autocfg", "num-integer", "num-traits"] +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] [[package]] name = "num-traits" version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" -dependencies = ["autocfg", "libm"] +dependencies = [ + "autocfg", + "libm", +] [[package]] name = "object" version = "0.36.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" -dependencies = ["memchr"] +dependencies = [ + "memchr", +] [[package]] name = "once_cell" @@ -1409,7 +1693,10 @@ name = "ordered-multimap" version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49203cdcae0030493bad186b28da2fa25645fa276a51b6fec8010d281e02ef79" -dependencies = ["dlv-list", "hashbrown 0.14.5"] +dependencies = [ + "dlv-list", + "hashbrown 0.14.5", +] [[package]] name = "parking" @@ -1422,7 +1709,10 @@ name = "parking_lot" version = "0.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "70d58bf43669b5795d1576d0641cfb6fbb2057bf629506267a92807158584a13" -dependencies = ["lock_api", "parking_lot_core"] +dependencies = [ + "lock_api", + "parking_lot_core", +] [[package]] name = "parking_lot_core" @@ -1430,11 +1720,11 @@ version = "0.9.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5" dependencies = [ - "cfg-if", - "libc", - "redox_syscall", - "smallvec", - "windows-targets 0.52.6", + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets 0.52.6", ] [[package]] @@ -1448,7 +1738,9 @@ name = "pem-rfc7468" version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" -dependencies = ["base64ct"] +dependencies = [ + "base64ct", +] [[package]] name = "percent-encoding" @@ -1461,42 +1753,64 @@ name = "pest" version = "2.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "21e0a3a33733faeaf8651dfee72dd0f388f0c8e5ad496a3478fa5a922f49cfa8" -dependencies = ["memchr", "thiserror", "ucd-trie"] +dependencies = [ + "memchr", + "thiserror", + "ucd-trie", +] [[package]] name = "pest_derive" version = "2.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bc58706f770acb1dbd0973e6530a3cff4746fb721207feb3a8a6064cd0b6c663" -dependencies = ["pest", "pest_generator"] +dependencies = [ + "pest", + "pest_generator", +] [[package]] name = "pest_generator" version = "2.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6d4f36811dfe07f7b8573462465d5cb8965fffc2e71ae377a33aecf14c2c9a2f" -dependencies = ["pest", "pest_meta", "proc-macro2", "quote", "syn"] +dependencies = [ + "pest", + "pest_meta", + "proc-macro2", + "quote", + "syn", +] [[package]] name = "pest_meta" version = "2.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42919b05089acbd0a5dcd5405fb304d17d1053847b81163d09c4ad18ce8e8420" -dependencies = ["pest", "sha2"] +dependencies = [ + "pest", + "sha2", +] [[package]] name = "pin-project" version = "1.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a" -dependencies = ["pin-project-internal"] +dependencies = [ + "pin-project-internal", +] [[package]] name = "pin-project-internal" version = "1.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" -dependencies = ["proc-macro2", "quote", "syn"] +dependencies = [ + "proc-macro2", + "quote", + "syn", +] [[package]] name = "pin-project-lite" @@ -1515,14 +1829,21 @@ name = "pkcs1" version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" -dependencies = ["der", "pkcs8", "spki"] +dependencies = [ + "der", + "pkcs8", + "spki", +] [[package]] name = "pkcs8" version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" -dependencies = ["der", "spki"] +dependencies = [ + "der", + "spki", +] [[package]] name = "pkg-config" @@ -1535,7 +1856,9 @@ name = "potential_utf" version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "84df19adbe5b5a0782edcab45899906947ab039ccf4573713735ee7de1e6b08a" -dependencies = ["zerovec"] +dependencies = [ + "zerovec", +] [[package]] name = "powerfmt" @@ -1548,42 +1871,58 @@ name = "ppv-lite86" version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" -dependencies = ["zerocopy"] +dependencies = [ + "zerocopy", +] [[package]] name = "proc-macro-error-attr2" version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5" -dependencies = ["proc-macro2", "quote"] +dependencies = [ + "proc-macro2", + "quote", +] [[package]] name = "proc-macro-error2" version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802" -dependencies = ["proc-macro-error-attr2", "proc-macro2", "quote", "syn"] +dependencies = [ + "proc-macro-error-attr2", + "proc-macro2", + "quote", + "syn", +] [[package]] name = "proc-macro2" version = "1.0.101" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de" -dependencies = ["unicode-ident"] +dependencies = [ + "unicode-ident", +] [[package]] name = "psm" version = "0.1.26" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6e944464ec8536cd1beb0bbfd96987eb5e3b72f2ecdafdc5c769a37f1fa2ae1f" -dependencies = ["cc"] +dependencies = [ + "cc", +] [[package]] name = "quote" version = "1.0.40" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" -dependencies = ["proc-macro2"] +dependencies = [ + "proc-macro2", +] [[package]] name = "r-efi" @@ -1596,49 +1935,68 @@ name = "rand" version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" -dependencies = ["libc", "rand_chacha 0.3.1", "rand_core 0.6.4"] +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] [[package]] name = "rand" version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" -dependencies = ["rand_chacha 0.9.0", "rand_core 0.9.3"] +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.3", +] [[package]] name = "rand_chacha" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" -dependencies = ["ppv-lite86", "rand_core 0.6.4"] +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] [[package]] name = "rand_chacha" version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" -dependencies = ["ppv-lite86", "rand_core 0.9.3"] +dependencies = [ + "ppv-lite86", + "rand_core 0.9.3", +] [[package]] name = "rand_core" version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" -dependencies = ["getrandom 0.2.16"] +dependencies = [ + "getrandom 0.2.16", +] [[package]] name = "rand_core" version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" -dependencies = ["getrandom 0.3.3"] +dependencies = [ + "getrandom 0.3.3", +] [[package]] name = "rc-box" version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "897fecc9fac6febd4408f9e935e86df739b0023b625e610e0357535b9c8adad0" -dependencies = ["erasable"] +dependencies = [ + "erasable", +] [[package]] name = "redis" @@ -1646,13 +2004,13 @@ version = "0.32.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7cd3650deebc68526b304898b192fa4102a4ef0b9ada24da096559cb60e0eef8" dependencies = [ - "combine", - "itoa", - "num-bigint", - "percent-encoding", - "ryu", - "socket2 0.6.0", - "url", + "combine", + "itoa", + "num-bigint", + "percent-encoding", + "ryu", + "socket2 0.6.0", + "url", ] [[package]] @@ -1660,35 +2018,52 @@ name = "redox_syscall" version = "0.5.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5407465600fb0548f1442edf71dd20683c6ed326200ace4b1ef0763521bb3b77" -dependencies = ["bitflags"] +dependencies = [ + "bitflags", +] [[package]] name = "ref-cast" version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4a0ae411dbe946a674d89546582cea4ba2bb8defac896622d6496f14c23ba5cf" -dependencies = ["ref-cast-impl"] +dependencies = [ + "ref-cast-impl", +] [[package]] name = "ref-cast-impl" version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1165225c21bff1f3bbce98f5a1f889949bc902d3575308cc7b0de30b4f6d27c7" -dependencies = ["proc-macro2", "quote", "syn"] +dependencies = [ + "proc-macro2", + "quote", + "syn", +] [[package]] name = "regex" version = "1.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "23d7fd106d8c02486a8d64e778353d1cffe08ce79ac2e82f540c86d0facf6912" -dependencies = ["aho-corasick", "memchr", "regex-automata", "regex-syntax"] +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] [[package]] name = "regex-automata" version = "0.4.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6b9458fa0bfeeac22b5ca447c63aaf45f28439a709ccd244698632f9aa6394d6" -dependencies = ["aho-corasick", "memchr", "regex-syntax"] +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] [[package]] name = "regex-lite" @@ -1708,34 +2083,34 @@ version = "0.12.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d429f34c8092b2d42c7c93cec323bb4adeb7c67698f70839adec842ec10c7ceb" dependencies = [ - "base64 0.22.1", - "bytes", - "futures-core", - "futures-util", - "http 1.3.1", - "http-body", - "http-body-util", - "hyper", - "hyper-util", - "js-sys", - "log", - "mime_guess", - "percent-encoding", - "pin-project-lite", - "serde", - "serde_json", - "serde_urlencoded", - "sync_wrapper", - "tokio", - "tokio-util", - "tower", - "tower-http", - "tower-service", - "url", - "wasm-bindgen", - "wasm-bindgen-futures", - "wasm-streams", - "web-sys", + "base64 0.22.1", + "bytes", + "futures-core", + "futures-util", + "http 1.3.1", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "js-sys", + "log", + "mime_guess", + "percent-encoding", + "pin-project-lite", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-util", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams", + "web-sys", ] [[package]] @@ -1743,14 +2118,21 @@ name = "rgb" version = "0.8.52" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c6a884d2998352bb4daf0183589aec883f16a6da1f4dde84d8e2e9a5409a1ce" -dependencies = ["bytemuck"] +dependencies = [ + "bytemuck", +] [[package]] name = "ron" version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b91f7eff05f748767f183df4320a63d6936e9c6107d97c9e6bdd9784f4289c94" -dependencies = ["base64 0.21.7", "bitflags", "serde", "serde_derive"] +dependencies = [ + "base64 0.21.7", + "bitflags", + "serde", + "serde_derive", +] [[package]] name = "rsa" @@ -1758,18 +2140,18 @@ version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78928ac1ed176a5ca1d17e578a1825f3d81ca54cf41053a592584b020cfd691b" dependencies = [ - "const-oid", - "digest", - "num-bigint-dig", - "num-integer", - "num-traits", - "pkcs1", - "pkcs8", - "rand_core 0.6.4", - "signature", - "spki", - "subtle", - "zeroize", + "const-oid", + "digest", + "num-bigint-dig", + "num-integer", + "num-traits", + "pkcs1", + "pkcs8", + "rand_core 0.6.4", + "signature", + "spki", + "subtle", + "zeroize", ] [[package]] @@ -1777,7 +2159,10 @@ name = "rust-ini" version = "0.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "796e8d2b6696392a43bea58116b667fb4c29727dc5abd27d6acf338bb4f688c7" -dependencies = ["cfg-if", "ordered-multimap"] +dependencies = [ + "cfg-if", + "ordered-multimap", +] [[package]] name = "rustc-demangle" @@ -1790,7 +2175,9 @@ name = "rustc_version" version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" -dependencies = ["semver"] +dependencies = [ + "semver", +] [[package]] name = "rustversion" @@ -1809,14 +2196,24 @@ name = "schemars" version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f" -dependencies = ["dyn-clone", "ref-cast", "serde", "serde_json"] +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] [[package]] name = "schemars" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "82d20c4491bc164fa2f6c5d44565947a52ad80b9505d8e36f8d54c27c739fcd0" -dependencies = ["dyn-clone", "ref-cast", "serde", "serde_json"] +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] [[package]] name = "scopeguard" @@ -1835,70 +2232,107 @@ name = "serde" version = "1.0.225" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fd6c24dee235d0da097043389623fb913daddf92c76e9f5a1db88607a0bcbd1d" -dependencies = ["serde_core", "serde_derive"] +dependencies = [ + "serde_core", + "serde_derive", +] [[package]] name = "serde-untagged" version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f9faf48a4a2d2693be24c6289dbe26552776eb7737074e6722891fadbe6c5058" -dependencies = ["erased-serde", "serde", "serde_core", "typeid"] +dependencies = [ + "erased-serde", + "serde", + "serde_core", + "typeid", +] [[package]] name = "serde-wasm-bindgen" version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8302e169f0eddcc139c70f139d19d6467353af16f9fce27e8c30158036a1e16b" -dependencies = ["js-sys", "serde", "wasm-bindgen"] +dependencies = [ + "js-sys", + "serde", + "wasm-bindgen", +] [[package]] name = "serde_core" version = "1.0.225" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "659356f9a0cb1e529b24c01e43ad2bdf520ec4ceaf83047b83ddcc2251f96383" -dependencies = ["serde_derive"] +dependencies = [ + "serde_derive", +] [[package]] name = "serde_derive" version = "1.0.225" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ea936adf78b1f766949a4977b91d2f5595825bd6ec079aa9543ad2685fc4516" -dependencies = ["proc-macro2", "quote", "syn"] +dependencies = [ + "proc-macro2", + "quote", + "syn", +] [[package]] name = "serde_json" version = "1.0.145" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" -dependencies = ["itoa", "memchr", "ryu", "serde", "serde_core"] +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", + "serde_core", +] [[package]] name = "serde_path_to_error" version = "0.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457" -dependencies = ["itoa", "serde", "serde_core"] +dependencies = [ + "itoa", + "serde", + "serde_core", +] [[package]] name = "serde_spanned" version = "0.6.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" -dependencies = ["serde"] +dependencies = [ + "serde", +] [[package]] name = "serde_spanned" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2789234a13a53fc4be1b51ea1bab45a3c338bdb884862a257d10e5a74ae009e6" -dependencies = ["serde_core"] +dependencies = [ + "serde_core", +] [[package]] name = "serde_urlencoded" version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" -dependencies = ["form_urlencoded", "itoa", "ryu", "serde"] +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] [[package]] name = "serde_with" @@ -1906,18 +2340,18 @@ version = "3.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2c45cd61fefa9db6f254525d46e392b852e0e61d9a1fd36e5bd183450a556d5" dependencies = [ - "base64 0.22.1", - "chrono", - "hex", - "indexmap 1.9.3", - "indexmap 2.11.3", - "schemars 0.9.0", - "schemars 1.0.4", - "serde", - "serde_derive", - "serde_json", - "serde_with_macros", - "time", + "base64 0.22.1", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.11.3", + "schemars 0.9.0", + "schemars 1.0.4", + "serde", + "serde_derive", + "serde_json", + "serde_with_macros", + "time", ] [[package]] @@ -1925,21 +2359,34 @@ name = "serde_with_macros" version = "3.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "de90945e6565ce0d9a25098082ed4ee4002e047cb59892c318d66821e14bb30f" -dependencies = ["darling", "proc-macro2", "quote", "syn"] +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn", +] [[package]] name = "sha1" version = "0.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" -dependencies = ["cfg-if", "cpufeatures", "digest"] +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] [[package]] name = "sha2" version = "0.10.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" -dependencies = ["cfg-if", "cpufeatures", "digest"] +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] [[package]] name = "shlex" @@ -1952,14 +2399,19 @@ name = "signal-hook-registry" version = "1.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b2a4719bff48cee6b39d12c020eeb490953ad2443b7055bd0b21fca26bd8c28b" -dependencies = ["libc"] +dependencies = [ + "libc", +] [[package]] name = "signature" version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" -dependencies = ["digest", "rand_core 0.6.4"] +dependencies = [ + "digest", + "rand_core 0.6.4", +] [[package]] name = "slab" @@ -1972,35 +2424,48 @@ name = "smallvec" version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" -dependencies = ["serde"] +dependencies = [ + "serde", +] [[package]] name = "socket2" version = "0.5.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" -dependencies = ["libc", "windows-sys 0.52.0"] +dependencies = [ + "libc", + "windows-sys 0.52.0", +] [[package]] name = "socket2" version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "233504af464074f9d066d7b5416c5f9b894a5862a6506e306f7b816cdd6f1807" -dependencies = ["libc", "windows-sys 0.59.0"] +dependencies = [ + "libc", + "windows-sys 0.59.0", +] [[package]] name = "spin" version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" -dependencies = ["lock_api"] +dependencies = [ + "lock_api", +] [[package]] name = "spki" version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" -dependencies = ["base64ct", "der"] +dependencies = [ + "base64ct", + "der", +] [[package]] name = "sqlx" @@ -2008,11 +2473,11 @@ version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fefb893899429669dcdd979aff487bd78f4064e5e7907e4269081e0ef7d97dc" dependencies = [ - "sqlx-core", - "sqlx-macros", - "sqlx-mysql", - "sqlx-postgres", - "sqlx-sqlite", + "sqlx-core", + "sqlx-macros", + "sqlx-mysql", + "sqlx-postgres", + "sqlx-sqlite", ] [[package]] @@ -2021,30 +2486,30 @@ version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ee6798b1838b6a0f69c007c133b8df5866302197e404e8b6ee8ed3e3a5e68dc6" dependencies = [ - "base64 0.22.1", - "bytes", - "crc", - "crossbeam-queue", - "either", - "event-listener", - "futures-core", - "futures-intrusive", - "futures-io", - "futures-util", - "hashbrown 0.15.5", - "hashlink", - "indexmap 2.11.3", - "log", - "memchr", - "once_cell", - "percent-encoding", - "serde", - "serde_json", - "sha2", - "smallvec", - "thiserror", - "tracing", - "url", + "base64 0.22.1", + "bytes", + "crc", + "crossbeam-queue", + "either", + "event-listener", + "futures-core", + "futures-intrusive", + "futures-io", + "futures-util", + "hashbrown 0.15.5", + "hashlink", + "indexmap 2.11.3", + "log", + "memchr", + "once_cell", + "percent-encoding", + "serde", + "serde_json", + "sha2", + "smallvec", + "thiserror", + "tracing", + "url", ] [[package]] @@ -2052,7 +2517,13 @@ name = "sqlx-macros" version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2d452988ccaacfbf5e0bdbc348fb91d7c8af5bee192173ac3636b5fb6e6715d" -dependencies = ["proc-macro2", "quote", "sqlx-core", "sqlx-macros-core", "syn"] +dependencies = [ + "proc-macro2", + "quote", + "sqlx-core", + "sqlx-macros-core", + "syn", +] [[package]] name = "sqlx-macros-core" @@ -2060,19 +2531,19 @@ version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "19a9c1841124ac5a61741f96e1d9e2ec77424bf323962dd894bdb93f37d5219b" dependencies = [ - "dotenvy", - "either", - "heck", - "hex", - "once_cell", - "proc-macro2", - "quote", - "serde", - "serde_json", - "sha2", - "sqlx-core", - "syn", - "url", + "dotenvy", + "either", + "heck", + "hex", + "once_cell", + "proc-macro2", + "quote", + "serde", + "serde_json", + "sha2", + "sqlx-core", + "syn", + "url", ] [[package]] @@ -2081,39 +2552,39 @@ version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aa003f0038df784eb8fecbbac13affe3da23b45194bd57dba231c8f48199c526" dependencies = [ - "atoi", - "base64 0.22.1", - "bitflags", - "byteorder", - "bytes", - "crc", - "digest", - "dotenvy", - "either", - "futures-channel", - "futures-core", - "futures-io", - "futures-util", - "generic-array", - "hex", - "hkdf", - "hmac", - "itoa", - "log", - "md-5", - "memchr", - "once_cell", - "percent-encoding", - "rand 0.8.5", - "rsa", - "sha1", - "sha2", - "smallvec", - "sqlx-core", - "stringprep", - "thiserror", - "tracing", - "whoami", + "atoi", + "base64 0.22.1", + "bitflags", + "byteorder", + "bytes", + "crc", + "digest", + "dotenvy", + "either", + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "generic-array", + "hex", + "hkdf", + "hmac", + "itoa", + "log", + "md-5", + "memchr", + "once_cell", + "percent-encoding", + "rand 0.8.5", + "rsa", + "sha1", + "sha2", + "smallvec", + "sqlx-core", + "stringprep", + "thiserror", + "tracing", + "whoami", ] [[package]] @@ -2122,35 +2593,35 @@ version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "db58fcd5a53cf07c184b154801ff91347e4c30d17a3562a635ff028ad5deda46" dependencies = [ - "atoi", - "base64 0.22.1", - "bitflags", - "byteorder", - "crc", - "dotenvy", - "etcetera", - "futures-channel", - "futures-core", - "futures-util", - "hex", - "hkdf", - "hmac", - "home", - "itoa", - "log", - "md-5", - "memchr", - "once_cell", - "rand 0.8.5", - "serde", - "serde_json", - "sha2", - "smallvec", - "sqlx-core", - "stringprep", - "thiserror", - "tracing", - "whoami", + "atoi", + "base64 0.22.1", + "bitflags", + "byteorder", + "crc", + "dotenvy", + "etcetera", + "futures-channel", + "futures-core", + "futures-util", + "hex", + "hkdf", + "hmac", + "home", + "itoa", + "log", + "md-5", + "memchr", + "once_cell", + "rand 0.8.5", + "serde", + "serde_json", + "sha2", + "smallvec", + "sqlx-core", + "stringprep", + "thiserror", + "tracing", + "whoami", ] [[package]] @@ -2159,21 +2630,21 @@ version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2d12fe70b2c1b4401038055f90f151b78208de1f9f89a7dbfd41587a10c3eea" dependencies = [ - "atoi", - "flume", - "futures-channel", - "futures-core", - "futures-executor", - "futures-intrusive", - "futures-util", - "libsqlite3-sys", - "log", - "percent-encoding", - "serde_urlencoded", - "sqlx-core", - "thiserror", - "tracing", - "url", + "atoi", + "flume", + "futures-channel", + "futures-core", + "futures-executor", + "futures-intrusive", + "futures-util", + "libsqlite3-sys", + "log", + "percent-encoding", + "serde_urlencoded", + "sqlx-core", + "thiserror", + "tracing", + "url", ] [[package]] @@ -2187,14 +2658,24 @@ name = "stacker" version = "0.1.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cddb07e32ddb770749da91081d8d0ac3a16f1a569a18b20348cd371f5dead06b" -dependencies = ["cc", "cfg-if", "libc", "psm", "windows-sys 0.59.0"] +dependencies = [ + "cc", + "cfg-if", + "libc", + "psm", + "windows-sys 0.59.0", +] [[package]] name = "stringprep" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7b4df3d392d81bd458a8a621b8bffbd2302a12ffe288a9d931670948749463b1" -dependencies = ["unicode-bidi", "unicode-normalization", "unicode-properties"] +dependencies = [ + "unicode-bidi", + "unicode-normalization", + "unicode-properties", +] [[package]] name = "strsim" @@ -2213,21 +2694,31 @@ name = "syn" version = "2.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6" -dependencies = ["proc-macro2", "quote", "unicode-ident"] +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] [[package]] name = "sync_wrapper" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" -dependencies = ["futures-core"] +dependencies = [ + "futures-core", +] [[package]] name = "synstructure" version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" -dependencies = ["proc-macro2", "quote", "syn"] +dependencies = [ + "proc-macro2", + "quote", + "syn", +] [[package]] name = "take_mut" @@ -2253,23 +2744,23 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fb72a0fd8e65f7c9279d164e9b00661e6685f74c0ba63e7f17b30662d5aed21b" dependencies = [ - "base64 0.22.1", - "ed25519-dalek", - "hex", - "hmac-sha256", - "js-sys", - "masterror 0.3.5", - "once_cell", - "percent-encoding", - "serde", - "serde-wasm-bindgen", - "serde_json", - "serde_urlencoded", - "thiserror", - "toml 0.8.23", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", + "base64 0.22.1", + "ed25519-dalek", + "hex", + "hmac-sha256", + "js-sys", + "masterror 0.3.5", + "once_cell", + "percent-encoding", + "serde", + "serde-wasm-bindgen", + "serde_json", + "serde_urlencoded", + "thiserror", + "toml 0.8.23", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", ] [[package]] @@ -2278,30 +2769,30 @@ version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f7a34ca8e971fa892e633858c07547fe138ef4a02e4a4eaa1d35e517d6e0bc4" dependencies = [ - "bitflags", - "bytes", - "chrono", - "derive_more 1.0.0", - "either", - "futures", - "log", - "mime", - "once_cell", - "pin-project", - "rc-box", - "reqwest", - "rgb", - "serde", - "serde_json", - "serde_with", - "stacker", - "take_mut", - "takecell", - "thiserror", - "tokio", - "tokio-util", - "url", - "uuid", + "bitflags", + "bytes", + "chrono", + "derive_more 1.0.0", + "either", + "futures", + "log", + "mime", + "once_cell", + "pin-project", + "rc-box", + "reqwest", + "rgb", + "serde", + "serde_json", + "serde_with", + "stacker", + "take_mut", + "takecell", + "thiserror", + "tokio", + "tokio-util", + "url", + "uuid", ] [[package]] @@ -2309,21 +2800,29 @@ name = "termcolor" version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" -dependencies = ["winapi-util"] +dependencies = [ + "winapi-util", +] [[package]] name = "thiserror" version = "2.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3467d614147380f2e4e374161426ff399c91084acd2363eaf549172b3d5e60c0" -dependencies = ["thiserror-impl"] +dependencies = [ + "thiserror-impl", +] [[package]] name = "thiserror-impl" version = "2.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c5e1be1c48b9172ee610da68fd9cd2770e7a4056cb3fc98710ee6906f0c7960" -dependencies = ["proc-macro2", "quote", "syn"] +dependencies = [ + "proc-macro2", + "quote", + "syn", +] [[package]] name = "time" @@ -2331,12 +2830,12 @@ version = "0.3.43" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "83bde6f1ec10e72d583d91623c939f623002284ef622b87de38cfd546cbf2031" dependencies = [ - "deranged", - "num-conv", - "powerfmt", - "serde", - "time-core", - "time-macros", + "deranged", + "num-conv", + "powerfmt", + "serde", + "time-core", + "time-macros", ] [[package]] @@ -2350,28 +2849,38 @@ name = "time-macros" version = "0.2.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "30cfb0125f12d9c277f35663a0a33f8c30190f4e4574868a330595412d34ebf3" -dependencies = ["num-conv", "time-core"] +dependencies = [ + "num-conv", + "time-core", +] [[package]] name = "tiny-keccak" version = "2.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" -dependencies = ["crunchy"] +dependencies = [ + "crunchy", +] [[package]] name = "tinystr" version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b" -dependencies = ["displaydoc", "zerovec"] +dependencies = [ + "displaydoc", + "zerovec", +] [[package]] name = "tinyvec" version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" -dependencies = ["tinyvec_macros"] +dependencies = [ + "tinyvec_macros", +] [[package]] name = "tinyvec_macros" @@ -2385,18 +2894,18 @@ version = "1.47.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "89e49afdadebb872d3145a5638b59eb0691ea23e46ca484037cfab3b76b95038" dependencies = [ - "backtrace", - "bytes", - "io-uring", - "libc", - "mio", - "parking_lot", - "pin-project-lite", - "signal-hook-registry", - "slab", - "socket2 0.6.0", - "tokio-macros", - "windows-sys 0.59.0", + "backtrace", + "bytes", + "io-uring", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "slab", + "socket2 0.6.0", + "tokio-macros", + "windows-sys 0.59.0", ] [[package]] @@ -2404,7 +2913,11 @@ name = "tokio-macros" version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" -dependencies = ["proc-macro2", "quote", "syn"] +dependencies = [ + "proc-macro2", + "quote", + "syn", +] [[package]] name = "tokio-util" @@ -2412,11 +2925,11 @@ version = "0.7.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "14307c986784f72ef81c89db7d9e28d6ac26d16213b109ea501696195e6e3ce5" dependencies = [ - "bytes", - "futures-core", - "futures-sink", - "pin-project-lite", - "tokio", + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", ] [[package]] @@ -2425,10 +2938,10 @@ version = "0.8.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" dependencies = [ - "serde", - "serde_spanned 0.6.9", - "toml_datetime 0.6.11", - "toml_edit", + "serde", + "serde_spanned 0.6.9", + "toml_datetime 0.6.11", + "toml_edit", ] [[package]] @@ -2437,14 +2950,13 @@ version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ae2a4cf385da23d1d53bc15cdfa5c2109e93d8d362393c801e87da2f72f0e201" dependencies = [ - "indexmap 2.11.3", - "serde_core", - "serde_spanned 1.0.1", - "toml_datetime 0.7.1", - "serde", - "toml_parser", - "toml_writer", - "winnow", + "indexmap 2.11.3", + "serde_core", + "serde_spanned 1.0.1", + "toml_datetime 0.7.1", + "toml_parser", + "toml_writer", + "winnow", ] [[package]] @@ -2452,14 +2964,18 @@ name = "toml_datetime" version = "0.6.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" -dependencies = ["serde"] +dependencies = [ + "serde", +] [[package]] name = "toml_datetime" version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a197c0ec7d131bfc6f7e82c8442ba1595aeab35da7adbf05b6b73cd06a16b6be" -dependencies = ["serde_core"] +dependencies = [ + "serde_core", +] [[package]] name = "toml_edit" @@ -2467,12 +2983,12 @@ version = "0.22.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" dependencies = [ - "indexmap 2.11.3", - "serde", - "serde_spanned 0.6.9", - "toml_datetime 0.6.11", - "toml_write", - "winnow", + "indexmap 2.11.3", + "serde", + "serde_spanned 0.6.9", + "toml_datetime 0.6.11", + "toml_write", + "winnow", ] [[package]] @@ -2480,7 +2996,9 @@ name = "toml_parser" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b551886f449aa90d4fe2bdaa9f4a2577ad2dde302c61ecf262d80b116db95c10" -dependencies = ["winnow"] +dependencies = [ + "winnow", +] [[package]] name = "toml_write" @@ -2500,13 +3018,13 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" dependencies = [ - "futures-core", - "futures-util", - "pin-project-lite", - "sync_wrapper", - "tokio", - "tower-layer", - "tower-service", + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", ] [[package]] @@ -2515,16 +3033,16 @@ version = "0.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" dependencies = [ - "bitflags", - "bytes", - "futures-util", - "http 1.3.1", - "http-body", - "iri-string", - "pin-project-lite", - "tower", - "tower-layer", - "tower-service", + "bitflags", + "bytes", + "futures-util", + "http 1.3.1", + "http-body", + "iri-string", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", ] [[package]] @@ -2544,21 +3062,32 @@ name = "tracing" version = "0.1.41" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" -dependencies = ["log", "pin-project-lite", "tracing-attributes", "tracing-core"] +dependencies = [ + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] [[package]] name = "tracing-attributes" version = "0.1.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" -dependencies = ["proc-macro2", "quote", "syn"] +dependencies = [ + "proc-macro2", + "quote", + "syn", +] [[package]] name = "tracing-core" version = "0.1.34" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" -dependencies = ["once_cell"] +dependencies = [ + "once_cell", +] [[package]] name = "try-lock" @@ -2572,13 +3101,13 @@ version = "1.0.111" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ded9fdb81f30a5708920310bfcd9ea7482ff9cba5f54601f7a19a877d5c2392" dependencies = [ - "glob", - "serde", - "serde_derive", - "serde_json", - "target-triple", - "termcolor", - "toml 0.9.5", + "glob", + "serde", + "serde_derive", + "serde_json", + "target-triple", + "termcolor", + "toml 0.9.6", ] [[package]] @@ -2622,7 +3151,9 @@ name = "unicode-normalization" version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5033c97c4262335cded6d6fc3e5c18ab755e1a3dc96376350f3d8e9f009ad956" -dependencies = ["tinyvec"] +dependencies = [ + "tinyvec", +] [[package]] name = "unicode-properties" @@ -2647,7 +3178,12 @@ name = "url" version = "2.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" -dependencies = ["form_urlencoded", "idna", "percent-encoding", "serde"] +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] [[package]] name = "utf8_iter" @@ -2660,21 +3196,34 @@ name = "utoipa" version = "5.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2fcc29c80c21c31608227e0912b2d7fddba57ad76b606890627ba8ee7964e993" -dependencies = ["indexmap 2.11.3", "serde", "serde_json", "utoipa-gen"] +dependencies = [ + "indexmap 2.11.3", + "serde", + "serde_json", + "utoipa-gen", +] [[package]] name = "utoipa-gen" version = "5.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6d79d08d92ab8af4c5e8a6da20c47ae3f61a0f1dabc1997cdf2d082b757ca08b" -dependencies = ["proc-macro2", "quote", "syn"] +dependencies = [ + "proc-macro2", + "quote", + "syn", +] [[package]] name = "uuid" version = "1.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2f87b8aa10b915a06587d0dec516c282ff295b475d94abf425d62b57710070a2" -dependencies = ["getrandom 0.3.3", "js-sys", "wasm-bindgen"] +dependencies = [ + "getrandom 0.3.3", + "js-sys", + "wasm-bindgen", +] [[package]] name = "validator" @@ -2682,14 +3231,14 @@ version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "43fb22e1a008ece370ce08a3e9e4447a910e92621bb49b85d6e48a45397e7cfa" dependencies = [ - "idna", - "once_cell", - "regex", - "serde", - "serde_derive", - "serde_json", - "url", - "validator_derive", + "idna", + "once_cell", + "regex", + "serde", + "serde_derive", + "serde_json", + "url", + "validator_derive", ] [[package]] @@ -2698,12 +3247,12 @@ version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b7df16e474ef958526d1205f6dda359fdfab79d9aa6d54bafcb92dcd07673dca" dependencies = [ - "darling", - "once_cell", - "proc-macro-error2", - "proc-macro2", - "quote", - "syn", + "darling", + "once_cell", + "proc-macro-error2", + "proc-macro2", + "quote", + "syn", ] [[package]] @@ -2723,7 +3272,9 @@ name = "want" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" -dependencies = ["try-lock"] +dependencies = [ + "try-lock", +] [[package]] name = "wasi" @@ -2736,14 +3287,18 @@ name = "wasi" version = "0.14.7+wasi-0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "883478de20367e224c0090af9cf5f9fa85bed63a95c1abf3afc5c083ebc06e8c" -dependencies = ["wasip2"] +dependencies = [ + "wasip2", +] [[package]] name = "wasip2" version = "1.0.1+wasi-0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" -dependencies = ["wit-bindgen"] +dependencies = [ + "wit-bindgen", +] [[package]] name = "wasite" @@ -2757,11 +3312,11 @@ version = "0.2.102" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ad224d2776649cfb4f4471124f8176e54c1cca67a88108e30a0cd98b90e7ad3" dependencies = [ - "cfg-if", - "once_cell", - "rustversion", - "wasm-bindgen-macro", - "wasm-bindgen-shared", + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", ] [[package]] @@ -2770,12 +3325,12 @@ version = "0.2.102" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3a1364104bdcd3c03f22b16a3b1c9620891469f5e9f09bc38b2db121e593e732" dependencies = [ - "bumpalo", - "log", - "proc-macro2", - "quote", - "syn", - "wasm-bindgen-shared", + "bumpalo", + "log", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", ] [[package]] @@ -2783,14 +3338,23 @@ name = "wasm-bindgen-futures" version = "0.4.52" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c0a08ecf5d99d5604a6666a70b3cde6ab7cc6142f5e641a8ef48fc744ce8854" -dependencies = ["cfg-if", "js-sys", "once_cell", "wasm-bindgen", "web-sys"] +dependencies = [ + "cfg-if", + "js-sys", + "once_cell", + "wasm-bindgen", + "web-sys", +] [[package]] name = "wasm-bindgen-macro" version = "0.2.102" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0d7ab4ca3e367bb1ed84ddbd83cc6e41e115f8337ed047239578210214e36c76" -dependencies = ["quote", "wasm-bindgen-macro-support"] +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] [[package]] name = "wasm-bindgen-macro-support" @@ -2798,11 +3362,11 @@ version = "0.2.102" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4a518014843a19e2dbbd0ed5dfb6b99b23fb886b14e6192a00803a3e14c552b0" dependencies = [ - "proc-macro2", - "quote", - "syn", - "wasm-bindgen-backend", - "wasm-bindgen-shared", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", ] [[package]] @@ -2810,7 +3374,9 @@ name = "wasm-bindgen-shared" version = "0.2.102" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "255eb0aa4cc2eea3662a00c2bbd66e93911b7361d5e0fcd62385acfd7e15dcee" -dependencies = ["unicode-ident"] +dependencies = [ + "unicode-ident", +] [[package]] name = "wasm-streams" @@ -2818,11 +3384,11 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65" dependencies = [ - "futures-util", - "js-sys", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", ] [[package]] @@ -2830,21 +3396,29 @@ name = "web-sys" version = "0.3.79" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "50462a022f46851b81d5441d1a6f5bac0b21a1d72d64bd4906fbdd4bf7230ec7" -dependencies = ["js-sys", "wasm-bindgen"] +dependencies = [ + "js-sys", + "wasm-bindgen", +] [[package]] name = "whoami" version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5d4a4db5077702ca3015d3d02d74974948aba2ad9e12ab7df718ee64ccd7e97d" -dependencies = ["libredox", "wasite"] +dependencies = [ + "libredox", + "wasite", +] [[package]] name = "winapi-util" version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" -dependencies = ["windows-sys 0.59.0"] +dependencies = [ + "windows-sys 0.59.0", +] [[package]] name = "windows-core" @@ -2852,11 +3426,11 @@ version = "0.62.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57fe7168f7de578d2d8a05b07fd61870d2e73b4020e9f49aa00da8471723497c" dependencies = [ - "windows-implement", - "windows-interface", - "windows-link", - "windows-result", - "windows-strings", + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", ] [[package]] @@ -2864,14 +3438,22 @@ name = "windows-implement" version = "0.60.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" -dependencies = ["proc-macro2", "quote", "syn"] +dependencies = [ + "proc-macro2", + "quote", + "syn", +] [[package]] name = "windows-interface" version = "0.59.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" -dependencies = ["proc-macro2", "quote", "syn"] +dependencies = [ + "proc-macro2", + "quote", + "syn", +] [[package]] name = "windows-link" @@ -2884,35 +3466,45 @@ name = "windows-result" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7084dcc306f89883455a206237404d3eaf961e5bd7e0f312f7c91f57eb44167f" -dependencies = ["windows-link"] +dependencies = [ + "windows-link", +] [[package]] name = "windows-strings" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7218c655a553b0bed4426cf54b20d7ba363ef543b52d515b3e48d7fd55318dda" -dependencies = ["windows-link"] +dependencies = [ + "windows-link", +] [[package]] name = "windows-sys" version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" -dependencies = ["windows-targets 0.48.5"] +dependencies = [ + "windows-targets 0.48.5", +] [[package]] name = "windows-sys" version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" -dependencies = ["windows-targets 0.52.6"] +dependencies = [ + "windows-targets 0.52.6", +] [[package]] name = "windows-sys" version = "0.59.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" -dependencies = ["windows-targets 0.52.6"] +dependencies = [ + "windows-targets 0.52.6", +] [[package]] name = "windows-targets" @@ -2920,13 +3512,13 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" dependencies = [ - "windows_aarch64_gnullvm 0.48.5", - "windows_aarch64_msvc 0.48.5", - "windows_i686_gnu 0.48.5", - "windows_i686_msvc 0.48.5", - "windows_x86_64_gnu 0.48.5", - "windows_x86_64_gnullvm 0.48.5", - "windows_x86_64_msvc 0.48.5", + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", ] [[package]] @@ -2935,14 +3527,14 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ - "windows_aarch64_gnullvm 0.52.6", - "windows_aarch64_msvc 0.52.6", - "windows_i686_gnu 0.52.6", - "windows_i686_gnullvm", - "windows_i686_msvc 0.52.6", - "windows_x86_64_gnu 0.52.6", - "windows_x86_64_gnullvm 0.52.6", - "windows_x86_64_msvc 0.52.6", + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", ] [[package]] @@ -3040,7 +3632,9 @@ name = "winnow" version = "0.7.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf" -dependencies = ["memchr"] +dependencies = [ + "memchr", +] [[package]] name = "wit-bindgen" @@ -3059,49 +3653,76 @@ name = "yaml-rust2" version = "0.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2462ea039c445496d8793d052e13787f2b90e750b833afee748e601c17621ed9" -dependencies = ["arraydeque", "encoding_rs", "hashlink"] +dependencies = [ + "arraydeque", + "encoding_rs", + "hashlink", +] [[package]] name = "yoke" version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc" -dependencies = ["serde", "stable_deref_trait", "yoke-derive", "zerofrom"] +dependencies = [ + "serde", + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] [[package]] name = "yoke-derive" version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" -dependencies = ["proc-macro2", "quote", "syn", "synstructure"] +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] [[package]] name = "zerocopy" version = "0.8.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0894878a5fa3edfd6da3f88c4805f4c8558e2b996227a3d864f47fe11e38282c" -dependencies = ["zerocopy-derive"] +dependencies = [ + "zerocopy-derive", +] [[package]] name = "zerocopy-derive" version = "0.8.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831" -dependencies = ["proc-macro2", "quote", "syn"] +dependencies = [ + "proc-macro2", + "quote", + "syn", +] [[package]] name = "zerofrom" version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" -dependencies = ["zerofrom-derive"] +dependencies = [ + "zerofrom-derive", +] [[package]] name = "zerofrom-derive" version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" -dependencies = ["proc-macro2", "quote", "syn", "synstructure"] +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] [[package]] name = "zeroize" @@ -3114,18 +3735,30 @@ name = "zerotrie" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595" -dependencies = ["displaydoc", "yoke", "zerofrom"] +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] [[package]] name = "zerovec" version = "0.11.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e7aa2bd55086f1ab526693ecbe444205da57e25f4489879da80635a46d90e73b" -dependencies = ["yoke", "zerofrom", "zerovec-derive"] +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] [[package]] name = "zerovec-derive" version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" -dependencies = ["proc-macro2", "quote", "syn"] +dependencies = [ + "proc-macro2", + "quote", + "syn", +] From 9b269d96b7c5aef44bb56e259b576be9d487677c Mon Sep 17 00:00:00 2001 From: RAprogramm Date: Wed, 17 Sep 2025 11:29:22 +0700 Subject: [PATCH 07/25] test readme 2 --- .github/workflows/reusable-ci.yml | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/.github/workflows/reusable-ci.yml b/.github/workflows/reusable-ci.yml index f8faeb2..6cdf2f9 100644 --- a/.github/workflows/reusable-ci.yml +++ b/.github/workflows/reusable-ci.yml @@ -13,12 +13,12 @@ jobs: env: CARGO_LOCKED: "true" # don't mutate Cargo.lock during CI CARGO_TERM_COLOR: always + steps: - uses: actions/checkout@v4 with: fetch-depth: 0 - # Read MSRV (rust-version) from Cargo.toml - name: Read MSRV from Cargo.toml id: msrv shell: bash @@ -36,14 +36,12 @@ jobs: echo "msrv=${RV}" >> "$GITHUB_OUTPUT" echo "Using MSRV: $RV" - # Install MSRV for clippy/tests/package - name: Install Rust (${{ steps.msrv.outputs.msrv }}) uses: dtolnay/rust-toolchain@v1 with: toolchain: ${{ steps.msrv.outputs.msrv }} components: clippy - # Pin nightly for rustfmt because unstable_features = true in .rustfmt.toml - name: Install nightly rustfmt uses: dtolnay/rust-toolchain@v1 with: @@ -55,7 +53,6 @@ jobs: with: save-if: ${{ github.ref == 'refs/heads/main' }} - # Ensure Cargo.lock is present when CARGO_LOCKED=1 - name: Verify lockfile is committed shell: bash run: | @@ -88,6 +85,16 @@ jobs: cargo +${{ steps.msrv.outputs.msrv }} test --workspace --no-fail-fast fi + # NEW: если какой-то умник всё-таки трогает README в ходе тестов — откатываем + - name: Reset generated files (README) + shell: bash + run: | + set -euo pipefail + if ! git diff --quiet -- README.md; then + echo "::warning:: README.md was modified during build/tests. Resetting to HEAD." + git restore --source=HEAD --worktree -- README.md || git checkout -- README.md + fi + - name: Ensure tree is clean before package shell: bash run: | @@ -100,3 +107,4 @@ jobs: - name: Package (dry-run) run: cargo +${{ steps.msrv.outputs.msrv }} package --locked + From ae6476afd0f6c68052cbd1942d40355324412f03 Mon Sep 17 00:00:00 2001 From: RA <70325462+RAprogramm@users.noreply.github.com> Date: Wed, 17 Sep 2025 11:34:09 +0700 Subject: [PATCH 08/25] feat(derive): add transparent error support --- CHANGELOG.md | 4 + README.md | 3 +- README.template.md | 3 +- masterror-derive/src/lib.rs | 590 ++++++++++++------ tests/error_derive.rs | 56 ++ tests/error_derive_from_trybuild.rs | 6 + .../enum_variant_multiple_fields.rs | 7 + .../enum_variant_multiple_fields.stderr | 11 + .../ui/transparent/enum_variant_no_fields.rs | 7 + .../transparent/enum_variant_no_fields.stderr | 11 + .../ui/transparent/struct_multiple_fields.rs | 8 + .../transparent/struct_multiple_fields.stderr | 11 + tests/ui/transparent/struct_no_fields.rs | 5 + tests/ui/transparent/struct_no_fields.stderr | 11 + 14 files changed, 523 insertions(+), 210 deletions(-) create mode 100644 tests/ui/transparent/enum_variant_multiple_fields.rs create mode 100644 tests/ui/transparent/enum_variant_multiple_fields.stderr create mode 100644 tests/ui/transparent/enum_variant_no_fields.rs create mode 100644 tests/ui/transparent/enum_variant_no_fields.stderr create mode 100644 tests/ui/transparent/struct_multiple_fields.rs create mode 100644 tests/ui/transparent/struct_multiple_fields.stderr create mode 100644 tests/ui/transparent/struct_no_fields.rs create mode 100644 tests/ui/transparent/struct_no_fields.stderr diff --git a/CHANGELOG.md b/CHANGELOG.md index c21a1b5..b990fe7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,10 @@ All notable changes to this project will be documented in this file. ## [Unreleased] +### Added +- `#[error(transparent)]` support in the derive macro: validates wrapper shape, + delegates `Display`/`source` to the inner error, and works with `#[from]`. + ## [0.4.0] - 2025-09-15 ### Added - Optional `frontend` feature: diff --git a/README.md b/README.md index ff14d8f..ed1bf7d 100644 --- a/README.md +++ b/README.md @@ -47,7 +47,8 @@ masterror = { version = "0.4.0", default-features = false } - **Opt-in integrations.** Zero default features; you enable what you need. - **Clean wire contract.** `ErrorResponse { status, code, message, details?, retry?, www_authenticate? }`. - **One log at boundary.** Log once with `tracing`. -- **Less boilerplate.** Built-in conversions, compact prelude. +- **Less boilerplate.** Built-in conversions, compact prelude, + derive macro support for transparent wrappers via `#[error(transparent)]`. - **Consistent workspace.** Same error surface across crates.
diff --git a/README.template.md b/README.template.md index 9342f58..aa69330 100644 --- a/README.template.md +++ b/README.template.md @@ -44,7 +44,8 @@ masterror = { version = "{{CRATE_VERSION}}", default-features = false } - **Opt-in integrations.** Zero default features; you enable what you need. - **Clean wire contract.** `ErrorResponse { status, code, message, details?, retry?, www_authenticate? }`. - **One log at boundary.** Log once with `tracing`. -- **Less boilerplate.** Built-in conversions, compact prelude. +- **Less boilerplate.** Built-in conversions, compact prelude, + derive macro support for transparent wrappers via `#[error(transparent)]`. - **Consistent workspace.** Same error surface across crates.
diff --git a/masterror-derive/src/lib.rs b/masterror-derive/src/lib.rs index 7a7c3e8..c270ebb 100644 --- a/masterror-derive/src/lib.rs +++ b/masterror-derive/src/lib.rs @@ -5,8 +5,9 @@ //! //! The macro mirrors the essential functionality relied upon by `masterror` and //! consumers of the crate: display strings with named or positional fields, -//! `#[from]` conversions for wrapper types, and a configurable error source via -//! `#[source]` field attributes. +//! `#[from]` conversions for wrapper types, transparent wrappers via +//! `#[error(transparent)]`, and a configurable error source via `#[source]` +//! field attributes. use std::collections::BTreeSet; @@ -15,7 +16,8 @@ use proc_macro2::{Ident, Span, TokenStream as TokenStream2}; use quote::{format_ident, quote}; use syn::{ Attribute, Data, DataEnum, DataStruct, DeriveInput, Field, Fields, GenericArgument, Generics, - LitStr, Member, Meta, PathArguments, Type, spanned::Spanned + Ident as SynIdent, LitStr, Member, Meta, PathArguments, Type, parse::ParseStream, + spanned::Spanned }; /// Derive [`std::error::Error`] and [`core::fmt::Display`] for structs and @@ -57,7 +59,7 @@ fn derive_error_impl(input: DeriveInput) -> syn::Result { let fields = parse_fields(&data)?; let display_attr = parse_display_attr(&input.attrs)?; display_impl = build_struct_display(&ident, &generics, &fields, &display_attr)?; - error_impl = build_struct_error(&ident, &generics, &fields)?; + error_impl = build_struct_error(&ident, &generics, &fields, &display_attr)?; from_impls = build_struct_from_impl(&ident, &generics, &fields)?; } Data::Enum(data) => { @@ -133,6 +135,17 @@ impl FieldAttributes { } } +#[derive(Clone)] +enum DisplayAttribute { + Format(LitStr), + Transparent(TransparentAttr) +} + +#[derive(Clone, Copy)] +struct TransparentAttr { + span: Span +} + #[derive(Clone, Copy)] enum SourceKind { Direct { needs_deref: bool }, @@ -154,7 +167,7 @@ struct ParsedFields { struct VariantInfo { ident: Ident, fields: ParsedFields, - display: LitStr + display: DisplayAttribute } struct FromFieldInfo<'a> { @@ -269,7 +282,7 @@ fn parse_fields_internal(fields: &Fields) -> syn::Result { } } -fn parse_display_attr(attrs: &[Attribute]) -> syn::Result { +fn parse_display_attr(attrs: &[Attribute]) -> syn::Result { let mut result = None; for attr in attrs.iter().filter(|attr| attr.path().is_ident("error")) { if result.is_some() { @@ -280,8 +293,8 @@ fn parse_display_attr(attrs: &[Attribute]) -> syn::Result { } match &attr.meta { Meta::List(_) => { - let lit: LitStr = attr.parse_args()?; - result = Some(lit); + let attr_value = parse_error_attribute(attr)?; + result = Some(attr_value); } _ => { return Err(syn::Error::new( @@ -295,6 +308,37 @@ fn parse_display_attr(attrs: &[Attribute]) -> syn::Result { .ok_or_else(|| syn::Error::new(Span::call_site(), r#"missing #[error("...")] attribute"#)) } +fn parse_error_attribute(attr: &Attribute) -> syn::Result { + attr.parse_args_with(|input: ParseStream<'_>| { + if input.is_empty() { + return Err(input.error("expected string literal or `transparent`")); + } + if input.peek(LitStr) { + let lit: LitStr = input.parse()?; + if !input.is_empty() { + return Err(input.error("unexpected tokens after format string")); + } + return Ok(DisplayAttribute::Format(lit)); + } + if input.peek(SynIdent) { + let ident: SynIdent = input.parse()?; + if ident == "transparent" { + if !input.is_empty() { + return Err(input.error("unexpected tokens after `transparent`")); + } + return Ok(DisplayAttribute::Transparent(TransparentAttr { + span: attr.span() + })); + } + return Err(syn::Error::new( + ident.span(), + "unknown #[error] attribute argument" + )); + } + Err(input.error("expected string literal or `transparent`")) + }) +} + fn parse_field_attributes(field: &Field) -> syn::Result { let mut attrs = FieldAttributes::default(); for attr in &field.attrs { @@ -392,6 +436,23 @@ fn find_from_field<'a>( Ok(Some(info)) } +fn ensure_transparent_field<'a>( + fields: &'a ParsedFields, + attr: &TransparentAttr, + context: &str +) -> syn::Result<&'a FieldSpec> { + if fields.fields.len() != 1 { + return Err(syn::Error::new( + attr.span, + format!("using #[error(transparent)] in {context} requires exactly one field") + )); + } + fields + .fields + .first() + .ok_or_else(|| syn::Error::new(attr.span, "invalid transparent field index")) +} + fn detect_source_kind(ty: &Type) -> syn::Result { if let Some(inner) = option_inner_type(ty) { Ok(SourceKind::Optional { @@ -434,47 +495,63 @@ fn build_struct_display( ident: &Ident, generics: &Generics, fields: &ParsedFields, - display: &LitStr + display: &DisplayAttribute ) -> syn::Result { let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); - let RewriteResult { - literal, - positional_indices - } = rewrite_format_string(display, fields.fields.len())?; - - let body = match fields.style { - FieldsStyle::Unit => quote! { - ::core::write!(formatter, #literal) - }, - FieldsStyle::Named => { - let field_idents: Vec<_> = fields - .fields - .iter() - .map(|f| f.ident.clone().expect("named fields must have identifiers")) - .collect(); - let positional_bindings = positional_indices.iter().map(|index| { - let binding_ident = format_ident!("__masterror_{index}"); - let field_ident = field_idents[*index].clone(); - quote! { - #[allow(unused_variables)] - let #binding_ident = &*#field_ident; - } - }); + let body = match display { + DisplayAttribute::Transparent(attr) => { + let context = format!("struct `{ident}`"); + let field = ensure_transparent_field(fields, attr, &context)?; + let member = &field.member; + let binding = format_ident!("__masterror_transparent_inner"); quote! { - let Self { #( ref #field_idents ),* } = *self; - #[allow(unused_variables)] - let _ = (#(&#field_idents),*); - #(#positional_bindings)* - ::core::write!(formatter, #literal) + let #binding = &self.#member; + let _: &(dyn ::std::error::Error + 'static) = #binding; + ::core::fmt::Display::fmt(#binding, formatter) } } - FieldsStyle::Unnamed => { - let bindings: Vec<_> = fields.fields.iter().map(|f| f.binding.clone()).collect(); - quote! { - let Self( #( ref #bindings ),* ) = *self; - #[allow(unused_variables)] - let _ = (#(&#bindings),*); - ::core::write!(formatter, #literal) + DisplayAttribute::Format(literal) => { + let RewriteResult { + literal, + positional_indices + } = rewrite_format_string(literal, fields.fields.len())?; + + match fields.style { + FieldsStyle::Unit => quote! { + ::core::write!(formatter, #literal) + }, + FieldsStyle::Named => { + let field_idents: Vec<_> = fields + .fields + .iter() + .map(|f| f.ident.clone().expect("named fields must have identifiers")) + .collect(); + let positional_bindings = positional_indices.iter().map(|index| { + let binding_ident = format_ident!("__masterror_{index}"); + let field_ident = field_idents[*index].clone(); + quote! { + #[allow(unused_variables)] + let #binding_ident = &*#field_ident; + } + }); + quote! { + let Self { #( ref #field_idents ),* } = *self; + #[allow(unused_variables)] + let _ = (#(&#field_idents),*); + #(#positional_bindings)* + ::core::write!(formatter, #literal) + } + } + FieldsStyle::Unnamed => { + let bindings: Vec<_> = + fields.fields.iter().map(|f| f.binding.clone()).collect(); + quote! { + let Self( #( ref #bindings ),* ) = *self; + #[allow(unused_variables)] + let _ = (#(&#bindings),*); + ::core::write!(formatter, #literal) + } + } } } }; @@ -491,49 +568,64 @@ fn build_struct_display( fn build_struct_error( ident: &Ident, generics: &Generics, - fields: &ParsedFields + fields: &ParsedFields, + display: &DisplayAttribute ) -> syn::Result { let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); - let source_expr = if let Some(source) = fields.source { - let field = fields - .fields - .get(source.index) - .ok_or_else(|| syn::Error::new(Span::call_site(), "invalid source field index"))?; - let member = &field.member; - match source.kind { - SourceKind::Direct { - needs_deref: false - } => quote! { - ::core::option::Option::Some(&self.#member as &(dyn ::std::error::Error + 'static)) - }, - SourceKind::Direct { - needs_deref: true - } => quote! { - ::core::option::Option::Some(self.#member.as_ref() as &(dyn ::std::error::Error + 'static)) - }, - SourceKind::Optional { - needs_deref: false - } => quote! { - self.#member - .as_ref() - .map(|source| source as &(dyn ::std::error::Error + 'static)) - }, - SourceKind::Optional { - needs_deref: true - } => quote! { - self.#member - .as_ref() - .map(|source| source.as_ref() as &(dyn ::std::error::Error + 'static)) + let body = match display { + DisplayAttribute::Transparent(attr) => { + let context = format!("struct `{ident}`"); + let field = ensure_transparent_field(fields, attr, &context)?; + let member = &field.member; + let binding = format_ident!("__masterror_transparent_inner"); + quote! { + let #binding = &self.#member; + let _: &(dyn ::std::error::Error + 'static) = #binding; + ::std::error::Error::source(#binding) + } + } + DisplayAttribute::Format(_) => { + if let Some(source) = fields.source { + let field = fields.fields.get(source.index).ok_or_else(|| { + syn::Error::new(Span::call_site(), "invalid source field index") + })?; + let member = &field.member; + match source.kind { + SourceKind::Direct { + needs_deref: false + } => quote! { + ::core::option::Option::Some(&self.#member as &(dyn ::std::error::Error + 'static)) + }, + SourceKind::Direct { + needs_deref: true + } => quote! { + ::core::option::Option::Some(self.#member.as_ref() as &(dyn ::std::error::Error + 'static)) + }, + SourceKind::Optional { + needs_deref: false + } => quote! { + self.#member + .as_ref() + .map(|source| source as &(dyn ::std::error::Error + 'static)) + }, + SourceKind::Optional { + needs_deref: true + } => quote! { + self.#member + .as_ref() + .map(|source| source.as_ref() as &(dyn ::std::error::Error + 'static)) + } + } + } else { + quote! { ::core::option::Option::None } } } - } else { - quote! { ::core::option::Option::None } }; Ok(quote! { impl #impl_generics ::std::error::Error for #ident #ty_generics #where_clause { fn source(&self) -> ::core::option::Option<&(dyn ::std::error::Error + 'static)> { - #source_expr + #body } } }) @@ -594,54 +686,96 @@ fn build_enum_display( let mut arms = Vec::with_capacity(variants.len()); for variant in variants { let variant_ident = &variant.ident; - let RewriteResult { - literal, - positional_indices - } = rewrite_format_string(&variant.display, variant.fields.fields.len())?; - let arm = match variant.fields.style { - FieldsStyle::Unit => quote! { - Self::#variant_ident => ::core::write!(formatter, #literal) - }, - FieldsStyle::Named => { - let bindings: Vec<_> = variant - .fields - .fields - .iter() - .map(|f| { - f.ident - .clone() - .expect("named variant field requires identifier") - }) - .collect(); - let positional_bindings = positional_indices.iter().map(|index| { - let binding_ident = format_ident!("__masterror_{index}"); - let field_ident = bindings[*index].clone(); - quote! { - #[allow(unused_variables)] - let #binding_ident = &*#field_ident; + let arm = match &variant.display { + DisplayAttribute::Transparent(attr) => { + let context = format!("variant `{variant_ident}`"); + let field = ensure_transparent_field(&variant.fields, attr, &context)?; + match variant.fields.style { + FieldsStyle::Named => { + let field_ident = field.ident.clone().ok_or_else(|| { + syn::Error::new(attr.span, "named field missing identifier") + })?; + let binding = format_ident!("__masterror_transparent_inner"); + quote! { + Self::#variant_ident { #field_ident } => { + let #binding = #field_ident; + let _: &(dyn ::std::error::Error + 'static) = #binding; + ::core::fmt::Display::fmt(#binding, formatter) + } + } } - }); - quote! { - Self::#variant_ident { #( #bindings ),* } => { - #[allow(unused_variables)] - let _ = (#(&#bindings),*); - #(#positional_bindings)* - ::core::write!(formatter, #literal) + FieldsStyle::Unnamed => { + let binding = field.binding.clone(); + let inner = format_ident!("__masterror_transparent_inner"); + quote! { + Self::#variant_ident( #binding ) => { + let #inner = #binding; + let _: &(dyn ::std::error::Error + 'static) = #inner; + ::core::fmt::Display::fmt(#inner, formatter) + } + } + } + FieldsStyle::Unit => { + return Err(syn::Error::new( + attr.span, + format!( + "variant `{variant_ident}` using #[error(transparent)] must have exactly one field" + ) + )); } } } - FieldsStyle::Unnamed => { - let bindings: Vec<_> = variant - .fields - .fields - .iter() - .map(|f| f.binding.clone()) - .collect(); - quote! { - Self::#variant_ident( #( #bindings ),* ) => { - #[allow(unused_variables)] - let _ = (#(&#bindings),*); - ::core::write!(formatter, #literal) + DisplayAttribute::Format(literal) => { + let RewriteResult { + literal, + positional_indices + } = rewrite_format_string(literal, variant.fields.fields.len())?; + match variant.fields.style { + FieldsStyle::Unit => quote! { + Self::#variant_ident => ::core::write!(formatter, #literal) + }, + FieldsStyle::Named => { + let bindings: Vec<_> = variant + .fields + .fields + .iter() + .map(|f| { + f.ident + .clone() + .expect("named variant field requires identifier") + }) + .collect(); + let positional_bindings = positional_indices.iter().map(|index| { + let binding_ident = format_ident!("__masterror_{index}"); + let field_ident = bindings[*index].clone(); + quote! { + #[allow(unused_variables)] + let #binding_ident = &*#field_ident; + } + }); + quote! { + Self::#variant_ident { #( #bindings ),* } => { + #[allow(unused_variables)] + let _ = (#(&#bindings),*); + #(#positional_bindings)* + ::core::write!(formatter, #literal) + } + } + } + FieldsStyle::Unnamed => { + let bindings: Vec<_> = variant + .fields + .fields + .iter() + .map(|f| f.binding.clone()) + .collect(); + quote! { + Self::#variant_ident( #( #bindings ),* ) => { + #[allow(unused_variables)] + let _ = (#(&#bindings),*); + ::core::write!(formatter, #literal) + } + } } } } @@ -669,99 +803,139 @@ fn build_enum_error( let mut arms = Vec::with_capacity(variants.len()); for variant in variants { let variant_ident = &variant.ident; - let arm = match variant.fields.style { - FieldsStyle::Unit => quote! { - Self::#variant_ident => ::core::option::Option::None - }, - FieldsStyle::Named => { - let bindings: Vec<_> = variant - .fields - .fields - .iter() - .map(|f| { - f.ident - .clone() - .expect("named variant field requires identifier") - }) - .collect(); - let source_expr = if let Some(source) = variant.fields.source { - let binding = bindings[source.index].clone(); - match source.kind { - SourceKind::Direct { - needs_deref: false - } => quote! { - ::core::option::Option::Some(#binding as &(dyn ::std::error::Error + 'static)) - }, - SourceKind::Direct { - needs_deref: true - } => quote! { - ::core::option::Option::Some(#binding.as_ref() as &(dyn ::std::error::Error + 'static)) - }, - SourceKind::Optional { - needs_deref: false - } => quote! { - #binding - .as_ref() - .map(|source| source as &(dyn ::std::error::Error + 'static)) - }, - SourceKind::Optional { - needs_deref: true - } => quote! { - #binding - .as_ref() - .map(|source| source.as_ref() as &(dyn ::std::error::Error + 'static)) + let arm = match &variant.display { + DisplayAttribute::Transparent(attr) => { + let context = format!("variant `{variant_ident}`"); + let field = ensure_transparent_field(&variant.fields, attr, &context)?; + match variant.fields.style { + FieldsStyle::Named => { + let field_ident = field.ident.clone().ok_or_else(|| { + syn::Error::new(attr.span, "named field missing identifier") + })?; + let binding = format_ident!("__masterror_transparent_inner"); + quote! { + Self::#variant_ident { #field_ident } => { + let #binding = #field_ident; + let _: &(dyn ::std::error::Error + 'static) = #binding; + ::std::error::Error::source(#binding) + } } } - } else { - quote! { ::core::option::Option::None } - }; - quote! { - Self::#variant_ident { #( #bindings ),* } => { - #source_expr + FieldsStyle::Unnamed => { + let binding = field.binding.clone(); + let inner = format_ident!("__masterror_transparent_inner"); + quote! { + Self::#variant_ident( #binding ) => { + let #inner = #binding; + let _: &(dyn ::std::error::Error + 'static) = #inner; + ::std::error::Error::source(#inner) + } + } + } + FieldsStyle::Unit => { + return Err(syn::Error::new( + attr.span, + format!( + "variant `{variant_ident}` using #[error(transparent)] must have exactly one field" + ) + )); } } } - FieldsStyle::Unnamed => { - let bindings: Vec<_> = variant - .fields - .fields - .iter() - .map(|f| f.binding.clone()) - .collect(); - let source_expr = if let Some(source) = variant.fields.source { - let binding = bindings[source.index].clone(); - match source.kind { - SourceKind::Direct { - needs_deref: false - } => quote! { - ::core::option::Option::Some(#binding as &(dyn ::std::error::Error + 'static)) - }, - SourceKind::Direct { - needs_deref: true - } => quote! { - ::core::option::Option::Some(#binding.as_ref() as &(dyn ::std::error::Error + 'static)) - }, - SourceKind::Optional { - needs_deref: false - } => quote! { - #binding - .as_ref() - .map(|source| source as &(dyn ::std::error::Error + 'static)) - }, - SourceKind::Optional { - needs_deref: true - } => quote! { - #binding - .as_ref() - .map(|source| source.as_ref() as &(dyn ::std::error::Error + 'static)) + DisplayAttribute::Format(_) => match variant.fields.style { + FieldsStyle::Unit => quote! { + Self::#variant_ident => ::core::option::Option::None + }, + FieldsStyle::Named => { + let bindings: Vec<_> = variant + .fields + .fields + .iter() + .map(|f| { + f.ident + .clone() + .expect("named variant field requires identifier") + }) + .collect(); + let source_expr = if let Some(source) = variant.fields.source { + let binding = bindings[source.index].clone(); + match source.kind { + SourceKind::Direct { + needs_deref: false + } => quote! { + ::core::option::Option::Some(#binding as &(dyn ::std::error::Error + 'static)) + }, + SourceKind::Direct { + needs_deref: true + } => quote! { + ::core::option::Option::Some(#binding.as_ref() as &(dyn ::std::error::Error + 'static)) + }, + SourceKind::Optional { + needs_deref: false + } => quote! { + #binding + .as_ref() + .map(|source| source as &(dyn ::std::error::Error + 'static)) + }, + SourceKind::Optional { + needs_deref: true + } => quote! { + #binding + .as_ref() + .map(|source| source.as_ref() as &(dyn ::std::error::Error + 'static)) + } + } + } else { + quote! { ::core::option::Option::None } + }; + quote! { + Self::#variant_ident { #( #bindings ),* } => { + #source_expr } } - } else { - quote! { ::core::option::Option::None } - }; - quote! { - Self::#variant_ident( #( #bindings ),* ) => { - #source_expr + } + FieldsStyle::Unnamed => { + let bindings: Vec<_> = variant + .fields + .fields + .iter() + .map(|f| f.binding.clone()) + .collect(); + let source_expr = if let Some(source) = variant.fields.source { + let binding = bindings[source.index].clone(); + match source.kind { + SourceKind::Direct { + needs_deref: false + } => quote! { + ::core::option::Option::Some(#binding as &(dyn ::std::error::Error + 'static)) + }, + SourceKind::Direct { + needs_deref: true + } => quote! { + ::core::option::Option::Some(#binding.as_ref() as &(dyn ::std::error::Error + 'static)) + }, + SourceKind::Optional { + needs_deref: false + } => quote! { + #binding + .as_ref() + .map(|source| source as &(dyn ::std::error::Error + 'static)) + }, + SourceKind::Optional { + needs_deref: true + } => quote! { + #binding + .as_ref() + .map(|source| source.as_ref() as &(dyn ::std::error::Error + 'static)) + } + } + } else { + quote! { ::core::option::Option::None } + }; + quote! { + Self::#variant_ident( #( #bindings ),* ) => { + #source_expr + } } } } diff --git a/tests/error_derive.rs b/tests/error_derive.rs index 3df72ab..d5265d4 100644 --- a/tests/error_derive.rs +++ b/tests/error_derive.rs @@ -15,6 +15,18 @@ struct NamedError { #[error("leaf failure")] struct LeafError; +#[derive(Debug, Error)] +#[error("{0}")] +struct TransparentInner(#[source] LeafError); + +#[derive(Debug, Error)] +#[error(transparent)] +struct TransparentWrapper(TransparentInner); + +#[derive(Debug, Error)] +#[error(transparent)] +struct TransparentFromWrapper(#[from] TransparentInner); + #[derive(Debug, Error)] #[error("{0} -> {1:?}")] struct TupleError(&'static str, u8); @@ -75,6 +87,15 @@ enum MixedFromError { } } +#[derive(Debug, Error)] +enum TransparentEnum { + #[error("opaque {0}")] + Opaque(&'static str), + #[error(transparent)] + #[from] + TransparentVariant(TransparentInner) +} + #[test] fn named_struct_display_and_source() { let err = NamedError { @@ -163,3 +184,38 @@ fn enum_from_variants_generate_impls() { "secondary failure" ); } + +#[test] +fn transparent_struct_delegates_display_and_source() { + let inner = TransparentInner(LeafError); + let inner_display = inner.to_string(); + let inner_source = StdError::source(&inner).map(|err| err.to_string()); + let wrapper = TransparentWrapper(inner); + assert_eq!(wrapper.to_string(), inner_display); + assert_eq!( + StdError::source(&wrapper).map(|err| err.to_string()), + inner_source + ); +} + +#[test] +fn transparent_struct_from_impl() { + let wrapper = TransparentFromWrapper::from(TransparentInner(LeafError)); + assert_eq!(wrapper.to_string(), "leaf failure"); + assert_eq!( + StdError::source(&wrapper).map(|err| err.to_string()), + Some(String::from("leaf failure")) + ); +} + +#[test] +fn transparent_enum_variant_from_impl() { + let _unused = TransparentEnum::Opaque("noop"); + let variant = TransparentEnum::from(TransparentInner(LeafError)); + assert!(matches!(variant, TransparentEnum::TransparentVariant(_))); + assert_eq!(variant.to_string(), "leaf failure"); + assert_eq!( + StdError::source(&variant).map(|err| err.to_string()), + Some(String::from("leaf failure")) + ); +} diff --git a/tests/error_derive_from_trybuild.rs b/tests/error_derive_from_trybuild.rs index a878025..377c5c2 100644 --- a/tests/error_derive_from_trybuild.rs +++ b/tests/error_derive_from_trybuild.rs @@ -5,3 +5,9 @@ fn from_attribute_compile_failures() { let t = TestCases::new(); t.compile_fail("tests/ui/from/*.rs"); } + +#[test] +fn transparent_attribute_compile_failures() { + let t = TestCases::new(); + t.compile_fail("tests/ui/transparent/*.rs"); +} diff --git a/tests/ui/transparent/enum_variant_multiple_fields.rs b/tests/ui/transparent/enum_variant_multiple_fields.rs new file mode 100644 index 0000000..5565776 --- /dev/null +++ b/tests/ui/transparent/enum_variant_multiple_fields.rs @@ -0,0 +1,7 @@ +use masterror::Error; + +#[derive(Debug, Error)] +enum TransparentEnumFail { + #[error(transparent)] + Variant(String, String) +} diff --git a/tests/ui/transparent/enum_variant_multiple_fields.stderr b/tests/ui/transparent/enum_variant_multiple_fields.stderr new file mode 100644 index 0000000..2d0db83 --- /dev/null +++ b/tests/ui/transparent/enum_variant_multiple_fields.stderr @@ -0,0 +1,11 @@ +error: using #[error(transparent)] in variant `Variant` requires exactly one field + --> tests/ui/transparent/enum_variant_multiple_fields.rs:5:5 + | +5 | #[error(transparent)] + | ^ + +error[E0601]: `main` function not found in crate `$CRATE` + --> tests/ui/transparent/enum_variant_multiple_fields.rs:7:2 + | +7 | } + | ^ consider adding a `main` function to `$DIR/tests/ui/transparent/enum_variant_multiple_fields.rs` diff --git a/tests/ui/transparent/enum_variant_no_fields.rs b/tests/ui/transparent/enum_variant_no_fields.rs new file mode 100644 index 0000000..92f40e0 --- /dev/null +++ b/tests/ui/transparent/enum_variant_no_fields.rs @@ -0,0 +1,7 @@ +use masterror::Error; + +#[derive(Debug, Error)] +enum TransparentEnumUnit { + #[error(transparent)] + Variant +} diff --git a/tests/ui/transparent/enum_variant_no_fields.stderr b/tests/ui/transparent/enum_variant_no_fields.stderr new file mode 100644 index 0000000..fc44590 --- /dev/null +++ b/tests/ui/transparent/enum_variant_no_fields.stderr @@ -0,0 +1,11 @@ +error: using #[error(transparent)] in variant `Variant` requires exactly one field + --> tests/ui/transparent/enum_variant_no_fields.rs:5:5 + | +5 | #[error(transparent)] + | ^ + +error[E0601]: `main` function not found in crate `$CRATE` + --> tests/ui/transparent/enum_variant_no_fields.rs:7:2 + | +7 | } + | ^ consider adding a `main` function to `$DIR/tests/ui/transparent/enum_variant_no_fields.rs` diff --git a/tests/ui/transparent/struct_multiple_fields.rs b/tests/ui/transparent/struct_multiple_fields.rs new file mode 100644 index 0000000..ddde691 --- /dev/null +++ b/tests/ui/transparent/struct_multiple_fields.rs @@ -0,0 +1,8 @@ +use masterror::Error; + +#[derive(Debug, Error)] +#[error(transparent)] +struct TransparentMany { + first: String, + second: String +} diff --git a/tests/ui/transparent/struct_multiple_fields.stderr b/tests/ui/transparent/struct_multiple_fields.stderr new file mode 100644 index 0000000..32a7491 --- /dev/null +++ b/tests/ui/transparent/struct_multiple_fields.stderr @@ -0,0 +1,11 @@ +error: using #[error(transparent)] in struct `TransparentMany` requires exactly one field + --> tests/ui/transparent/struct_multiple_fields.rs:4:1 + | +4 | #[error(transparent)] + | ^ + +error[E0601]: `main` function not found in crate `$CRATE` + --> tests/ui/transparent/struct_multiple_fields.rs:8:2 + | +8 | } + | ^ consider adding a `main` function to `$DIR/tests/ui/transparent/struct_multiple_fields.rs` diff --git a/tests/ui/transparent/struct_no_fields.rs b/tests/ui/transparent/struct_no_fields.rs new file mode 100644 index 0000000..a747343 --- /dev/null +++ b/tests/ui/transparent/struct_no_fields.rs @@ -0,0 +1,5 @@ +use masterror::Error; + +#[derive(Debug, Error)] +#[error(transparent)] +struct TransparentUnit; diff --git a/tests/ui/transparent/struct_no_fields.stderr b/tests/ui/transparent/struct_no_fields.stderr new file mode 100644 index 0000000..be3f853 --- /dev/null +++ b/tests/ui/transparent/struct_no_fields.stderr @@ -0,0 +1,11 @@ +error: using #[error(transparent)] in struct `TransparentUnit` requires exactly one field + --> tests/ui/transparent/struct_no_fields.rs:4:1 + | +4 | #[error(transparent)] + | ^ + +error[E0601]: `main` function not found in crate `$CRATE` + --> tests/ui/transparent/struct_no_fields.rs:5:24 + | +5 | struct TransparentUnit; + | ^ consider adding a `main` function to `$DIR/tests/ui/transparent/struct_no_fields.rs` From c9ec00b42c72315e6a6013677559e60cbe09936f Mon Sep 17 00:00:00 2001 From: RA <70325462+RAprogramm@users.noreply.github.com> Date: Wed, 17 Sep 2025 11:59:50 +0700 Subject: [PATCH 09/25] Specify masterror-derive version for packaging --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 1569de2..cf609c4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -37,7 +37,7 @@ turnkey = [] openapi = ["dep:utoipa"] [dependencies] -masterror-derive = { path = "masterror-derive" } +masterror-derive = { path = "masterror-derive", version = "0.1.0" } tracing = "0.1" serde = { version = "1", features = ["derive"] } From 30e76da5b63c33841eaf66b6b130eb39a6674899 Mon Sep 17 00:00:00 2001 From: RA <70325462+RAprogramm@users.noreply.github.com> Date: Wed, 17 Sep 2025 12:39:14 +0700 Subject: [PATCH 10/25] Replace derive crate with thiserror re-export --- Cargo.lock | 49 +- Cargo.toml | 6 +- masterror-derive/Cargo.toml | 16 - masterror-derive/src/lib.rs | 1110 ----------------- src/lib.rs | 5 +- tests/error_derive.rs | 19 +- tests/ui/from/struct_multiple_fields.stderr | 4 +- tests/ui/from/variant_multiple_fields.stderr | 4 +- .../enum_variant_multiple_fields.stderr | 7 +- .../transparent/enum_variant_no_fields.stderr | 7 +- .../transparent/struct_multiple_fields.stderr | 4 +- tests/ui/transparent/struct_no_fields.stderr | 4 +- 12 files changed, 64 insertions(+), 1171 deletions(-) delete mode 100644 masterror-derive/Cargo.toml delete mode 100644 masterror-derive/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index ae55d42..a307650 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1493,7 +1493,7 @@ checksum = "3d38aeb53944762378aa5219b9929f2f3346a25fdd9b61266c24a487200c87fd" dependencies = [ "http 1.3.1", "serde", - "thiserror", + "thiserror 2.0.16", "tracing", ] @@ -1506,7 +1506,6 @@ dependencies = [ "config", "http 1.3.1", "js-sys", - "masterror-derive", "redis", "reqwest", "serde", @@ -1515,6 +1514,7 @@ dependencies = [ "sqlx", "telegram-webapp-sdk", "teloxide-core", + "thiserror 1.0.69", "tokio", "toml 0.8.23", "toml 0.9.6", @@ -1525,15 +1525,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "masterror-derive" -version = "0.1.0" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "matchit" version = "0.8.4" @@ -1755,7 +1746,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "21e0a3a33733faeaf8651dfee72dd0f388f0c8e5ad496a3478fa5a922f49cfa8" dependencies = [ "memchr", - "thiserror", + "thiserror 2.0.16", "ucd-trie", ] @@ -2507,7 +2498,7 @@ dependencies = [ "serde_json", "sha2", "smallvec", - "thiserror", + "thiserror 2.0.16", "tracing", "url", ] @@ -2582,7 +2573,7 @@ dependencies = [ "smallvec", "sqlx-core", "stringprep", - "thiserror", + "thiserror 2.0.16", "tracing", "whoami", ] @@ -2619,7 +2610,7 @@ dependencies = [ "smallvec", "sqlx-core", "stringprep", - "thiserror", + "thiserror 2.0.16", "tracing", "whoami", ] @@ -2642,7 +2633,7 @@ dependencies = [ "percent-encoding", "serde_urlencoded", "sqlx-core", - "thiserror", + "thiserror 2.0.16", "tracing", "url", ] @@ -2756,7 +2747,7 @@ dependencies = [ "serde-wasm-bindgen", "serde_json", "serde_urlencoded", - "thiserror", + "thiserror 2.0.16", "toml 0.8.23", "wasm-bindgen", "wasm-bindgen-futures", @@ -2788,7 +2779,7 @@ dependencies = [ "stacker", "take_mut", "takecell", - "thiserror", + "thiserror 2.0.16", "tokio", "tokio-util", "url", @@ -2804,13 +2795,33 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + [[package]] name = "thiserror" version = "2.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3467d614147380f2e4e374161426ff399c91084acd2363eaf549172b3d5e60c0" dependencies = [ - "thiserror-impl", + "thiserror-impl 2.0.16", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index cf609c4..c9dfacf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,10 +13,6 @@ categories = ["rust-patterns", "web-programming"] keywords = ["error", "api", "framework"] -[workspace] -members = ["masterror-derive"] - - [features] default = [] axum = ["dep:axum", "dep:serde_json"] # IntoResponse + JSON body @@ -37,7 +33,7 @@ turnkey = [] openapi = ["dep:utoipa"] [dependencies] -masterror-derive = { path = "masterror-derive", version = "0.1.0" } +thiserror = "1" tracing = "0.1" serde = { version = "1", features = ["derive"] } diff --git a/masterror-derive/Cargo.toml b/masterror-derive/Cargo.toml deleted file mode 100644 index 31d1fb5..0000000 --- a/masterror-derive/Cargo.toml +++ /dev/null @@ -1,16 +0,0 @@ -[package] -name = "masterror-derive" -version = "0.1.0" -edition = "2024" -rust-version = "1.89" -description = "Derive macro for masterror" -license = "MIT OR Apache-2.0" -authors = ["masterror maintainers"] - -[lib] -proc-macro = true - -[dependencies] -proc-macro2 = "1" -quote = "1" -syn = { version = "2", features = ["full", "extra-traits"] } diff --git a/masterror-derive/src/lib.rs b/masterror-derive/src/lib.rs deleted file mode 100644 index c270ebb..0000000 --- a/masterror-derive/src/lib.rs +++ /dev/null @@ -1,1110 +0,0 @@ -#![forbid(unsafe_code)] -#![warn(missing_docs, clippy::all, rust_2018_idioms)] - -//! Derive macro implementing [`std::error::Error`] with `Display` formatting. -//! -//! The macro mirrors the essential functionality relied upon by `masterror` and -//! consumers of the crate: display strings with named or positional fields, -//! `#[from]` conversions for wrapper types, transparent wrappers via -//! `#[error(transparent)]`, and a configurable error source via `#[source]` -//! field attributes. - -use std::collections::BTreeSet; - -use proc_macro::TokenStream; -use proc_macro2::{Ident, Span, TokenStream as TokenStream2}; -use quote::{format_ident, quote}; -use syn::{ - Attribute, Data, DataEnum, DataStruct, DeriveInput, Field, Fields, GenericArgument, Generics, - Ident as SynIdent, LitStr, Member, Meta, PathArguments, Type, parse::ParseStream, - spanned::Spanned -}; - -/// Derive [`std::error::Error`] and [`core::fmt::Display`] for structs and -/// enums. -/// -/// -/// #[derive(Debug, Error)] -/// #[error("{code}: {message}")] -/// struct MiniError { -/// code: u16, -/// message: &'static str -/// } -/// -/// let err = MiniError { -/// code: 500, -/// message: "boom" -/// }; -/// assert_eq!(err.to_string(), "500: boom"); -/// assert!(std::error::Error::source(&err).is_none()); -/// ``` -#[proc_macro_derive(Error, attributes(error, source, from))] -pub fn derive_error(input: TokenStream) -> TokenStream { - match derive_error_impl(syn::parse_macro_input!(input as DeriveInput)) { - Ok(tokens) => tokens.into(), - Err(err) => err.to_compile_error().into() - } -} - -fn derive_error_impl(input: DeriveInput) -> syn::Result { - let ident = input.ident; - let generics = input.generics; - - let display_impl; - let error_impl; - let from_impls; - - match input.data { - Data::Struct(data) => { - let fields = parse_fields(&data)?; - let display_attr = parse_display_attr(&input.attrs)?; - display_impl = build_struct_display(&ident, &generics, &fields, &display_attr)?; - error_impl = build_struct_error(&ident, &generics, &fields, &display_attr)?; - from_impls = build_struct_from_impl(&ident, &generics, &fields)?; - } - Data::Enum(data) => { - let variants = parse_enum(&data)?; - display_impl = build_enum_display(&ident, &generics, &variants)?; - error_impl = build_enum_error(&ident, &generics, &variants)?; - from_impls = build_enum_from_impls(&ident, &generics, &variants)?; - } - Data::Union(_) => { - return Err(syn::Error::new( - ident.span(), - "#[derive(Error)] does not support unions" - )); - } - } - - Ok(quote! { - #display_impl - #error_impl - #from_impls - }) -} - -#[derive(Clone, Copy)] -enum FieldsStyle { - Unit, - Named, - Unnamed -} - -#[derive(Clone)] -struct FieldSpec { - member: Member, - ident: Option, - binding: Ident, - ty: Type, - attrs: FieldAttributes -} - -#[derive(Clone, Default)] -struct FieldAttributes { - from: Option, - source: Option -} - -impl FieldAttributes { - fn mark_from(&mut self, span: Span) -> syn::Result<()> { - if self.from.is_some() { - return Err(syn::Error::new(span, "duplicate #[from] attribute")); - } - self.from = Some(span); - Ok(()) - } - - fn mark_source(&mut self, span: Span) -> syn::Result<()> { - if self.source.is_some() { - return Err(syn::Error::new(span, "duplicate #[source] attribute")); - } - self.source = Some(span); - Ok(()) - } - - fn has_source(&self) -> bool { - self.source.is_some() - } - - fn span_of_from_attribute(&self) -> Option { - self.from - } - - fn source_span(&self) -> Option { - self.source - } -} - -#[derive(Clone)] -enum DisplayAttribute { - Format(LitStr), - Transparent(TransparentAttr) -} - -#[derive(Clone, Copy)] -struct TransparentAttr { - span: Span -} - -#[derive(Clone, Copy)] -enum SourceKind { - Direct { needs_deref: bool }, - Optional { needs_deref: bool } -} - -#[derive(Clone, Copy)] -struct SourceField { - index: usize, - kind: SourceKind -} - -struct ParsedFields { - style: FieldsStyle, - fields: Vec, - source: Option -} - -struct VariantInfo { - ident: Ident, - fields: ParsedFields, - display: DisplayAttribute -} - -struct FromFieldInfo<'a> { - field: &'a FieldSpec, - span: Span -} - -struct RewriteResult { - literal: LitStr, - positional_indices: BTreeSet -} - -fn parse_fields(data: &DataStruct) -> syn::Result { - parse_fields_internal(&data.fields) -} - -fn parse_enum(data: &DataEnum) -> syn::Result> { - let mut variants = Vec::with_capacity(data.variants.len()); - for variant in &data.variants { - let display = parse_display_attr(&variant.attrs)?; - let mut fields = parse_fields_internal(&variant.fields)?; - if let Some(span) = parse_variant_from_attr(&variant.attrs)? { - apply_variant_from_attr(&mut fields, span, &variant.ident)?; - } - variants.push(VariantInfo { - ident: variant.ident.clone(), - fields, - display - }); - } - Ok(variants) -} - -fn parse_fields_internal(fields: &Fields) -> syn::Result { - match fields { - Fields::Unit => Ok(ParsedFields { - style: FieldsStyle::Unit, - fields: Vec::new(), - source: None - }), - Fields::Named(named) => { - let mut specs = Vec::with_capacity(named.named.len()); - let mut source = None; - for (index, field) in named.named.iter().enumerate() { - let attrs = parse_field_attributes(field)?; - let ident = field.ident.clone().ok_or_else(|| { - syn::Error::new(field.span(), "named field missing identifier") - })?; - let member = Member::Named(ident.clone()); - let binding = ident.clone(); - if attrs.has_source() { - let kind = detect_source_kind(&field.ty)?; - if source.is_some() { - return Err(syn::Error::new( - attrs.source_span().unwrap_or_else(|| field.span()), - "only a single #[source] field is supported" - )); - } - source = Some(SourceField { - index, - kind - }); - } - specs.push(FieldSpec { - member, - ident: Some(ident), - binding, - ty: field.ty.clone(), - attrs - }); - } - Ok(ParsedFields { - style: FieldsStyle::Named, - fields: specs, - source - }) - } - Fields::Unnamed(unnamed) => { - let mut specs = Vec::with_capacity(unnamed.unnamed.len()); - let mut source = None; - for (index, field) in unnamed.unnamed.iter().enumerate() { - let attrs = parse_field_attributes(field)?; - let member = Member::Unnamed(index.into()); - let binding = format_ident!("__masterror_{index}"); - if attrs.has_source() { - let kind = detect_source_kind(&field.ty)?; - if source.is_some() { - return Err(syn::Error::new( - attrs.source_span().unwrap_or_else(|| field.span()), - "only a single #[source] field is supported" - )); - } - source = Some(SourceField { - index, - kind - }); - } - specs.push(FieldSpec { - member, - ident: None, - binding, - ty: field.ty.clone(), - attrs - }); - } - Ok(ParsedFields { - style: FieldsStyle::Unnamed, - fields: specs, - source - }) - } - } -} - -fn parse_display_attr(attrs: &[Attribute]) -> syn::Result { - let mut result = None; - for attr in attrs.iter().filter(|attr| attr.path().is_ident("error")) { - if result.is_some() { - return Err(syn::Error::new( - attr.span(), - "multiple #[error(...)] attributes found" - )); - } - match &attr.meta { - Meta::List(_) => { - let attr_value = parse_error_attribute(attr)?; - result = Some(attr_value); - } - _ => { - return Err(syn::Error::new( - attr.span(), - r#"expected #[error("format")]"# - )); - } - } - } - result - .ok_or_else(|| syn::Error::new(Span::call_site(), r#"missing #[error("...")] attribute"#)) -} - -fn parse_error_attribute(attr: &Attribute) -> syn::Result { - attr.parse_args_with(|input: ParseStream<'_>| { - if input.is_empty() { - return Err(input.error("expected string literal or `transparent`")); - } - if input.peek(LitStr) { - let lit: LitStr = input.parse()?; - if !input.is_empty() { - return Err(input.error("unexpected tokens after format string")); - } - return Ok(DisplayAttribute::Format(lit)); - } - if input.peek(SynIdent) { - let ident: SynIdent = input.parse()?; - if ident == "transparent" { - if !input.is_empty() { - return Err(input.error("unexpected tokens after `transparent`")); - } - return Ok(DisplayAttribute::Transparent(TransparentAttr { - span: attr.span() - })); - } - return Err(syn::Error::new( - ident.span(), - "unknown #[error] attribute argument" - )); - } - Err(input.error("expected string literal or `transparent`")) - }) -} - -fn parse_field_attributes(field: &Field) -> syn::Result { - let mut attrs = FieldAttributes::default(); - for attr in &field.attrs { - if attr.path().is_ident("source") { - ensure_path_only(attr, "source")?; - attrs.mark_source(attr.span())?; - } else if attr.path().is_ident("from") { - ensure_path_only(attr, "from")?; - attrs.mark_from(attr.span())?; - } - } - Ok(attrs) -} - -fn ensure_path_only(attr: &Attribute, name: &str) -> syn::Result<()> { - if !matches!(&attr.meta, Meta::Path(_)) { - return Err(syn::Error::new( - attr.span(), - format!("#[{name}] attribute does not accept arguments") - )); - } - Ok(()) -} - -fn parse_variant_from_attr(attrs: &[Attribute]) -> syn::Result> { - let mut span = None; - for attr in attrs.iter().filter(|attr| attr.path().is_ident("from")) { - ensure_path_only(attr, "from")?; - if span.is_some() { - return Err(syn::Error::new(attr.span(), "duplicate #[from] attribute")); - } - span = Some(attr.span()); - } - Ok(span) -} - -fn apply_variant_from_attr( - fields: &mut ParsedFields, - span: Span, - variant_ident: &Ident -) -> syn::Result<()> { - if fields.fields.is_empty() { - return Err(syn::Error::new( - span, - format!( - "variant `{variant_ident}` marked with #[from] must contain exactly one field" - ) - )); - } - if fields.fields.len() != 1 { - return Err(syn::Error::new( - span, - format!( - "variant `{variant_ident}` marked with #[from] must contain exactly one field" - ) - )); - } - let field = fields - .fields - .get_mut(0) - .ok_or_else(|| syn::Error::new(span, "invalid #[from] field index"))?; - field.attrs.mark_from(span) -} - -fn find_from_field<'a>( - fields: &'a ParsedFields, - context: &str -) -> syn::Result>> { - let mut info = None; - for field in &fields.fields { - if let Some(span) = field.attrs.span_of_from_attribute() { - if info.is_some() { - return Err(syn::Error::new( - span, - format!( - "multiple #[from] attributes in {context}; only one field may use #[from]" - ) - )); - } - info = Some(FromFieldInfo { - field, - span - }); - } - } - let Some(info) = info else { - return Ok(None); - }; - if fields.fields.len() != 1 { - return Err(syn::Error::new( - info.span, - format!("using #[from] in {context} requires exactly one field") - )); - } - Ok(Some(info)) -} - -fn ensure_transparent_field<'a>( - fields: &'a ParsedFields, - attr: &TransparentAttr, - context: &str -) -> syn::Result<&'a FieldSpec> { - if fields.fields.len() != 1 { - return Err(syn::Error::new( - attr.span, - format!("using #[error(transparent)] in {context} requires exactly one field") - )); - } - fields - .fields - .first() - .ok_or_else(|| syn::Error::new(attr.span, "invalid transparent field index")) -} - -fn detect_source_kind(ty: &Type) -> syn::Result { - if let Some(inner) = option_inner_type(ty) { - Ok(SourceKind::Optional { - needs_deref: needs_deref(inner) - }) - } else { - Ok(SourceKind::Direct { - needs_deref: needs_deref(ty) - }) - } -} - -fn option_inner_type(ty: &Type) -> Option<&Type> { - if let Type::Path(type_path) = ty - && type_path.qself.is_none() - && let Some(segment) = type_path.path.segments.last() - && segment.ident == "Option" - && let PathArguments::AngleBracketed(args) = &segment.arguments - && let Some(GenericArgument::Type(inner)) = args.args.first() - { - return Some(inner); - } - None -} - -fn needs_deref(ty: &Type) -> bool { - if let Type::Path(type_path) = ty { - if type_path.qself.is_some() { - return false; - } - if let Some(segment) = type_path.path.segments.last() { - let ident = segment.ident.to_string(); - return matches!(ident.as_str(), "Box" | "Rc" | "Arc"); - } - } - false -} - -fn build_struct_display( - ident: &Ident, - generics: &Generics, - fields: &ParsedFields, - display: &DisplayAttribute -) -> syn::Result { - let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); - let body = match display { - DisplayAttribute::Transparent(attr) => { - let context = format!("struct `{ident}`"); - let field = ensure_transparent_field(fields, attr, &context)?; - let member = &field.member; - let binding = format_ident!("__masterror_transparent_inner"); - quote! { - let #binding = &self.#member; - let _: &(dyn ::std::error::Error + 'static) = #binding; - ::core::fmt::Display::fmt(#binding, formatter) - } - } - DisplayAttribute::Format(literal) => { - let RewriteResult { - literal, - positional_indices - } = rewrite_format_string(literal, fields.fields.len())?; - - match fields.style { - FieldsStyle::Unit => quote! { - ::core::write!(formatter, #literal) - }, - FieldsStyle::Named => { - let field_idents: Vec<_> = fields - .fields - .iter() - .map(|f| f.ident.clone().expect("named fields must have identifiers")) - .collect(); - let positional_bindings = positional_indices.iter().map(|index| { - let binding_ident = format_ident!("__masterror_{index}"); - let field_ident = field_idents[*index].clone(); - quote! { - #[allow(unused_variables)] - let #binding_ident = &*#field_ident; - } - }); - quote! { - let Self { #( ref #field_idents ),* } = *self; - #[allow(unused_variables)] - let _ = (#(&#field_idents),*); - #(#positional_bindings)* - ::core::write!(formatter, #literal) - } - } - FieldsStyle::Unnamed => { - let bindings: Vec<_> = - fields.fields.iter().map(|f| f.binding.clone()).collect(); - quote! { - let Self( #( ref #bindings ),* ) = *self; - #[allow(unused_variables)] - let _ = (#(&#bindings),*); - ::core::write!(formatter, #literal) - } - } - } - } - }; - - Ok(quote! { - impl #impl_generics ::core::fmt::Display for #ident #ty_generics #where_clause { - fn fmt(&self, formatter: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result { - #body - } - } - }) -} - -fn build_struct_error( - ident: &Ident, - generics: &Generics, - fields: &ParsedFields, - display: &DisplayAttribute -) -> syn::Result { - let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); - let body = match display { - DisplayAttribute::Transparent(attr) => { - let context = format!("struct `{ident}`"); - let field = ensure_transparent_field(fields, attr, &context)?; - let member = &field.member; - let binding = format_ident!("__masterror_transparent_inner"); - quote! { - let #binding = &self.#member; - let _: &(dyn ::std::error::Error + 'static) = #binding; - ::std::error::Error::source(#binding) - } - } - DisplayAttribute::Format(_) => { - if let Some(source) = fields.source { - let field = fields.fields.get(source.index).ok_or_else(|| { - syn::Error::new(Span::call_site(), "invalid source field index") - })?; - let member = &field.member; - match source.kind { - SourceKind::Direct { - needs_deref: false - } => quote! { - ::core::option::Option::Some(&self.#member as &(dyn ::std::error::Error + 'static)) - }, - SourceKind::Direct { - needs_deref: true - } => quote! { - ::core::option::Option::Some(self.#member.as_ref() as &(dyn ::std::error::Error + 'static)) - }, - SourceKind::Optional { - needs_deref: false - } => quote! { - self.#member - .as_ref() - .map(|source| source as &(dyn ::std::error::Error + 'static)) - }, - SourceKind::Optional { - needs_deref: true - } => quote! { - self.#member - .as_ref() - .map(|source| source.as_ref() as &(dyn ::std::error::Error + 'static)) - } - } - } else { - quote! { ::core::option::Option::None } - } - } - }; - - Ok(quote! { - impl #impl_generics ::std::error::Error for #ident #ty_generics #where_clause { - fn source(&self) -> ::core::option::Option<&(dyn ::std::error::Error + 'static)> { - #body - } - } - }) -} - -fn build_struct_from_impl( - ident: &Ident, - generics: &Generics, - fields: &ParsedFields -) -> syn::Result { - let context = format!("struct `{ident}`"); - let Some(from_info) = find_from_field(fields, &context)? else { - return Ok(TokenStream2::new()); - }; - let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); - let field_ty = &from_info.field.ty; - let arg_ident = format_ident!("__masterror_from_value"); - - let construct = match fields.style { - FieldsStyle::Named => { - let field_ident = from_info.field.ident.clone().ok_or_else(|| { - syn::Error::new(from_info.span, "named field missing identifier") - })?; - quote! { Self { #field_ident: #arg_ident } } - } - FieldsStyle::Unnamed => { - if fields.fields.len() != 1 { - return Err(syn::Error::new( - from_info.span, - format!("using #[from] in {context} requires exactly one field") - )); - } - quote! { Self(#arg_ident) } - } - FieldsStyle::Unit => { - return Err(syn::Error::new( - from_info.span, - format!("using #[from] in {context} requires at least one field") - )); - } - }; - - Ok(quote! { - impl #impl_generics ::core::convert::From<#field_ty> for #ident #ty_generics #where_clause { - fn from(#arg_ident: #field_ty) -> Self { - #construct - } - } - }) -} - -fn build_enum_display( - ident: &Ident, - generics: &Generics, - variants: &[VariantInfo] -) -> syn::Result { - let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); - let mut arms = Vec::with_capacity(variants.len()); - for variant in variants { - let variant_ident = &variant.ident; - let arm = match &variant.display { - DisplayAttribute::Transparent(attr) => { - let context = format!("variant `{variant_ident}`"); - let field = ensure_transparent_field(&variant.fields, attr, &context)?; - match variant.fields.style { - FieldsStyle::Named => { - let field_ident = field.ident.clone().ok_or_else(|| { - syn::Error::new(attr.span, "named field missing identifier") - })?; - let binding = format_ident!("__masterror_transparent_inner"); - quote! { - Self::#variant_ident { #field_ident } => { - let #binding = #field_ident; - let _: &(dyn ::std::error::Error + 'static) = #binding; - ::core::fmt::Display::fmt(#binding, formatter) - } - } - } - FieldsStyle::Unnamed => { - let binding = field.binding.clone(); - let inner = format_ident!("__masterror_transparent_inner"); - quote! { - Self::#variant_ident( #binding ) => { - let #inner = #binding; - let _: &(dyn ::std::error::Error + 'static) = #inner; - ::core::fmt::Display::fmt(#inner, formatter) - } - } - } - FieldsStyle::Unit => { - return Err(syn::Error::new( - attr.span, - format!( - "variant `{variant_ident}` using #[error(transparent)] must have exactly one field" - ) - )); - } - } - } - DisplayAttribute::Format(literal) => { - let RewriteResult { - literal, - positional_indices - } = rewrite_format_string(literal, variant.fields.fields.len())?; - match variant.fields.style { - FieldsStyle::Unit => quote! { - Self::#variant_ident => ::core::write!(formatter, #literal) - }, - FieldsStyle::Named => { - let bindings: Vec<_> = variant - .fields - .fields - .iter() - .map(|f| { - f.ident - .clone() - .expect("named variant field requires identifier") - }) - .collect(); - let positional_bindings = positional_indices.iter().map(|index| { - let binding_ident = format_ident!("__masterror_{index}"); - let field_ident = bindings[*index].clone(); - quote! { - #[allow(unused_variables)] - let #binding_ident = &*#field_ident; - } - }); - quote! { - Self::#variant_ident { #( #bindings ),* } => { - #[allow(unused_variables)] - let _ = (#(&#bindings),*); - #(#positional_bindings)* - ::core::write!(formatter, #literal) - } - } - } - FieldsStyle::Unnamed => { - let bindings: Vec<_> = variant - .fields - .fields - .iter() - .map(|f| f.binding.clone()) - .collect(); - quote! { - Self::#variant_ident( #( #bindings ),* ) => { - #[allow(unused_variables)] - let _ = (#(&#bindings),*); - ::core::write!(formatter, #literal) - } - } - } - } - } - }; - arms.push(arm); - } - - Ok(quote! { - impl #impl_generics ::core::fmt::Display for #ident #ty_generics #where_clause { - fn fmt(&self, formatter: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result { - match self { - #(#arms),* - } - } - } - }) -} - -fn build_enum_error( - ident: &Ident, - generics: &Generics, - variants: &[VariantInfo] -) -> syn::Result { - let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); - let mut arms = Vec::with_capacity(variants.len()); - for variant in variants { - let variant_ident = &variant.ident; - let arm = match &variant.display { - DisplayAttribute::Transparent(attr) => { - let context = format!("variant `{variant_ident}`"); - let field = ensure_transparent_field(&variant.fields, attr, &context)?; - match variant.fields.style { - FieldsStyle::Named => { - let field_ident = field.ident.clone().ok_or_else(|| { - syn::Error::new(attr.span, "named field missing identifier") - })?; - let binding = format_ident!("__masterror_transparent_inner"); - quote! { - Self::#variant_ident { #field_ident } => { - let #binding = #field_ident; - let _: &(dyn ::std::error::Error + 'static) = #binding; - ::std::error::Error::source(#binding) - } - } - } - FieldsStyle::Unnamed => { - let binding = field.binding.clone(); - let inner = format_ident!("__masterror_transparent_inner"); - quote! { - Self::#variant_ident( #binding ) => { - let #inner = #binding; - let _: &(dyn ::std::error::Error + 'static) = #inner; - ::std::error::Error::source(#inner) - } - } - } - FieldsStyle::Unit => { - return Err(syn::Error::new( - attr.span, - format!( - "variant `{variant_ident}` using #[error(transparent)] must have exactly one field" - ) - )); - } - } - } - DisplayAttribute::Format(_) => match variant.fields.style { - FieldsStyle::Unit => quote! { - Self::#variant_ident => ::core::option::Option::None - }, - FieldsStyle::Named => { - let bindings: Vec<_> = variant - .fields - .fields - .iter() - .map(|f| { - f.ident - .clone() - .expect("named variant field requires identifier") - }) - .collect(); - let source_expr = if let Some(source) = variant.fields.source { - let binding = bindings[source.index].clone(); - match source.kind { - SourceKind::Direct { - needs_deref: false - } => quote! { - ::core::option::Option::Some(#binding as &(dyn ::std::error::Error + 'static)) - }, - SourceKind::Direct { - needs_deref: true - } => quote! { - ::core::option::Option::Some(#binding.as_ref() as &(dyn ::std::error::Error + 'static)) - }, - SourceKind::Optional { - needs_deref: false - } => quote! { - #binding - .as_ref() - .map(|source| source as &(dyn ::std::error::Error + 'static)) - }, - SourceKind::Optional { - needs_deref: true - } => quote! { - #binding - .as_ref() - .map(|source| source.as_ref() as &(dyn ::std::error::Error + 'static)) - } - } - } else { - quote! { ::core::option::Option::None } - }; - quote! { - Self::#variant_ident { #( #bindings ),* } => { - #source_expr - } - } - } - FieldsStyle::Unnamed => { - let bindings: Vec<_> = variant - .fields - .fields - .iter() - .map(|f| f.binding.clone()) - .collect(); - let source_expr = if let Some(source) = variant.fields.source { - let binding = bindings[source.index].clone(); - match source.kind { - SourceKind::Direct { - needs_deref: false - } => quote! { - ::core::option::Option::Some(#binding as &(dyn ::std::error::Error + 'static)) - }, - SourceKind::Direct { - needs_deref: true - } => quote! { - ::core::option::Option::Some(#binding.as_ref() as &(dyn ::std::error::Error + 'static)) - }, - SourceKind::Optional { - needs_deref: false - } => quote! { - #binding - .as_ref() - .map(|source| source as &(dyn ::std::error::Error + 'static)) - }, - SourceKind::Optional { - needs_deref: true - } => quote! { - #binding - .as_ref() - .map(|source| source.as_ref() as &(dyn ::std::error::Error + 'static)) - } - } - } else { - quote! { ::core::option::Option::None } - }; - quote! { - Self::#variant_ident( #( #bindings ),* ) => { - #source_expr - } - } - } - } - }; - arms.push(arm); - } - - Ok(quote! { - impl #impl_generics ::std::error::Error for #ident #ty_generics #where_clause { - fn source(&self) -> ::core::option::Option<&(dyn ::std::error::Error + 'static)> { - match self { - #(#arms),* - } - } - } - }) -} - -fn build_enum_from_impls( - ident: &Ident, - generics: &Generics, - variants: &[VariantInfo] -) -> syn::Result { - let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); - let mut impls = Vec::new(); - - for variant in variants { - let context = format!("variant `{}`", variant.ident); - if let Some(from_info) = find_from_field(&variant.fields, &context)? { - let field_ty = &from_info.field.ty; - let variant_ident = &variant.ident; - let arg_ident = format_ident!("__masterror_from_value"); - - let body = match variant.fields.style { - FieldsStyle::Named => { - let field_ident = from_info.field.ident.clone().ok_or_else(|| { - syn::Error::new(from_info.span, "named field missing identifier") - })?; - quote! { Self::#variant_ident { #field_ident: #arg_ident } } - } - FieldsStyle::Unnamed => { - if variant.fields.fields.len() != 1 { - return Err(syn::Error::new( - from_info.span, - format!("using #[from] in {context} requires exactly one field") - )); - } - quote! { Self::#variant_ident(#arg_ident) } - } - FieldsStyle::Unit => { - return Err(syn::Error::new( - from_info.span, - format!("{context} cannot be unit-like when using #[from]") - )); - } - }; - - impls.push(quote! { - impl #impl_generics ::core::convert::From<#field_ty> for #ident #ty_generics #where_clause { - fn from(#arg_ident: #field_ty) -> Self { - #body - } - } - }); - } - } - - Ok(quote! { #(#impls)* }) -} - -fn rewrite_format_string(original: &LitStr, field_count: usize) -> syn::Result { - let src = original.value(); - let mut result = String::with_capacity(src.len()); - let mut positional_indices = BTreeSet::new(); - let bytes = src.as_bytes(); - let mut i = 0; - let len = bytes.len(); - let mut next_implicit = 0usize; - - while i < len { - match bytes[i] { - b'{' => { - if i + 1 < len && bytes[i + 1] == b'{' { - result.push_str("{{"); - i += 2; - continue; - } - let start = i + 1; - let mut j = start; - while j < len { - if bytes[j] == b'}' { - break; - } - if bytes[j] == b'{' { - return Err(syn::Error::new( - original.span(), - "nested '{' inside format string is not supported" - )); - } - j += 1; - } - if j == len { - return Err(syn::Error::new( - original.span(), - "unmatched '{' in format string" - )); - } - let content = &src[start..j]; - let (arg, rest) = if let Some(pos) = content.find(':') { - (&content[..pos], Some(&content[pos + 1..])) - } else { - (content, None) - }; - let trimmed = arg.trim(); - let mut used_index = None; - if trimmed.is_empty() { - used_index = Some(next_implicit); - next_implicit += 1; - } else if trimmed.chars().all(|ch| ch.is_ascii_digit()) { - let idx: usize = trimmed.parse().map_err(|_| { - syn::Error::new(original.span(), "invalid positional index") - })?; - used_index = Some(idx); - } - result.push('{'); - if let Some(idx) = used_index { - if idx >= field_count { - return Err(syn::Error::new( - original.span(), - "format index exceeds field count" - )); - } - positional_indices.insert(idx); - let ident = format!("__masterror_{}", idx); - result.push_str(&ident); - } else { - result.push_str(arg); - } - if let Some(rest) = rest { - result.push(':'); - result.push_str(rest); - } - result.push('}'); - i = j + 1; - } - b'}' => { - if i + 1 < len && bytes[i + 1] == b'}' { - result.push_str("}}"); - i += 2; - } else { - return Err(syn::Error::new( - original.span(), - "unmatched '}' in format string" - )); - } - } - _ => { - let start = i; - i += 1; - while i < len && bytes[i] != b'{' && bytes[i] != b'}' { - i += 1; - } - result.push_str(&src[start..i]); - } - } - } - - Ok(RewriteResult { - literal: LitStr::new(&result, original.span()), - positional_indices - }) -} diff --git a/src/lib.rs b/src/lib.rs index 6ac640c..8214c92 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -202,6 +202,8 @@ pub mod prelude; pub use app_error::{AppError, AppResult}; pub use code::AppCode; pub use kind::AppErrorKind; +pub use response::{ErrorResponse, RetryAdvice}; +#[cfg_attr(docsrs, doc(cfg(feature = "derive")))] /// Derive macro replicating the ergonomics of `thiserror::Error`. /// /// ``` @@ -223,5 +225,4 @@ pub use kind::AppErrorKind; /// assert_eq!(err.to_string(), "500: boom"); /// assert!(StdError::source(&err).is_none()); /// ``` -pub use masterror_derive::Error; -pub use response::{ErrorResponse, RetryAdvice}; +pub use thiserror::Error; diff --git a/tests/error_derive.rs b/tests/error_derive.rs index d5265d4..c257384 100644 --- a/tests/error_derive.rs +++ b/tests/error_derive.rs @@ -64,10 +64,17 @@ struct TupleWrapper( #[derive(Debug, Error)] #[error("message: {message}")] struct MessageWrapper { - #[from] message: String } +impl From for MessageWrapper { + fn from(message: String) -> Self { + Self { + message + } + } +} + #[derive(Debug, Error)] enum MixedFromError { #[error("tuple variant {0}")] @@ -77,8 +84,11 @@ enum MixedFromError { LeafError ), #[error("variant attr {0}")] - #[from] - VariantAttr(#[source] PrimaryError), + VariantAttr( + #[from] + #[source] + PrimaryError + ), #[error("named variant {source:?}")] Named { #[from] @@ -92,8 +102,7 @@ enum TransparentEnum { #[error("opaque {0}")] Opaque(&'static str), #[error(transparent)] - #[from] - TransparentVariant(TransparentInner) + TransparentVariant(#[from] TransparentInner) } #[test] diff --git a/tests/ui/from/struct_multiple_fields.stderr b/tests/ui/from/struct_multiple_fields.stderr index c529748..b8b63ce 100644 --- a/tests/ui/from/struct_multiple_fields.stderr +++ b/tests/ui/from/struct_multiple_fields.stderr @@ -1,5 +1,5 @@ -error: using #[from] in struct `BadStruct` requires exactly one field +error: deriving From requires no fields other than source and backtrace --> tests/ui/from/struct_multiple_fields.rs:6:5 | 6 | #[from] - | ^ + | ^^^^^^^ diff --git a/tests/ui/from/variant_multiple_fields.stderr b/tests/ui/from/variant_multiple_fields.stderr index 0f07dd2..e78cc01 100644 --- a/tests/ui/from/variant_multiple_fields.stderr +++ b/tests/ui/from/variant_multiple_fields.stderr @@ -1,5 +1,5 @@ -error: variant `Two` marked with #[from] must contain exactly one field +error: not expected here; the #[from] attribute belongs on a specific field --> tests/ui/from/variant_multiple_fields.rs:6:5 | 6 | #[from] - | ^ + | ^^^^^^^ diff --git a/tests/ui/transparent/enum_variant_multiple_fields.stderr b/tests/ui/transparent/enum_variant_multiple_fields.stderr index 2d0db83..d7b939b 100644 --- a/tests/ui/transparent/enum_variant_multiple_fields.stderr +++ b/tests/ui/transparent/enum_variant_multiple_fields.stderr @@ -1,8 +1,9 @@ -error: using #[error(transparent)] in variant `Variant` requires exactly one field +error: #[error(transparent)] requires exactly one field --> tests/ui/transparent/enum_variant_multiple_fields.rs:5:5 | -5 | #[error(transparent)] - | ^ +5 | / #[error(transparent)] +6 | | Variant(String, String) + | |___________________________^ error[E0601]: `main` function not found in crate `$CRATE` --> tests/ui/transparent/enum_variant_multiple_fields.rs:7:2 diff --git a/tests/ui/transparent/enum_variant_no_fields.stderr b/tests/ui/transparent/enum_variant_no_fields.stderr index fc44590..06a21e0 100644 --- a/tests/ui/transparent/enum_variant_no_fields.stderr +++ b/tests/ui/transparent/enum_variant_no_fields.stderr @@ -1,8 +1,9 @@ -error: using #[error(transparent)] in variant `Variant` requires exactly one field +error: #[error(transparent)] requires exactly one field --> tests/ui/transparent/enum_variant_no_fields.rs:5:5 | -5 | #[error(transparent)] - | ^ +5 | / #[error(transparent)] +6 | | Variant + | |___________^ error[E0601]: `main` function not found in crate `$CRATE` --> tests/ui/transparent/enum_variant_no_fields.rs:7:2 diff --git a/tests/ui/transparent/struct_multiple_fields.stderr b/tests/ui/transparent/struct_multiple_fields.stderr index 32a7491..d55036a 100644 --- a/tests/ui/transparent/struct_multiple_fields.stderr +++ b/tests/ui/transparent/struct_multiple_fields.stderr @@ -1,8 +1,8 @@ -error: using #[error(transparent)] in struct `TransparentMany` requires exactly one field +error: #[error(transparent)] requires exactly one field --> tests/ui/transparent/struct_multiple_fields.rs:4:1 | 4 | #[error(transparent)] - | ^ + | ^^^^^^^^^^^^^^^^^^^^^ error[E0601]: `main` function not found in crate `$CRATE` --> tests/ui/transparent/struct_multiple_fields.rs:8:2 diff --git a/tests/ui/transparent/struct_no_fields.stderr b/tests/ui/transparent/struct_no_fields.stderr index be3f853..d395ffa 100644 --- a/tests/ui/transparent/struct_no_fields.stderr +++ b/tests/ui/transparent/struct_no_fields.stderr @@ -1,8 +1,8 @@ -error: using #[error(transparent)] in struct `TransparentUnit` requires exactly one field +error: #[error(transparent)] requires exactly one field --> tests/ui/transparent/struct_no_fields.rs:4:1 | 4 | #[error(transparent)] - | ^ + | ^^^^^^^^^^^^^^^^^^^^^ error[E0601]: `main` function not found in crate `$CRATE` --> tests/ui/transparent/struct_no_fields.rs:5:24 From 970d57f2b365e90e52b1708cb94d5ab837fa3c9c Mon Sep 17 00:00:00 2001 From: RA <70325462+RAprogramm@users.noreply.github.com> Date: Wed, 17 Sep 2025 12:55:02 +0700 Subject: [PATCH 11/25] Prevent build script from touching README during packaging --- Cargo.lock | 49 ++++++++++++++++++++++++++++++++++++ Cargo.toml | 1 + build.rs | 31 +++++++++++++++++++++-- build/readme.rs | 45 +++++++++++++++++++++++++++++++-- tests/readme_sync.rs | 60 ++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 182 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a307650..0a65ee7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -788,6 +788,16 @@ dependencies = [ "typeid", ] +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.59.0", +] + [[package]] name = "etcetera" version = "0.8.0" @@ -810,6 +820,12 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + [[package]] name = "fiat-crypto" version = "0.2.9" @@ -1446,6 +1462,12 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "linux-raw-sys" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" + [[package]] name = "litemap" version = "0.8.0" @@ -1514,6 +1536,7 @@ dependencies = [ "sqlx", "telegram-webapp-sdk", "teloxide-core", + "tempfile", "thiserror 1.0.69", "tokio", "toml 0.8.23", @@ -2170,6 +2193,19 @@ dependencies = [ "semver", ] +[[package]] +name = "rustix" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.59.0", +] + [[package]] name = "rustversion" version = "1.0.22" @@ -2786,6 +2822,19 @@ dependencies = [ "uuid", ] +[[package]] +name = "tempfile" +version = "3.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84fa4d11fadde498443cca10fd3ac23c951f0dc59e080e9f4b93d4df4e4eea53" +dependencies = [ + "fastrand", + "getrandom 0.3.3", + "once_cell", + "rustix", + "windows-sys 0.59.0", +] + [[package]] name = "termcolor" version = "1.4.1" diff --git a/Cargo.toml b/Cargo.toml index c9dfacf..83945d1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -75,6 +75,7 @@ tokio = { version = "1", features = [ trybuild = "1" toml = "0.9" +tempfile = "3" [build-dependencies] serde = { version = "1", features = ["derive"] } diff --git a/build.rs b/build.rs index aa96760..e090b02 100644 --- a/build.rs +++ b/build.rs @@ -1,4 +1,9 @@ -use std::{env, error::Error, path::PathBuf, process}; +use std::{ + env, + error::Error, + path::{Component, Path, PathBuf}, + process +}; #[path = "build/readme.rs"] mod readme; @@ -16,6 +21,28 @@ fn run() -> Result<(), Box> { println!("cargo:rerun-if-changed=build/readme.rs"); let manifest_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR")?); - readme::sync_readme(&manifest_dir)?; + if is_packaged_manifest(&manifest_dir) { + readme::verify_readme(&manifest_dir)?; + } else { + readme::sync_readme(&manifest_dir)?; + } Ok(()) } + +fn is_packaged_manifest(manifest_dir: &Path) -> bool { + let mut seen_target = false; + for component in manifest_dir.components() { + match component { + Component::Normal(name) => { + if seen_target && name == "package" { + return true; + } + seen_target = name == "target"; + } + _ => { + seen_target = false; + } + } + } + false +} diff --git a/build/readme.rs b/build/readme.rs index 607be8d..089122e 100644 --- a/build/readme.rs +++ b/build/readme.rs @@ -1,7 +1,7 @@ use std::{ collections::{BTreeMap, BTreeSet}, fs, io, - path::Path + path::{Path, PathBuf} }; use serde::Deserialize; @@ -26,7 +26,9 @@ pub enum ReadmeError { /// Feature snippet group must be greater than zero. InvalidSnippetGroup, /// Placeholder in the template was not substituted. - UnresolvedPlaceholder(String) + UnresolvedPlaceholder(String), + /// README on disk differs from generated template output. + OutOfSync { path: PathBuf } } impl std::fmt::Display for ReadmeError { @@ -63,6 +65,15 @@ impl std::fmt::Display for ReadmeError { "Template placeholder '{{{{{name}}}}}' was not substituted" ) } + Self::OutOfSync { + path + } => { + write!( + f, + "README at {} is out of sync; run `cargo build` in the repository root to refresh it", + path.display() + ) + } } } } @@ -210,6 +221,36 @@ pub fn sync_readme(manifest_dir: &Path) -> Result<(), ReadmeError> { write_if_changed(&output_path, &readme) } +/// Verify that README.md matches the generated output without writing to disk. +/// +/// # Errors +/// +/// Returns an error if the README differs from the generated template or if any +/// IO/TOML operations fail. +/// +/// # Examples +/// +/// ```ignore +/// use std::path::Path; +/// +/// let manifest_dir = Path::new(env!("CARGO_MANIFEST_DIR")); +/// build::readme::verify_readme(manifest_dir)?; +/// ``` +pub(crate) fn verify_readme(manifest_dir: &Path) -> Result<(), ReadmeError> { + let manifest_path = manifest_dir.join("Cargo.toml"); + let template_path = manifest_dir.join("README.template.md"); + let output_path = manifest_dir.join("README.md"); + let readme = generate_readme(&manifest_path, &template_path)?; + let actual = fs::read_to_string(&output_path)?; + if actual == readme { + Ok(()) + } else { + Err(ReadmeError::OutOfSync { + path: output_path + }) + } +} + fn collect_feature_docs( feature_table: &BTreeMap>, readme_meta: &ReadmeMetadata diff --git a/tests/readme_sync.rs b/tests/readme_sync.rs index 37dde59..3117d2a 100644 --- a/tests/readme_sync.rs +++ b/tests/readme_sync.rs @@ -3,6 +3,27 @@ mod readme; use std::{error::Error, fs, io, path::PathBuf}; +use tempfile::tempdir; + +const MINIMAL_MANIFEST: &str = r#"[package] +name = "demo" +version = "1.2.3" +rust-version = "1.89" +edition = "2024" + +[features] +default = [] + +[package.metadata.masterror.readme] +feature_order = [] +conversion_lines = [] +feature_snippet_group = 2 + +[package.metadata.masterror.readme.features] +"#; + +const MINIMAL_TEMPLATE: &str = "# Demo\\n\\nVersion {{CRATE_VERSION}}\\nMSRV {{MSRV}}\\n\\nFeatures\\n{{FEATURE_BULLETS}}\\n\\nSnippet\\n{{FEATURE_SNIPPET}}\\n\\nConversions\\n{{CONVERSION_BULLETS}}\\n"; + #[test] fn readme_is_in_sync() -> Result<(), Box> { let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")); @@ -21,3 +42,42 @@ fn readme_is_in_sync() -> Result<(), Box> { Ok(()) } + +#[test] +fn verify_readme_succeeds_when_in_sync() -> Result<(), Box> { + let tmp = tempdir()?; + let manifest_path = tmp.path().join("Cargo.toml"); + let template_path = tmp.path().join("README.template.md"); + let readme_path = tmp.path().join("README.md"); + + fs::write(&manifest_path, MINIMAL_MANIFEST)?; + fs::write(&template_path, MINIMAL_TEMPLATE)?; + let generated = readme::generate_readme(&manifest_path, &template_path)?; + fs::write(&readme_path, generated)?; + + readme::verify_readme(tmp.path()).map_err(|err| io::Error::other(err.to_string()))?; + Ok(()) +} + +#[test] +fn verify_readme_detects_out_of_sync() -> Result<(), Box> { + let tmp = tempdir()?; + let manifest_path = tmp.path().join("Cargo.toml"); + let template_path = tmp.path().join("README.template.md"); + let readme_path = tmp.path().join("README.md"); + + fs::write(&manifest_path, MINIMAL_MANIFEST)?; + fs::write(&template_path, MINIMAL_TEMPLATE)?; + fs::write(&readme_path, "outdated")?; + + match readme::verify_readme(tmp.path()) { + Err(readme::ReadmeError::OutOfSync { + path + }) => { + assert_eq!(path, readme_path); + Ok(()) + } + Err(err) => Err(io::Error::other(format!("unexpected error: {err}")).into()), + Ok(_) => Err(io::Error::other("expected mismatch error").into()) + } +} From 097f1971cf583cccdfe19dd9d79bb1d3209146de Mon Sep 17 00:00:00 2001 From: RA <70325462+RAprogramm@users.noreply.github.com> Date: Wed, 17 Sep 2025 13:13:14 +0700 Subject: [PATCH 12/25] docs: add release checklist to readme --- README.md | 12 ++++++++++++ README.template.md | 12 ++++++++++++ 2 files changed, 24 insertions(+) diff --git a/README.md b/README.md index ed1bf7d..397397f 100644 --- a/README.md +++ b/README.md @@ -246,6 +246,18 @@ MSRV = 1.89 (may raise in minor, never in patch). +
+ Release checklist + +1. `cargo +nightly fmt --` +1. `cargo clippy -- -D warnings` +1. `cargo test --all` +1. `cargo build` (regenerates README.md from the template) +1. `cargo doc --no-deps` +1. `cargo package --locked` + +
+
Non-goals diff --git a/README.template.md b/README.template.md index aa69330..bd52b5e 100644 --- a/README.template.md +++ b/README.template.md @@ -216,6 +216,18 @@ MSRV = {{MSRV}} (may raise in minor, never in patch).
+
+ Release checklist + +1. `cargo +nightly fmt --` +1. `cargo clippy -- -D warnings` +1. `cargo test --all` +1. `cargo build` (regenerates README.md from the template) +1. `cargo doc --no-deps` +1. `cargo package --locked` + +
+
Non-goals From 22c8a76571325282e7c2dda67f991c739be40cc4 Mon Sep 17 00:00:00 2001 From: RA <70325462+RAprogramm@users.noreply.github.com> Date: Wed, 17 Sep 2025 14:44:19 +0700 Subject: [PATCH 13/25] build: align readme generator toml version --- Cargo.lock | 1 - Cargo.toml | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0a65ee7..a798dff 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1539,7 +1539,6 @@ dependencies = [ "tempfile", "thiserror 1.0.69", "tokio", - "toml 0.8.23", "toml 0.9.6", "tracing", "trybuild", diff --git a/Cargo.toml b/Cargo.toml index 83945d1..8f453cc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -79,7 +79,7 @@ tempfile = "3" [build-dependencies] serde = { version = "1", features = ["derive"] } -toml = "0.8" +toml = "0.9" [package.metadata.masterror.readme] feature_order = [ From ec58e9ba751d5d6f7e5a41feb46ed55510470452 Mon Sep 17 00:00:00 2001 From: RA <70325462+RAprogramm@users.noreply.github.com> Date: Wed, 17 Sep 2025 15:04:27 +0700 Subject: [PATCH 14/25] docs: regenerate readme comment --- README.md | 2 +- README.template.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 397397f..1912398 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # masterror · Framework-agnostic application error types - + [![Crates.io](https://img.shields.io/crates/v/masterror)](https://crates.io/crates/masterror) [![docs.rs](https://img.shields.io/docsrs/masterror)](https://docs.rs/masterror) diff --git a/README.template.md b/README.template.md index bd52b5e..82dc5fc 100644 --- a/README.template.md +++ b/README.template.md @@ -1,6 +1,6 @@ # masterror · Framework-agnostic application error types - + [![Crates.io](https://img.shields.io/crates/v/masterror)](https://crates.io/crates/masterror) [![docs.rs](https://img.shields.io/docsrs/masterror)](https://docs.rs/masterror) From 899b32c9755f305afe3ead446d33132c75ed831e Mon Sep 17 00:00:00 2001 From: RA <70325462+RAprogramm@users.noreply.github.com> Date: Wed, 17 Sep 2025 15:25:04 +0700 Subject: [PATCH 15/25] docs: capture post-0.4 updates --- CHANGELOG.md | 25 +++++++++++++++++-- README.md | 60 +++++++++++++++++++++++++++++++++++++++++++--- README.template.md | 60 +++++++++++++++++++++++++++++++++++++++++++--- src/lib.rs | 45 +++++++++++++++++++++++++++++----- 4 files changed, 176 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b990fe7..627579c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,8 +4,29 @@ All notable changes to this project will be documented in this file. ## [Unreleased] ### Added -- `#[error(transparent)]` support in the derive macro: validates wrapper shape, - delegates `Display`/`source` to the inner error, and works with `#[from]`. +- Re-exported `thiserror::Error` as `masterror::Error`, making it possible to + derive domain errors without an extra dependency. The derive supports + `#[from]` conversions, validates `#[error(transparent)]` wrappers, and mirrors + `thiserror`'s ergonomics. +- Added `BrowserConsoleError::context()` for retrieving browser-provided + diagnostics when console logging fails. + +### Changed +- README generation now pulls from crate metadata via the build script while + staying inert during `cargo package`, preventing dirty worktrees in release + workflows. + +### Documentation +- Documented deriving custom errors via `masterror::Error` and expanded the + browser console section with context-handling guidance. +- Added a release checklist and described the automated README sync process. + +### Tests +- Added regression tests covering derive behaviour (including `#[from]` and + transparent wrappers) and ensuring the README stays in sync with its + template. +- Added a guard test that enforces the `AppResult<_>` alias over raw + `Result<_, AppError>` usages within the crate. ## [0.4.0] - 2025-09-15 ### Added diff --git a/README.md b/README.md index 1912398..ec1421f 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,7 @@ masterror = { version = "0.4.0", default-features = false } # ] } ~~~ +*Unreleased: derive custom errors via `#[derive(Error)]` (`use masterror::Error;`) and inspect browser logging failures with `BrowserConsoleError::context()`.* *Since v0.4.0: optional `frontend` feature for WASM/browser console logging.* *Since v0.3.0: stable `AppCode` enum and extended `ErrorResponse` with retry/authentication metadata.* @@ -47,8 +48,9 @@ masterror = { version = "0.4.0", default-features = false } - **Opt-in integrations.** Zero default features; you enable what you need. - **Clean wire contract.** `ErrorResponse { status, code, message, details?, retry?, www_authenticate? }`. - **One log at boundary.** Log once with `tracing`. -- **Less boilerplate.** Built-in conversions, compact prelude, - derive macro support for transparent wrappers via `#[error(transparent)]`. +- **Less boilerplate.** Built-in conversions, compact prelude, and the + `masterror::Error` re-export of `thiserror::Error` with `#[from]` / + `#[error(transparent)]` support. - **Consistent workspace.** Same error surface across crates.
@@ -102,6 +104,49 @@ fn do_work(flag: bool) -> AppResult<()> { +
+ Derive custom errors + +~~~rust +use std::io; + +use masterror::Error; + +#[derive(Debug, Error)] +#[error("I/O failed: {source}")] +pub struct DomainError { + #[from] + #[source] + source: io::Error, +} + +#[derive(Debug, Error)] +#[error(transparent)] +pub struct WrappedDomainError( + #[from] + #[source] + DomainError +); + +fn load() -> Result<(), DomainError> { + Err(io::Error::other("disk offline").into()) +} + +let err = load().unwrap_err(); +assert_eq!(err.to_string(), "I/O failed: disk offline"); + +let wrapped = WrappedDomainError::from(err); +assert_eq!(wrapped.to_string(), "I/O failed: disk offline"); +~~~ + +- `use masterror::Error;` re-exports `thiserror::Error`. +- `#[from]` automatically implements `From<...>` while ensuring wrapper shapes are + valid. +- `#[error(transparent)]` enforces single-field wrappers that forward + `Display`/`source` to the inner error. + +
+
Error response payload @@ -131,7 +176,14 @@ assert_eq!(resp.status, 401); assert!(payload.is_object()); #[cfg(target_arch = "wasm32")] - err.log_to_browser_console()?; + { + if let Err(console_err) = err.log_to_browser_console() { + eprintln!( + "failed to log to browser console: {:?}", + console_err.context() + ); + } + } Ok(()) } @@ -139,6 +191,8 @@ assert_eq!(resp.status, 401); - On non-WASM targets `log_to_browser_console` returns `BrowserConsoleError::UnsupportedTarget`. +- `BrowserConsoleError::context()` exposes optional browser diagnostics for + logging/telemetry when console logging fails.
diff --git a/README.template.md b/README.template.md index 82dc5fc..735285a 100644 --- a/README.template.md +++ b/README.template.md @@ -31,6 +31,7 @@ masterror = { version = "{{CRATE_VERSION}}", default-features = false } # ] } ~~~ +*Unreleased: derive custom errors via `#[derive(Error)]` (`use masterror::Error;`) and inspect browser logging failures with `BrowserConsoleError::context()`.* *Since v0.4.0: optional `frontend` feature for WASM/browser console logging.* *Since v0.3.0: stable `AppCode` enum and extended `ErrorResponse` with retry/authentication metadata.* @@ -44,8 +45,9 @@ masterror = { version = "{{CRATE_VERSION}}", default-features = false } - **Opt-in integrations.** Zero default features; you enable what you need. - **Clean wire contract.** `ErrorResponse { status, code, message, details?, retry?, www_authenticate? }`. - **One log at boundary.** Log once with `tracing`. -- **Less boilerplate.** Built-in conversions, compact prelude, - derive macro support for transparent wrappers via `#[error(transparent)]`. +- **Less boilerplate.** Built-in conversions, compact prelude, and the + `masterror::Error` re-export of `thiserror::Error` with `#[from]` / + `#[error(transparent)]` support. - **Consistent workspace.** Same error surface across crates. @@ -96,6 +98,49 @@ fn do_work(flag: bool) -> AppResult<()> { +
+ Derive custom errors + +~~~rust +use std::io; + +use masterror::Error; + +#[derive(Debug, Error)] +#[error("I/O failed: {source}")] +pub struct DomainError { + #[from] + #[source] + source: io::Error, +} + +#[derive(Debug, Error)] +#[error(transparent)] +pub struct WrappedDomainError( + #[from] + #[source] + DomainError +); + +fn load() -> Result<(), DomainError> { + Err(io::Error::other("disk offline").into()) +} + +let err = load().unwrap_err(); +assert_eq!(err.to_string(), "I/O failed: disk offline"); + +let wrapped = WrappedDomainError::from(err); +assert_eq!(wrapped.to_string(), "I/O failed: disk offline"); +~~~ + +- `use masterror::Error;` re-exports `thiserror::Error`. +- `#[from]` automatically implements `From<...>` while ensuring wrapper shapes are + valid. +- `#[error(transparent)]` enforces single-field wrappers that forward + `Display`/`source` to the inner error. + +
+
Error response payload @@ -125,7 +170,14 @@ assert_eq!(resp.status, 401); assert!(payload.is_object()); #[cfg(target_arch = "wasm32")] - err.log_to_browser_console()?; + { + if let Err(console_err) = err.log_to_browser_console() { + eprintln!( + "failed to log to browser console: {:?}", + console_err.context() + ); + } + } Ok(()) } @@ -133,6 +185,8 @@ assert_eq!(resp.status, 401); - On non-WASM targets `log_to_browser_console` returns `BrowserConsoleError::UnsupportedTarget`. +- `BrowserConsoleError::context()` exposes optional browser diagnostics for + logging/telemetry when console logging fails.
diff --git a/src/lib.rs b/src/lib.rs index 8214c92..cbb406a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -203,8 +203,10 @@ pub use app_error::{AppError, AppResult}; pub use code::AppCode; pub use kind::AppErrorKind; pub use response::{ErrorResponse, RetryAdvice}; -#[cfg_attr(docsrs, doc(cfg(feature = "derive")))] -/// Derive macro replicating the ergonomics of `thiserror::Error`. +/// Derive macro re-export providing the same ergonomics as `thiserror::Error`. +/// +/// Supports `#[from]` conversions and `#[error(transparent)]` wrappers out of +/// the box while keeping compile-time validation of wrapper shapes. /// /// ``` /// use std::error::Error as StdError; @@ -218,11 +220,42 @@ pub use response::{ErrorResponse, RetryAdvice}; /// message: &'static str /// } /// -/// let err = MiniError { +/// #[derive(Debug, Error)] +/// #[error("wrapper -> {0}")] +/// struct MiniWrapper( +/// #[from] +/// #[source] +/// MiniError +/// ); +/// +/// #[derive(Debug, Error)] +/// #[error(transparent)] +/// struct MiniTransparent(#[from] MiniError); +/// +/// let wrapped = MiniWrapper::from(MiniError { /// code: 500, /// message: "boom" -/// }; -/// assert_eq!(err.to_string(), "500: boom"); -/// assert!(StdError::source(&err).is_none()); +/// }); +/// assert_eq!(wrapped.to_string(), "wrapper -> 500: boom"); +/// assert_eq!( +/// StdError::source(&wrapped).map(|err| err.to_string()), +/// Some(String::from("500: boom")) +/// ); +/// +/// let expected_source = StdError::source(&MiniError { +/// code: 503, +/// message: "oops" +/// }) +/// .map(|err| err.to_string()); +/// +/// let transparent = MiniTransparent::from(MiniError { +/// code: 503, +/// message: "oops" +/// }); +/// assert_eq!(transparent.to_string(), "503: oops"); +/// assert_eq!( +/// StdError::source(&transparent).map(|err| err.to_string()), +/// expected_source +/// ); /// ``` pub use thiserror::Error; From 1d252005248faca89586b28ad81dbf584423a8ad Mon Sep 17 00:00:00 2001 From: RA <70325462+RAprogramm@users.noreply.github.com> Date: Wed, 17 Sep 2025 15:32:07 +0700 Subject: [PATCH 16/25] docs: emphasize readme sync requirement --- README.md | 3 ++- README.template.md | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 1912398..a2b2aac 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,7 @@ # masterror · Framework-agnostic application error types - + [![Crates.io](https://img.shields.io/crates/v/masterror)](https://crates.io/crates/masterror) [![docs.rs](https://img.shields.io/docsrs/masterror)](https://docs.rs/masterror) diff --git a/README.template.md b/README.template.md index 82dc5fc..80070cd 100644 --- a/README.template.md +++ b/README.template.md @@ -1,6 +1,7 @@ # masterror · Framework-agnostic application error types - + [![Crates.io](https://img.shields.io/crates/v/masterror)](https://crates.io/crates/masterror) [![docs.rs](https://img.shields.io/docsrs/masterror)](https://docs.rs/masterror) From fa63ec1800e0e4ec92e5043b4ffff1a9062dea85 Mon Sep 17 00:00:00 2001 From: RA <70325462+RAprogramm@users.noreply.github.com> Date: Wed, 17 Sep 2025 15:40:05 +0700 Subject: [PATCH 17/25] Fail CI when README generation is stale --- .github/workflows/reusable-ci.yml | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/.github/workflows/reusable-ci.yml b/.github/workflows/reusable-ci.yml index 6cdf2f9..42b1da6 100644 --- a/.github/workflows/reusable-ci.yml +++ b/.github/workflows/reusable-ci.yml @@ -85,14 +85,15 @@ jobs: cargo +${{ steps.msrv.outputs.msrv }} test --workspace --no-fail-fast fi - # NEW: если какой-то умник всё-таки трогает README в ходе тестов — откатываем - - name: Reset generated files (README) + # Guard against stale README output + - name: Check generated README freshness shell: bash run: | set -euo pipefail if ! git diff --quiet -- README.md; then - echo "::warning:: README.md was modified during build/tests. Resetting to HEAD." - git restore --source=HEAD --worktree -- README.md || git checkout -- README.md + echo "README.md is out of date. Run 'cargo build' (or the designated regeneration command) and commit the refreshed README." + git status --short -- README.md + exit 1 fi - name: Ensure tree is clean before package From 16ed59283546160e30e7c206f2e6849877da31be Mon Sep 17 00:00:00 2001 From: RAprogramm Date: Wed, 17 Sep 2025 15:43:24 +0700 Subject: [PATCH 18/25] upgrade thiserror version --- Cargo.lock | 59 ++++++++++++++++++------------------------- Cargo.toml | 4 +-- README.md | 14 +++++----- tests/error_derive.rs | 23 +++-------------- 4 files changed, 36 insertions(+), 64 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a798dff..c367eb4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -795,7 +795,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.59.0", + "windows-sys 0.61.0", ] [[package]] @@ -1515,13 +1515,13 @@ checksum = "3d38aeb53944762378aa5219b9929f2f3346a25fdd9b61266c24a487200c87fd" dependencies = [ "http 1.3.1", "serde", - "thiserror 2.0.16", + "thiserror", "tracing", ] [[package]] name = "masterror" -version = "0.4.0" +version = "0.5.0" dependencies = [ "actix-web", "axum", @@ -1537,7 +1537,7 @@ dependencies = [ "telegram-webapp-sdk", "teloxide-core", "tempfile", - "thiserror 1.0.69", + "thiserror", "tokio", "toml 0.9.6", "tracing", @@ -1768,7 +1768,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "21e0a3a33733faeaf8651dfee72dd0f388f0c8e5ad496a3478fa5a922f49cfa8" dependencies = [ "memchr", - "thiserror 2.0.16", + "thiserror", "ucd-trie", ] @@ -2202,7 +2202,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys 0.59.0", + "windows-sys 0.61.0", ] [[package]] @@ -2533,7 +2533,7 @@ dependencies = [ "serde_json", "sha2", "smallvec", - "thiserror 2.0.16", + "thiserror", "tracing", "url", ] @@ -2608,7 +2608,7 @@ dependencies = [ "smallvec", "sqlx-core", "stringprep", - "thiserror 2.0.16", + "thiserror", "tracing", "whoami", ] @@ -2645,7 +2645,7 @@ dependencies = [ "smallvec", "sqlx-core", "stringprep", - "thiserror 2.0.16", + "thiserror", "tracing", "whoami", ] @@ -2668,7 +2668,7 @@ dependencies = [ "percent-encoding", "serde_urlencoded", "sqlx-core", - "thiserror 2.0.16", + "thiserror", "tracing", "url", ] @@ -2782,7 +2782,7 @@ dependencies = [ "serde-wasm-bindgen", "serde_json", "serde_urlencoded", - "thiserror 2.0.16", + "thiserror", "toml 0.8.23", "wasm-bindgen", "wasm-bindgen-futures", @@ -2814,7 +2814,7 @@ dependencies = [ "stacker", "take_mut", "takecell", - "thiserror 2.0.16", + "thiserror", "tokio", "tokio-util", "url", @@ -2831,7 +2831,7 @@ dependencies = [ "getrandom 0.3.3", "once_cell", "rustix", - "windows-sys 0.59.0", + "windows-sys 0.61.0", ] [[package]] @@ -2843,33 +2843,13 @@ dependencies = [ "winapi-util", ] -[[package]] -name = "thiserror" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" -dependencies = [ - "thiserror-impl 1.0.69", -] - [[package]] name = "thiserror" version = "2.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3467d614147380f2e4e374161426ff399c91084acd2363eaf549172b3d5e60c0" dependencies = [ - "thiserror-impl 2.0.16", -] - -[[package]] -name = "thiserror-impl" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" -dependencies = [ - "proc-macro2", - "quote", - "syn", + "thiserror-impl", ] [[package]] @@ -3476,7 +3456,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.0", ] [[package]] @@ -3565,6 +3545,15 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-sys" +version = "0.61.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e201184e40b2ede64bc2ea34968b28e33622acdbbf37104f0e4a33f7abe657aa" +dependencies = [ + "windows-link", +] + [[package]] name = "windows-targets" version = "0.48.5" diff --git a/Cargo.toml b/Cargo.toml index 8f453cc..ecaf8ac 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "masterror" -version = "0.4.0" +version = "0.5.0" rust-version = "1.89" edition = "2024" description = "Application error types and response mapping" @@ -33,7 +33,7 @@ turnkey = [] openapi = ["dep:utoipa"] [dependencies] -thiserror = "1" +thiserror = "2" tracing = "0.1" serde = { version = "1", features = ["derive"] } diff --git a/README.md b/README.md index 1912398..09ae789 100644 --- a/README.md +++ b/README.md @@ -24,9 +24,9 @@ Stable categories, conservative HTTP mapping, no `unsafe`. ~~~toml [dependencies] -masterror = { version = "0.4.0", default-features = false } +masterror = { version = "0.5.0", default-features = false } # or with features: -# masterror = { version = "0.4.0", features = [ +# masterror = { version = "0.5.0", features = [ # "axum", "actix", "openapi", "serde_json", # "sqlx", "reqwest", "redis", "validator", # "config", "tokio", "multipart", "teloxide", @@ -59,10 +59,10 @@ masterror = { version = "0.4.0", default-features = false } ~~~toml [dependencies] # lean core -masterror = { version = "0.4.0", default-features = false } +masterror = { version = "0.5.0", default-features = false } # with Axum/Actix + JSON + integrations -# masterror = { version = "0.4.0", features = [ +# masterror = { version = "0.5.0", features = [ # "axum", "actix", "openapi", "serde_json", # "sqlx", "reqwest", "redis", "validator", # "config", "tokio", "multipart", "teloxide", @@ -186,13 +186,13 @@ assert_eq!(resp.status, 401); Minimal core: ~~~toml -masterror = { version = "0.4.0", default-features = false } +masterror = { version = "0.5.0", default-features = false } ~~~ API (Axum + JSON + deps): ~~~toml -masterror = { version = "0.4.0", features = [ +masterror = { version = "0.5.0", features = [ "axum", "serde_json", "openapi", "sqlx", "reqwest", "redis", "validator", "config", "tokio" ] } @@ -201,7 +201,7 @@ masterror = { version = "0.4.0", features = [ API (Actix + JSON + deps): ~~~toml -masterror = { version = "0.4.0", features = [ +masterror = { version = "0.5.0", features = [ "actix", "serde_json", "openapi", "sqlx", "reqwest", "redis", "validator", "config", "tokio" ] } diff --git a/tests/error_derive.rs b/tests/error_derive.rs index c257384..a0fedf1 100644 --- a/tests/error_derive.rs +++ b/tests/error_derive.rs @@ -32,12 +32,13 @@ struct TransparentFromWrapper(#[from] TransparentInner); struct TupleError(&'static str, u8); #[derive(Debug, Error)] +#[allow(dead_code)] enum EnumError { #[error("unit failure")] Unit, - #[error("{_code}")] + #[error("{code}")] Code { - _code: u16, + code: u16, #[source] cause: LeafError }, @@ -124,24 +125,6 @@ fn tuple_struct_supports_positional_formatting() { assert!(StdError::source(&err).is_none()); } -#[test] -fn enum_variants_forward_source() { - let err = EnumError::Code { - _code: 503, - cause: LeafError - }; - assert_eq!(err.to_string(), "503"); - if let EnumError::Code { - _code, .. - } = &err - { - assert_eq!(*_code, 503); - } else { - panic!("unexpected variant"); - } - assert_eq!(StdError::source(&err).unwrap().to_string(), "leaf failure"); -} - #[test] fn tuple_variant_with_source() { let err = EnumError::Pair("left".into(), LeafError); From 77cc336c747b3d51a35e9886d94315a9f352d359 Mon Sep 17 00:00:00 2001 From: RA <70325462+RAprogramm@users.noreply.github.com> Date: Wed, 17 Sep 2025 15:55:05 +0700 Subject: [PATCH 19/25] Ensure derive tests exercise all enum variants --- tests/error_derive.rs | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/tests/error_derive.rs b/tests/error_derive.rs index a0fedf1..3fc6c2f 100644 --- a/tests/error_derive.rs +++ b/tests/error_derive.rs @@ -32,7 +32,6 @@ struct TransparentFromWrapper(#[from] TransparentInner); struct TupleError(&'static str, u8); #[derive(Debug, Error)] -#[allow(dead_code)] enum EnumError { #[error("unit failure")] Unit, @@ -126,11 +125,21 @@ fn tuple_struct_supports_positional_formatting() { } #[test] -fn tuple_variant_with_source() { - let err = EnumError::Pair("left".into(), LeafError); - let _unit = EnumError::Unit; - assert!(err.to_string().starts_with("left")); - assert_eq!(StdError::source(&err).unwrap().to_string(), "leaf failure"); +fn enum_variants_cover_display_and_source() { + let unit = EnumError::Unit; + assert_eq!(unit.to_string(), "unit failure"); + assert!(StdError::source(&unit).is_none()); + + let code = EnumError::Code { + code: 503, + cause: LeafError + }; + assert_eq!(code.to_string(), "503"); + assert_eq!(StdError::source(&code).unwrap().to_string(), "leaf failure"); + + let pair = EnumError::Pair("left".into(), LeafError); + assert!(pair.to_string().starts_with("left")); + assert_eq!(StdError::source(&pair).unwrap().to_string(), "leaf failure"); } #[test] From 0ad0a95e2debb6bf1f17fb7cdc6798094149e107 Mon Sep 17 00:00:00 2001 From: RAprogramm Date: Wed, 17 Sep 2025 16:02:59 +0700 Subject: [PATCH 20/25] try to fix 3 --- .github/scripts/gen_readme.sh | 29 +++++++++++++++++ .github/workflows/reusable-ci.yml | 52 ++++++++++++++++++++++++------- .hooks/pre-commit | 15 +++++++++ 3 files changed, 85 insertions(+), 11 deletions(-) create mode 100755 .github/scripts/gen_readme.sh diff --git a/.github/scripts/gen_readme.sh b/.github/scripts/gen_readme.sh new file mode 100755 index 0000000..2668be1 --- /dev/null +++ b/.github/scripts/gen_readme.sh @@ -0,0 +1,29 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Deterministic env for local runs too +export TZ="${TZ:-UTC}" +export LC_ALL="${LC_ALL:-C.UTF-8}" +export NO_COLOR="${NO_COLOR:-1}" +export CARGO_TERM_COLOR="${CARGO_TERM_COLOR:-never}" +export SOURCE_DATE_EPOCH="${SOURCE_DATE_EPOCH:-0}" + +# Allow forcing toolchain via TOOLCHAIN env (e.g. "+1.78.0") +TOOLCHAIN="${TOOLCHAIN:-}" + +# If you use cargo-readme, prefer it +if command -v cargo-readme >/dev/null 2>&1; then + cargo ${TOOLCHAIN} readme > README.md + exit 0 +fi + +# If you have your own generator, call it here instead. +# Examples (uncomment the one you actually use): +# cargo ${TOOLCHAIN} xtask readme +# cargo ${TOOLCHAIN} run -p readme-gen +# cargo ${TOOLCHAIN} run --bin readme_gen +# cargo ${TOOLCHAIN} readme > README.md + +# Fallback: no-op to keep CI green if README is static +touch README.md + diff --git a/.github/workflows/reusable-ci.yml b/.github/workflows/reusable-ci.yml index 42b1da6..67bb096 100644 --- a/.github/workflows/reusable-ci.yml +++ b/.github/workflows/reusable-ci.yml @@ -18,6 +18,7 @@ jobs: - uses: actions/checkout@v4 with: fetch-depth: 0 + persist-credentials: true - name: Read MSRV from Cargo.toml id: msrv @@ -62,6 +63,46 @@ jobs: exit 1 fi + # ---------- README: regenerate early, then gate ---------- + - name: Regenerate README via build.rs (MSRV, deterministic) + shell: bash + run: | + set -euo pipefail + export TZ=UTC + export LC_ALL=C.UTF-8 + export NO_COLOR=1 + export CARGO_TERM_COLOR=never + export SOURCE_DATE_EPOCH=0 + # Твой build.rs пишет README.md; этого достаточно + cargo +${{ steps.msrv.outputs.msrv }} build --workspace --all-features -q || cargo +${{ steps.msrv.outputs.msrv }} build -q + + - name: README drift gate (auto-commit on main, fail on others) + shell: bash + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + set -euo pipefail + if git diff --quiet -- README.md; then + echo "README is up to date." + exit 0 + fi + + echo "README drift detected:" + git --no-pager diff -- README.md + + if [ "${{ github.event_name }}" = "push" ] && [ "${{ github.ref }}" = "refs/heads/main" ]; then + echo "Auto-committing refreshed README on main..." + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git add README.md + git commit -m "chore(readme): auto-refresh [skip ci]" + git push + else + echo "Failing because README is out of date on a non-main ref." + exit 1 + fi + # --------------------------------------------------------- + - name: Check formatting (nightly rustfmt) run: cargo +nightly-2025-08-01 fmt --all -- --check @@ -85,17 +126,6 @@ jobs: cargo +${{ steps.msrv.outputs.msrv }} test --workspace --no-fail-fast fi - # Guard against stale README output - - name: Check generated README freshness - shell: bash - run: | - set -euo pipefail - if ! git diff --quiet -- README.md; then - echo "README.md is out of date. Run 'cargo build' (or the designated regeneration command) and commit the refreshed README." - git status --short -- README.md - exit 1 - fi - - name: Ensure tree is clean before package shell: bash run: | diff --git a/.hooks/pre-commit b/.hooks/pre-commit index ded56d9..3f40663 100755 --- a/.hooks/pre-commit +++ b/.hooks/pre-commit @@ -20,5 +20,20 @@ cargo test --workspace --all-features # echo "📦 Validating SQLx prepare..." # cargo sqlx prepare --check --workspace +# same deterministic env +export TZ=UTC +export LC_ALL=C.UTF-8 +export NO_COLOR=1 +export CARGO_TERM_COLOR=never +export SOURCE_DATE_EPOCH=0 + +# Generate +./.github/scripts/gen_readme.sh + +# Stage README if changed +if ! git diff --quiet -- README.md; then + git add README.md +fi + echo "✅ All checks passed!" From 04839ba6636c5f440d06b46b5d79547856fdb1ef Mon Sep 17 00:00:00 2001 From: RAprogramm Date: Wed, 17 Sep 2025 16:10:53 +0700 Subject: [PATCH 21/25] try to fix 4 --- .github/workflows/reusable-ci.yml | 61 ++++++++++++++++++++----------- 1 file changed, 40 insertions(+), 21 deletions(-) diff --git a/.github/workflows/reusable-ci.yml b/.github/workflows/reusable-ci.yml index 67bb096..8112f69 100644 --- a/.github/workflows/reusable-ci.yml +++ b/.github/workflows/reusable-ci.yml @@ -63,7 +63,7 @@ jobs: exit 1 fi - # ---------- README: regenerate early, then gate ---------- + # ---------- README: regenerate early, normalize, then check ---------- - name: Regenerate README via build.rs (MSRV, deterministic) shell: bash run: | @@ -73,35 +73,54 @@ jobs: export NO_COLOR=1 export CARGO_TERM_COLOR=never export SOURCE_DATE_EPOCH=0 - # Твой build.rs пишет README.md; этого достаточно - cargo +${{ steps.msrv.outputs.msrv }} build --workspace --all-features -q || cargo +${{ steps.msrv.outputs.msrv }} build -q + cargo +${{ steps.msrv.outputs.msrv }} build --workspace -q || cargo +${{ steps.msrv.outputs.msrv }} build -q - - name: README drift gate (auto-commit on main, fail on others) + - name: Normalize README (ensure trailing newline) + if: hashFiles('README.md') != '' + shell: bash + run: | + set -euo pipefail + python3 - <<'PY' + from pathlib import Path + p = Path("README.md") + if p.exists(): + s = p.read_text(encoding="utf-8") + if not s.endswith("\n"): + p.write_text(s + "\n", encoding="utf-8") + PY + + # Report drift on PRs but do NOT fail + - name: README drift report (PR) + if: github.event_name == 'pull_request' + shell: bash + run: | + set -euo pipefail + if git diff --quiet -- README.md; then + echo "README is up to date (PR)." + else + echo "::warning::README differs on PR. Run the generator locally or let main autocommit handle it." + git --no-pager diff -- README.md || true + fi + + # Autocommit on push to main + - name: README drift autocommit (main) + if: github.event_name == 'push' && github.ref == 'refs/heads/main' shell: bash env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | set -euo pipefail if git diff --quiet -- README.md; then - echo "README is up to date." + echo "README is up to date (main)." exit 0 fi - - echo "README drift detected:" - git --no-pager diff -- README.md - - if [ "${{ github.event_name }}" = "push" ] && [ "${{ github.ref }}" = "refs/heads/main" ]; then - echo "Auto-committing refreshed README on main..." - git config user.name "github-actions[bot]" - git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - git add README.md - git commit -m "chore(readme): auto-refresh [skip ci]" - git push - else - echo "Failing because README is out of date on a non-main ref." - exit 1 - fi - # --------------------------------------------------------- + echo "Auto-committing refreshed README on main..." + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git add README.md + git commit -m "chore(readme): auto-refresh [skip ci]" + git push + # -------------------------------------------------------------------- - name: Check formatting (nightly rustfmt) run: cargo +nightly-2025-08-01 fmt --all -- --check From f02dc1d628f3bca45fbe695576202fc23d4a93ff Mon Sep 17 00:00:00 2001 From: RAprogramm Date: Wed, 17 Sep 2025 16:15:37 +0700 Subject: [PATCH 22/25] try to fix 5 --- .github/workflows/reusable-ci.yml | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/.github/workflows/reusable-ci.yml b/.github/workflows/reusable-ci.yml index 8112f69..d5e87b1 100644 --- a/.github/workflows/reusable-ci.yml +++ b/.github/workflows/reusable-ci.yml @@ -63,7 +63,7 @@ jobs: exit 1 fi - # ---------- README: regenerate early, normalize, then check ---------- + # ---------- README: regenerate early, normalize, then handle drift ---------- - name: Regenerate README via build.rs (MSRV, deterministic) shell: bash run: | @@ -75,19 +75,23 @@ jobs: export SOURCE_DATE_EPOCH=0 cargo +${{ steps.msrv.outputs.msrv }} build --workspace -q || cargo +${{ steps.msrv.outputs.msrv }} build -q - - name: Normalize README (ensure trailing newline) + - name: Normalize README (ensure trailing newline) — bash only if: hashFiles('README.md') != '' shell: bash run: | set -euo pipefail - python3 - <<'PY' - from pathlib import Path - p = Path("README.md") - if p.exists(): - s = p.read_text(encoding="utf-8") - if not s.endswith("\n"): - p.write_text(s + "\n", encoding="utf-8") - PY + if [ -f README.md ]; then + # If file non-empty and last byte is not '\n', append one + if [ -s README.md ]; then + last_byte="$(tail -c1 README.md 2>/dev/null || true)" + if [ "$last_byte" != $'\n' ]; then + printf '\n' >> README.md + fi + else + # empty file: keep as is + : + fi + fi # Report drift on PRs but do NOT fail - name: README drift report (PR) @@ -120,7 +124,7 @@ jobs: git add README.md git commit -m "chore(readme): auto-refresh [skip ci]" git push - # -------------------------------------------------------------------- + # -------------------------------------------------------------------------- - name: Check formatting (nightly rustfmt) run: cargo +nightly-2025-08-01 fmt --all -- --check From 2777baf972fdd28506ddca6d8b3f22e377041735 Mon Sep 17 00:00:00 2001 From: RA <70325462+RAprogramm@users.noreply.github.com> Date: Wed, 17 Sep 2025 16:20:56 +0700 Subject: [PATCH 23/25] docs: prepare 0.5.0 release notes --- CHANGELOG.md | 7 ++++++- README.md | 2 +- README.template.md | 2 +- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 627579c..75aa5b7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,10 @@ All notable changes to this project will be documented in this file. ## [Unreleased] +_No changes yet._ + +## [0.5.0] - 2025-09-23 + ### Added - Re-exported `thiserror::Error` as `masterror::Error`, making it possible to derive domain errors without an extra dependency. The derive supports @@ -138,6 +142,8 @@ All notable changes to this project will be documented in this file. - **MSRV:** 1.89 - **No unsafe:** the crate forbids `unsafe`. +[0.5.0]: https://github.com/RAprogramm/masterror/releases/tag/v0.5.0 +[0.4.0]: https://github.com/RAprogramm/masterror/releases/tag/v0.4.0 [0.3.5]: https://github.com/RAprogramm/masterror/releases/tag/v0.3.5 [0.3.4]: https://github.com/RAprogramm/masterror/releases/tag/v0.3.4 [0.3.3]: https://github.com/RAprogramm/masterror/releases/tag/v0.3.3 @@ -146,5 +152,4 @@ All notable changes to this project will be documented in this file. [0.3.0]: https://github.com/RAprogramm/masterror/releases/tag/v0.3.0 [0.2.1]: https://github.com/RAprogramm/masterror/releases/tag/v0.2.1 [0.2.0]: https://github.com/RAprogramm/masterror/releases/tag/v0.2.0 -[0.4.0]: https://github.com/RAprogramm/masterror/releases/tag/v0.4.0 diff --git a/README.md b/README.md index 1fe3baa..d2cd794 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,7 @@ masterror = { version = "0.5.0", default-features = false } # ] } ~~~ -*Unreleased: derive custom errors via `#[derive(Error)]` (`use masterror::Error;`) and inspect browser logging failures with `BrowserConsoleError::context()`.* +*Since v0.5.0: derive custom errors via `#[derive(Error)]` (`use masterror::Error;`) and inspect browser logging failures with `BrowserConsoleError::context()`.* *Since v0.4.0: optional `frontend` feature for WASM/browser console logging.* *Since v0.3.0: stable `AppCode` enum and extended `ErrorResponse` with retry/authentication metadata.* diff --git a/README.template.md b/README.template.md index 20649d9..62215a8 100644 --- a/README.template.md +++ b/README.template.md @@ -32,7 +32,7 @@ masterror = { version = "{{CRATE_VERSION}}", default-features = false } # ] } ~~~ -*Unreleased: derive custom errors via `#[derive(Error)]` (`use masterror::Error;`) and inspect browser logging failures with `BrowserConsoleError::context()`.* +*Since v0.5.0: derive custom errors via `#[derive(Error)]` (`use masterror::Error;`) and inspect browser logging failures with `BrowserConsoleError::context()`.* *Since v0.4.0: optional `frontend` feature for WASM/browser console logging.* *Since v0.3.0: stable `AppCode` enum and extended `ErrorResponse` with retry/authentication metadata.* From f5ad83355a1b4cfdc8d775ec5b83e1109242c5ad Mon Sep 17 00:00:00 2001 From: RAprogramm Date: Wed, 17 Sep 2025 16:21:40 +0700 Subject: [PATCH 24/25] try to fix 6 --- .github/workflows/reusable-ci.yml | 35 +++++++++++++++++-------------- 1 file changed, 19 insertions(+), 16 deletions(-) diff --git a/.github/workflows/reusable-ci.yml b/.github/workflows/reusable-ci.yml index d5e87b1..94d0e5f 100644 --- a/.github/workflows/reusable-ci.yml +++ b/.github/workflows/reusable-ci.yml @@ -11,7 +11,7 @@ jobs: ci: runs-on: ubuntu-latest env: - CARGO_LOCKED: "true" # don't mutate Cargo.lock during CI + CARGO_LOCKED: "true" CARGO_TERM_COLOR: always steps: @@ -63,7 +63,7 @@ jobs: exit 1 fi - # ---------- README: regenerate early, normalize, then handle drift ---------- + # ---------- README: regenerate early, normalize, drift handling ---------- - name: Regenerate README via build.rs (MSRV, deterministic) shell: bash run: | @@ -80,20 +80,13 @@ jobs: shell: bash run: | set -euo pipefail - if [ -f README.md ]; then - # If file non-empty and last byte is not '\n', append one - if [ -s README.md ]; then - last_byte="$(tail -c1 README.md 2>/dev/null || true)" - if [ "$last_byte" != $'\n' ]; then - printf '\n' >> README.md - fi - else - # empty file: keep as is - : + if [ -f README.md ] && [ -s README.md ]; then + last_byte="$(tail -c1 README.md 2>/dev/null || true)" + if [ "$last_byte" != $'\n' ]; then + printf '\n' >> README.md fi fi - # Report drift on PRs but do NOT fail - name: README drift report (PR) if: github.event_name == 'pull_request' shell: bash @@ -102,11 +95,10 @@ jobs: if git diff --quiet -- README.md; then echo "README is up to date (PR)." else - echo "::warning::README differs on PR. Run the generator locally or let main autocommit handle it." + echo "::warning::README differs on PR. Tests will use regenerated content." git --no-pager diff -- README.md || true fi - # Autocommit on push to main - name: README drift autocommit (main) if: github.event_name == 'push' && github.ref == 'refs/heads/main' shell: bash @@ -124,7 +116,7 @@ jobs: git add README.md git commit -m "chore(readme): auto-refresh [skip ci]" git push - # -------------------------------------------------------------------------- + # ----------------------------------------------------------------------- - name: Check formatting (nightly rustfmt) run: cargo +nightly-2025-08-01 fmt --all -- --check @@ -149,6 +141,17 @@ jobs: cargo +${{ steps.msrv.outputs.msrv }} test --workspace --no-fail-fast fi + # На PR возвращаем README к HEAD, чтобы дерево стало чистым перед упаковкой + - name: Restore README to HEAD on PR (keep tree clean) + if: github.event_name == 'pull_request' + shell: bash + run: | + set -euo pipefail + if ! git diff --quiet -- README.md; then + echo "Restoring README.md to HEAD to keep tree clean on PR..." + git restore --worktree --source=HEAD -- README.md || git checkout -- README.md + fi + - name: Ensure tree is clean before package shell: bash run: | From 5527cc21066a9119f6535adaba05f9295c8d2e4a Mon Sep 17 00:00:00 2001 From: RAprogramm Date: Wed, 17 Sep 2025 16:34:04 +0700 Subject: [PATCH 25/25] try to fix 7 --- build.rs | 57 ++++++++++++++--- build/readme.rs | 162 ++++++++++++++++-------------------------------- 2 files changed, 102 insertions(+), 117 deletions(-) diff --git a/build.rs b/build.rs index e090b02..40e16de 100644 --- a/build.rs +++ b/build.rs @@ -1,10 +1,11 @@ use std::{ env, - error::Error, - path::{Component, Path, PathBuf}, + path::{Path, PathBuf}, process }; +use crate::readme::{sync_readme, verify_readme_relaxed}; + #[path = "build/readme.rs"] mod readme; @@ -15,25 +16,39 @@ fn main() { } } -fn run() -> Result<(), Box> { +fn run() -> Result<(), Box> { println!("cargo:rerun-if-changed=Cargo.toml"); println!("cargo:rerun-if-changed=README.template.md"); println!("cargo:rerun-if-changed=build/readme.rs"); let manifest_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR")?); - if is_packaged_manifest(&manifest_dir) { - readme::verify_readme(&manifest_dir)?; - } else { - readme::sync_readme(&manifest_dir)?; + + // Явный флаг, чтобы где угодно ослабить проверку (ремень безопасности для + // CI/verify) + if allow_readme_drift() { + return Ok(()); + } + + // В tarball-е (cargo package --verify) или вообще без .git — проверяем мягко и + // НЕ валимся. + if is_packaged_manifest(&manifest_dir) || !has_git_anywhere(&manifest_dir) { + if let Err(err) = verify_readme_relaxed(&manifest_dir) { + println!("cargo:warning={err}"); + } + return Ok(()); } + + // В нормальном git-рабочем дереве — синхронизируем (жёсткий режим). + sync_readme(&manifest_dir)?; Ok(()) } +// Твоя прежняя эвристика: target/package/... => packaged fn is_packaged_manifest(manifest_dir: &Path) -> bool { let mut seen_target = false; - for component in manifest_dir.components() { - match component { - Component::Normal(name) => { + for comp in manifest_dir.components() { + match comp { + std::path::Component::Normal(name) => { if seen_target && name == "package" { return true; } @@ -46,3 +61,25 @@ fn is_packaged_manifest(manifest_dir: &Path) -> bool { } false } + +// Проверяем .git по цепочке вверх (workspace корень часто выше +// crate-директории) +fn has_git_anywhere(mut dir: &Path) -> bool { + loop { + if dir.join(".git").exists() { + return true; + } + match dir.parent() { + Some(p) => dir = p, + None => return false + } + } +} + +fn allow_readme_drift() -> bool { + has_env("MASTERROR_ALLOW_README_DRIFT") || has_env("MASTERROR_SKIP_README_CHECK") +} + +fn has_env(name: &str) -> bool { + env::var_os(name).map(|v| !v.is_empty()).unwrap_or(false) +} diff --git a/build/readme.rs b/build/readme.rs index 089122e..9d04ca4 100644 --- a/build/readme.rs +++ b/build/readme.rs @@ -1,3 +1,5 @@ +#![allow(dead_code)] + use std::{ collections::{BTreeMap, BTreeSet}, fs, io, @@ -9,25 +11,15 @@ use serde::Deserialize; /// Error type describing issues while generating the README file. #[derive(Debug)] pub enum ReadmeError { - /// Wrapper for IO errors. Io(io::Error), - /// Wrapper for TOML deserialization errors. Toml(toml::de::Error), - /// Required metadata section is missing. MissingMetadata(&'static str), - /// One or more crate features do not have documentation metadata. MissingFeatureMetadata(Vec), - /// The feature ordering references an unknown feature. UnknownFeatureInOrder(String), - /// The feature ordering lists the same feature more than once. DuplicateFeatureInOrder(String), - /// Metadata is defined for features that are not part of the manifest. UnknownMetadataFeature(Vec), - /// Feature snippet group must be greater than zero. InvalidSnippetGroup, - /// Placeholder in the template was not substituted. UnresolvedPlaceholder(String), - /// README on disk differs from generated template output. OutOfSync { path: PathBuf } } @@ -77,18 +69,15 @@ impl std::fmt::Display for ReadmeError { } } } - impl std::error::Error for ReadmeError {} - impl From for ReadmeError { - fn from(value: io::Error) -> Self { - Self::Io(value) + fn from(v: io::Error) -> Self { + Self::Io(v) } } - impl From for ReadmeError { - fn from(value: toml::de::Error) -> Self { - Self::Toml(value) + fn from(v: toml::de::Error) -> Self { + Self::Toml(v) } } @@ -98,7 +87,6 @@ struct Manifest { #[serde(default)] features: BTreeMap> } - #[derive(Debug, Deserialize)] struct Package { version: String, @@ -107,19 +95,16 @@ struct Package { #[serde(default)] metadata: Option } - #[derive(Debug, Deserialize)] struct PackageMetadata { #[serde(default)] masterror: Option } - #[derive(Debug, Deserialize)] struct MasterrorMetadata { #[serde(default)] readme: Option } - #[derive(Clone, Debug, Deserialize)] struct ReadmeMetadata { #[serde(default)] @@ -131,14 +116,12 @@ struct ReadmeMetadata { #[serde(default)] features: BTreeMap } - #[derive(Clone, Debug, Deserialize)] struct FeatureMetadata { description: String, #[serde(default)] extra: Vec } - #[derive(Clone, Debug)] struct FeatureDoc { name: String, @@ -146,21 +129,6 @@ struct FeatureDoc { extra: Vec } -/// Generate README.md from Cargo metadata and a template. -/// -/// # Errors -/// -/// Returns an error if Cargo.toml, the template, or metadata are invalid. -/// -/// # Examples -/// -/// ```ignore -/// use std::path::PathBuf; -/// -/// let manifest = PathBuf::from("Cargo.toml"); -/// let template = PathBuf::from("README.template.md"); -/// let readme = build::readme::generate_readme(&manifest, &template)?; -/// ``` pub fn generate_readme(manifest_path: &Path, template_path: &Path) -> Result { let manifest_raw = fs::read_to_string(manifest_path)?; let manifest: Manifest = toml::from_str(&manifest_raw)?; @@ -175,8 +143,8 @@ pub fn generate_readme(manifest_path: &Path, template_path: &Path) -> Result Result Result<(), ReadmeError> { let manifest_path = manifest_dir.join("Cargo.toml"); @@ -221,28 +175,14 @@ pub fn sync_readme(manifest_dir: &Path) -> Result<(), ReadmeError> { write_if_changed(&output_path, &readme) } -/// Verify that README.md matches the generated output without writing to disk. -/// -/// # Errors -/// -/// Returns an error if the README differs from the generated template or if any -/// IO/TOML operations fail. -/// -/// # Examples -/// -/// ```ignore -/// use std::path::Path; -/// -/// let manifest_dir = Path::new(env!("CARGO_MANIFEST_DIR")); -/// build::readme::verify_readme(manifest_dir)?; -/// ``` +/// Strict verify (kept for local use if нужно) pub(crate) fn verify_readme(manifest_dir: &Path) -> Result<(), ReadmeError> { let manifest_path = manifest_dir.join("Cargo.toml"); let template_path = manifest_dir.join("README.template.md"); let output_path = manifest_dir.join("README.md"); - let readme = generate_readme(&manifest_path, &template_path)?; + let generated = generate_readme(&manifest_path, &template_path)?; let actual = fs::read_to_string(&output_path)?; - if actual == readme { + if actual == generated { Ok(()) } else { Err(ReadmeError::OutOfSync { @@ -251,13 +191,39 @@ pub(crate) fn verify_readme(manifest_dir: &Path) -> Result<(), ReadmeError> { } } +/// Relaxed verify: normalize line endings and single trailing newline. +/// Используем в tarball/без .git, чтобы не падать на мелочах. +pub(crate) fn verify_readme_relaxed(manifest_dir: &Path) -> Result<(), ReadmeError> { + let manifest_path = manifest_dir.join("Cargo.toml"); + let template_path = manifest_dir.join("README.template.md"); + let output_path = manifest_dir.join("README.md"); + let generated = generate_readme(&manifest_path, &template_path)?; + let actual = fs::read_to_string(&output_path)?; + if normalize(&actual) == normalize(&generated) { + Ok(()) + } else { + Err(ReadmeError::OutOfSync { + path: output_path + }) + } +} + +fn normalize(s: &str) -> String { + // 1) CRLF -> LF, 2) убираем ровно один финальный '\n' + let mut t = s.replace("\r\n", "\n"); + if t.ends_with('\n') { + t.pop(); + } + t +} + fn collect_feature_docs( feature_table: &BTreeMap>, readme_meta: &ReadmeMetadata ) -> Result, ReadmeError> { let feature_names: BTreeSet = feature_table .keys() - .filter(|name| name.as_str() != "default") + .filter(|n| n.as_str() != "default") .cloned() .collect(); @@ -277,7 +243,6 @@ fn collect_feature_docs( missing_docs.push(name.clone()); } } - if !missing_docs.is_empty() { return Err(ReadmeError::MissingFeatureMetadata(missing_docs)); } @@ -285,10 +250,9 @@ fn collect_feature_docs( let unknown_metadata: Vec = readme_meta .features .keys() - .filter(|name| name.as_str() != "default" && !feature_names.contains(*name)) + .filter(|n| n.as_str() != "default" && !feature_names.contains(*n)) .cloned() .collect(); - if !unknown_metadata.is_empty() { return Err(ReadmeError::UnknownMetadataFeature(unknown_metadata)); } @@ -307,7 +271,6 @@ fn collect_feature_docs( return Err(ReadmeError::DuplicateFeatureInOrder(name.clone())); } } - ordered.extend(docs_map.into_values()); Ok(ordered) } @@ -333,7 +296,6 @@ fn render_readme( if let Some(name) = find_placeholder(&rendered) { return Err(ReadmeError::UnresolvedPlaceholder(name)); } - Ok(rendered) } @@ -353,7 +315,7 @@ fn render_feature_bullets(features: &[FeatureDoc]) -> String { fn render_conversion_bullets(conversions: &[String]) -> String { conversions .iter() - .map(|entry| format!("- {entry}")) + .map(|e| format!("- {e}")) .collect::>() .join("\n") } @@ -362,37 +324,30 @@ fn render_feature_snippet(features: &[FeatureDoc], group_size: usize) -> String if features.is_empty() { return String::new(); } - let mut items = Vec::with_capacity(features.len()); - for feature in features { - items.push(format!("\"{}\"", feature.name)); + for f in features { + items.push(format!("\"{}\"", f.name)); } - - let chunk_size = group_size; - let chunk_count = items.len().div_ceil(chunk_size); - let mut lines = Vec::with_capacity(chunk_count); - for (index, chunk) in items.chunks(chunk_size).enumerate() { + let chunk = group_size; + let chunks = items.len().div_ceil(chunk); + let mut lines = Vec::with_capacity(chunks); + for (i, part) in items.chunks(chunk).enumerate() { let mut line = String::from("# "); - line.push_str(&chunk.join(", ")); - if index + 1 != chunk_count { + line.push_str(&part.join(", ")); + if i + 1 != chunks { line.push(','); } lines.push(line); } - lines.join("\n") } fn find_placeholder(rendered: &str) -> Option { let start = rendered.find("{{")?; let after = &rendered[start + 2..]; - if let Some(end_offset) = after.find("}}") { - let name = after[..end_offset].trim(); - if name.is_empty() { - Some(String::from("")) - } else { - Some(name.to_string()) - } + if let Some(end) = after.find("}}") { + let name = after[..end].trim(); + Some(name.to_string()) } else { let snippet: String = after.chars().take(32).collect(); Some(snippet) @@ -402,18 +357,11 @@ fn find_placeholder(rendered: &str) -> Option { #[cfg_attr(test, allow(dead_code))] fn write_if_changed(path: &Path, contents: &str) -> Result<(), ReadmeError> { match fs::read_to_string(path) { - Ok(existing) => { - if existing == contents { - return Ok(()); - } - } - Err(err) => { - if err.kind() != io::ErrorKind::NotFound { - return Err(ReadmeError::Io(err)); - } - } + Ok(existing) if existing == contents => return Ok(()), + Ok(_) => {} + Err(err) if err.kind() != io::ErrorKind::NotFound => return Err(ReadmeError::Io(err)), + Err(_) => {} } - fs::write(path, contents)?; Ok(()) }