diff --git a/crates/tighterror-build/src/coder/formatter.rs b/crates/tighterror-build/src/coder/formatter.rs index 468c46f..0326216 100644 --- a/crates/tighterror-build/src/coder/formatter.rs +++ b/crates/tighterror-build/src/coder/formatter.rs @@ -27,6 +27,7 @@ fn add_newlines(file: String) -> String { r"^[[:space:]]*const fn", r"^[[:space:]]*pub fn", r"^[[:space:]]*#\[.*\]$", + r"^[[:space:]]*impl", ]) .unwrap(); let rg_post = RegexSet::new([ diff --git a/crates/tighterror-build/src/coder/generator/helpers.rs b/crates/tighterror-build/src/coder/generator/helpers.rs index a9f6499..f8a95c3 100644 --- a/crates/tighterror-build/src/coder/generator/helpers.rs +++ b/crates/tighterror-build/src/coder/generator/helpers.rs @@ -73,3 +73,11 @@ pub fn tests_mod_ident() -> Ident { pub fn categories_mod_ident() -> Ident { format_ident!("{}", idents::CATEGORY_CONSTS_MOD) } + +pub fn variants_mod_ident() -> Ident { + format_ident!("{}", idents::VARIANTS_MOD) +} + +pub fn types_mod_ident() -> Ident { + format_ident!("{}", idents::TYPES_MOD) +} diff --git a/crates/tighterror-build/src/coder/generator/module.rs b/crates/tighterror-build/src/coder/generator/module.rs index 18718bd..34a06cb 100644 --- a/crates/tighterror-build/src/coder/generator/module.rs +++ b/crates/tighterror-build/src/coder/generator/module.rs @@ -42,6 +42,7 @@ impl<'a> ModuleGenerator<'a> { let error_tokens = self.error_tokens(); let category_constants = self.category_constants_tokens(); let error_kind_constants = self.error_kind_constants_tokens(); + let variants_module = self.variants_module_tokens(); let test = self.test_tokens(); Ok(quote! { #module_doc @@ -51,6 +52,7 @@ impl<'a> ModuleGenerator<'a> { #private_modules #category_constants #error_kind_constants + #variants_module #test }) } @@ -237,7 +239,7 @@ impl<'a> ModuleGenerator<'a> { let const_ident = format_ident!("{}", e.name); if let Some(ref display) = e.display { quote! { - const #const_ident: &str = #display + pub(crate) const #const_ident: &str = #display } } else { quote! { @@ -258,7 +260,7 @@ impl<'a> ModuleGenerator<'a> { }; quote! { - mod #cat_mod_ident { + pub(crate) mod #cat_mod_ident { #names_mod_import #(#const_iter);* ; @@ -648,6 +650,228 @@ impl<'a> ModuleGenerator<'a> { } } + fn needs_variants_module(&self) -> bool { + self.module.has_variant_types() + } + + fn variants_module_tokens(&self) -> TokenStream { + if !self.needs_variants_module() { + return TokenStream::default(); + } + + let variant_types_mod_tokens = self.variant_types_module_tokens(); + let variants_mod = variants_mod_ident(); + quote! { + #[doc = " Error variant types and constants."] + pub mod #variants_mod { + #variant_types_mod_tokens + } + } + } + + fn variant_types_module_tokens(&self) -> TokenStream { + let mut tokens = TokenStream::default(); + + if !self.module.has_variant_types() { + return tokens; + } + + let kinds_mod = error_kinds_mod_ident(); + let private_mod = private_mod_ident(); + let category_names_mod = category_names_mod_ident(); + let categories_mod = categories_mod_ident(); + let display_mod = error_displays_mod_ident(); + let error_names_mod = error_names_mod_ident(); + let err_kind_name = self.err_kind_name_ident(); + let err_name = self.err_name_ident(); + let cat_name = self.err_cat_name_ident(); + let use_display_mod = if self.module.has_display_variant_types() { + quote! { #display_mod, } + } else { + TokenStream::default() + }; + let use_tokens = quote! { + super::{ + #use_display_mod #private_mod, #category_names_mod, + #error_names_mod, + #categories_mod, #kinds_mod, #err_kind_name, + #err_name, #cat_name + } + }; + if self.module.flat_kinds() { + tokens = quote! { use super::#use_tokens; }; + } + + for c in &self.module.categories { + if !self.module.cat_has_variant_types(c) { + continue; + } + + let cvt = self.category_variant_types_tokens(c); + if self.module.flat_kinds() { + tokens = quote! { + #tokens + #cvt + }; + } else { + let cat_mod_ident = format_ident!("{}", c.module_name()); + let cat_mod_doc = doc_tokens(&format!("{} category error variant types.", c.name)); + + tokens = quote! { + #tokens + + #cat_mod_doc + pub mod #cat_mod_ident { + use super::super::#use_tokens; + #cvt + } + }; + } + } + + let types_mod = types_mod_ident(); + quote! { + #[doc = " Error variant types."] + pub mod #types_mod { + #tokens + } + } + } + + fn category_variant_types_tokens(&self, c: &CategorySpec) -> TokenStream { + let mut tokens = TokenStream::default(); + + for e in &c.errors { + if !self.module.err_has_variant_type(c, e) { + continue; + } + let evt = self.error_variant_type_tokens(c, e); + tokens = quote! { + #tokens + #evt + }; + } + + tokens + } + + fn error_variant_type_tokens(&self, c: &CategorySpec, e: &ErrorSpec) -> TokenStream { + let display_mod = error_displays_mod_ident(); + let kinds_mod = error_kinds_mod_ident(); + let private_mod = private_mod_ident(); + let category_names_mod = category_names_mod_ident(); + let categories_mod = categories_mod_ident(); + let error_names_mod = error_names_mod_ident(); + let cat_const_ident = format_ident!("{}", c.ident_name()); + let err_kind_name_ident = self.err_kind_name_ident(); + let err_name_ident = self.err_name_ident(); + let cat_mod = format_ident!("{}", c.module_name()); + let cat_name_ident = self.err_cat_name_ident(); + let err_ident = format_ident!("{}", e.name); + let err_kind_const_ident = format_ident!("{}", e.name); + let var_type_name = e.variant_type_name(); + let var_type_ident = format_ident!("{}", var_type_name); + let err_doc = doc_tokens(self.module.err_kind_const_doc(c, e)); + let display = if e.display.is_some() { + quote! { #display_mod::#cat_mod::#err_ident } + } else { + quote! { ::NAME } + }; + let error_trait = if self.module.error_trait(self.spec.main.no_std) { + quote! { impl std::error::Error for #var_type_ident {} } + } else { + TokenStream::default() + }; + let err_kind_tokens = if self.module.flat_kinds() { + quote! { #kinds_mod::#err_kind_const_ident } + } else { + quote! { #kinds_mod::#cat_mod::#err_kind_const_ident } + }; + quote! { + #err_doc + #[derive(Clone, Copy)] + #[non_exhaustive] + pub struct #var_type_ident; + + impl #var_type_ident { + #[doc = " Returns the struct name."] + #[inline] + pub fn name(&self) -> &'static str { + <#var_type_ident as tighterror::VariantType>::NAME + } + + #[doc = " Returns the error kind constant."] + #[inline] + pub fn kind(&self) -> #err_kind_name_ident { + <#var_type_ident as tighterror::VariantType>::KIND + } + + #[doc = " Returns the error category constant."] + #[inline] + pub fn category(&self) -> #cat_name_ident { + <#var_type_ident as tighterror::VariantType>::CATEGORY + } + } + + impl core::fmt::Display for #var_type_ident { + #[inline] + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + f.pad(#display) + } + } + + impl core::fmt::Debug for #var_type_ident { + #[inline] + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + f.debug_struct(#var_type_name) + .field("cat", &#private_mod::Ident(#category_names_mod::#cat_const_ident)) + .field("var", &#private_mod::Ident(#error_names_mod::#cat_mod::#err_ident)) + .finish() + } + } + + impl tighterror::VariantType for #var_type_ident { + type R = #private_mod::R; + type Category = #cat_name_ident; + type Kind = #err_kind_name_ident; + + const CATEGORY: Self::Category = #categories_mod::#cat_const_ident; + const KIND: Self::Kind = #err_kind_tokens; + const NAME: &'static str = #var_type_name; + } + + impl core::convert::From<#var_type_ident> for #err_kind_name_ident { + #[inline] + fn from(_: #var_type_ident) -> #err_kind_name_ident { + <#var_type_ident as tighterror::VariantType>::KIND + } + } + + impl core::convert::From<#var_type_ident> for #err_name_ident { + #[inline] + fn from(_: #var_type_ident) -> Self { + <#var_type_ident as tighterror::VariantType>::KIND.into() + } + } + + impl core::convert::From<#var_type_ident> for core::result::Result { + #[inline] + fn from(_: #var_type_ident) -> Self { + <#var_type_ident as tighterror::VariantType>::KIND.into() + } + } + + impl core::convert::From<#var_type_ident> for core::result::Result { + #[inline] + fn from(v: #var_type_ident) -> Self { + Err(v) + } + } + + #error_trait + } + } + fn test_tokens(&self) -> TokenStream { let tests_mod = tests_mod_ident(); if self.opts.test { @@ -676,6 +900,10 @@ impl<'a> ModuleGenerator<'a> { let ut_err_kind_category = self.ut_err_kind_category(); let ut_err_kind_from_value = self.ut_err_kind_from_value(); let ut_err_display = self.ut_err_display(); + let ut_variant_types_display = self.ut_variant_types_display(); + let ut_variant_types_to_kind = self.ut_variant_types_to_kind(); + let ut_variant_types_to_error = self.ut_variant_types_to_error(); + let ut_variant_types_to_result = self.ut_variant_types_to_result(); quote! { #ut_category_name @@ -689,6 +917,10 @@ impl<'a> ModuleGenerator<'a> { #ut_err_kind_category #ut_err_kind_from_value #ut_err_display + #ut_variant_types_display + #ut_variant_types_to_kind + #ut_variant_types_to_error + #ut_variant_types_to_result } } @@ -947,6 +1179,152 @@ impl<'a> ModuleGenerator<'a> { } } + fn ut_variant_types_display(&self) -> TokenStream { + if !self.module.has_variant_types() { + return TokenStream::default(); + } + + let iter = self.module.categories.iter().map(|c| { + let err_iter = c.errors.iter().map(|e| { + if !self.module.err_has_variant_type(c, e) { + return TokenStream::default(); + } + let add_cat_mod = !self.module.flat_kinds(); + let var_type_ident = self.err_var_type_tokens(c, e, add_cat_mod); + let display = if let Some(ref d) = e.display { + d.clone() + } else { + e.variant_type_name() + }; + quote! { + assert_eq!(format!("{}", #var_type_ident), #display); + } + }); + quote! { + #(#err_iter)* + } + }); + + let variants_mod = variants_mod_ident(); + let types_mod = types_mod_ident(); + quote! { + #[test] + fn test_variant_types_display() { + use #variants_mod::#types_mod::*; + #(#iter)* + } + } + } + + fn ut_variant_types_to_kind(&self) -> TokenStream { + if !self.module.has_variant_types() { + return TokenStream::default(); + } + + let err_kinds_mod = error_kinds_mod_ident(); + let err_kind_name = self.err_kind_name_ident(); + + let iter = self.module.categories.iter().map(|c| { + let err_iter = c.errors.iter().map(|e| { + if !self.module.err_has_variant_type(c, e) { + return TokenStream::default(); + } + let add_cat_mod = !self.module.flat_kinds(); + let err_ident = self.err_const_tokens(c, e, add_cat_mod); + let var_type_ident = self.err_var_type_tokens(c, e, add_cat_mod); + quote! { + assert_eq!(#err_kind_name::from(#var_type_ident), #err_kinds_mod::#err_ident); + } + }); + quote! { + #(#err_iter)* + } + }); + + let variants_mod = variants_mod_ident(); + let types_mod = types_mod_ident(); + quote! { + #[test] + fn test_variant_types_to_kind() { + use #variants_mod::#types_mod::*; + #(#iter)* + } + } + } + + fn ut_variant_types_to_error(&self) -> TokenStream { + if !self.module.has_variant_types() { + return TokenStream::default(); + } + + let err_kinds_mod = error_kinds_mod_ident(); + let err_name = self.err_name_ident(); + + let iter = self.module.categories.iter().map(|c| { + let err_iter = c.errors.iter().map(|e| { + if !self.module.err_has_variant_type(c, e) { + return TokenStream::default(); + } + let add_cat_mod = !self.module.flat_kinds(); + let err_ident = self.err_const_tokens(c, e, add_cat_mod); + let var_type_ident = self.err_var_type_tokens(c, e, add_cat_mod); + quote! { + assert_eq!(#err_name::from(#var_type_ident).kind(), #err_kinds_mod::#err_ident); + } + }); + quote! { + #(#err_iter)* + } + }); + + let variants_mod = variants_mod_ident(); + let types_mod = types_mod_ident(); + quote! { + #[test] + fn test_variant_types_to_error() { + use #variants_mod::#types_mod::*; + #(#iter)* + } + } + } + + fn ut_variant_types_to_result(&self) -> TokenStream { + if !self.module.has_variant_types() { + return TokenStream::default(); + } + + let err_kinds_mod = error_kinds_mod_ident(); + let err_name = self.err_name_ident(); + + let iter = self.module.categories.iter().map(|c| { + let err_iter = c.errors.iter().map(|e| { + if !self.module.err_has_variant_type(c, e) { + return TokenStream::default(); + } + let add_cat_mod = !self.module.flat_kinds(); + let err_ident = self.err_const_tokens(c, e, add_cat_mod); + let var_type_ident = self.err_var_type_tokens(c, e, add_cat_mod); + quote! { + let res: Result<(), #err_name> = #var_type_ident.into(); + assert_eq!(res.unwrap_err().kind(), #err_kinds_mod::#err_ident); + } + }); + quote! { + #(#err_iter)* + } + }); + + let variants_mod = variants_mod_ident(); + let types_mod = types_mod_ident(); + quote! { + #[test] + fn test_variant_types_to_result() { + use #variants_mod::#types_mod::*; + #(#iter)* + } + } + } + fn err_cat_name_ident(&self) -> Ident { format_ident!("{}", self.module.err_cat_name()) } @@ -1028,6 +1406,25 @@ impl<'a> ModuleGenerator<'a> { } } + fn err_var_type_tokens( + &self, + c: &CategorySpec, + e: &ErrorSpec, + add_cat_mod: bool, + ) -> TokenStream { + let var_type_ident = format_ident!("{}", e.variant_type_name()); + let cat_mod_ident = format_ident!("{}", c.module_name()); + if add_cat_mod { + quote! { + #cat_mod_ident::#var_type_ident + } + } else { + quote! { + #var_type_ident + } + } + } + fn n_categories_literal(&self) -> Literal { Literal::usize_unsuffixed(self.module.categories.len()) } diff --git a/crates/tighterror-build/src/coder/idents.rs b/crates/tighterror-build/src/coder/idents.rs index aa3a5ad..5772390 100644 --- a/crates/tighterror-build/src/coder/idents.rs +++ b/crates/tighterror-build/src/coder/idents.rs @@ -7,9 +7,11 @@ pub const ERROR_DISPLAYS_MOD: &str = "_d"; pub const PRIVATE_MOD: &str = "_p"; pub const CATEGORY_CONSTS_MOD: &str = "category"; pub const ERROR_KINDS_MOD: &str = "kind"; +pub const VARIANTS_MOD: &str = "variant"; +pub const TYPES_MOD: &str = "types"; // singular `type` is rust-reserved pub const TESTS_MOD: &str = "test"; -const ROOT_LEVEL: [&str; 10] = [ +const ROOT_LEVEL: [&str; 11] = [ ERROR, ERROR_CATEGORY, ERROR_KIND, @@ -19,6 +21,7 @@ const ROOT_LEVEL: [&str; 10] = [ PRIVATE_MOD, CATEGORY_CONSTS_MOD, ERROR_KINDS_MOD, + VARIANTS_MOD, TESTS_MOD, ]; diff --git a/crates/tighterror-build/src/errors.rs b/crates/tighterror-build/src/errors.rs index 415d209..7b765dd 100644 --- a/crates/tighterror-build/src/errors.rs +++ b/crates/tighterror-build/src/errors.rs @@ -309,29 +309,31 @@ mod _n { } mod _d { - mod parser { - const BAD_IDENTIFIER_CHARACTERS: &str = "Identifier contains unsupported characters."; - const BAD_IDENTIFIER_CASE: &str = "Identifier is specified in an unsupported case."; - const BAD_KEYWORD_TYPE: &str = "Specification keyword is not a String."; - const BAD_MODULE_IDENTIFIER: &str = "Identifier is not valid on module-level."; - const BAD_NAME: &str = "Invalid name."; - const BAD_OBJECT_ATTRIBUTE: &str = "An object attribute is invalid."; - const BAD_SPEC_FILE_EXTENSION: &str = + pub(crate) mod parser { + pub(crate) const BAD_IDENTIFIER_CHARACTERS: &str = + "Identifier contains unsupported characters."; + pub(crate) const BAD_IDENTIFIER_CASE: &str = + "Identifier is specified in an unsupported case."; + pub(crate) const BAD_KEYWORD_TYPE: &str = "Specification keyword is not a String."; + pub(crate) const BAD_MODULE_IDENTIFIER: &str = "Identifier is not valid on module-level."; + pub(crate) const BAD_NAME: &str = "Invalid name."; + pub(crate) const BAD_OBJECT_ATTRIBUTE: &str = "An object attribute is invalid."; + pub(crate) const BAD_SPEC_FILE_EXTENSION: &str = "Specification filename extension is not supported or is missing."; - const BAD_TOML: &str = "TOML deserialization has failed."; - const BAD_ROOT_LEVEL_KEYWORD: &str = + pub(crate) const BAD_TOML: &str = "TOML deserialization has failed."; + pub(crate) const BAD_ROOT_LEVEL_KEYWORD: &str = "Specification contains an invalid root-level keyword."; - const BAD_VALUE_TYPE: &str = "Specification value type is invalid."; - const BAD_YAML: &str = "YAML deserialization has failed."; - const EMPTY_IDENTIFIER: &str = "An identifier cannot be an empty string."; - const EMPTY_LIST: &str = "Empty list of objects is not allowed."; - const FAILED_TO_OPEN_SPEC_FILE: &str = "Specification file couldn't be opened."; - const MISSING_ATTRIBUTE: &str = "Specification lacks a mandatory attribute."; - const MUTUALLY_EXCLUSIVE_KEYWORDS: &str = + pub(crate) const BAD_VALUE_TYPE: &str = "Specification value type is invalid."; + pub(crate) const BAD_YAML: &str = "YAML deserialization has failed."; + pub(crate) const EMPTY_IDENTIFIER: &str = "An identifier cannot be an empty string."; + pub(crate) const EMPTY_LIST: &str = "Empty list of objects is not allowed."; + pub(crate) const FAILED_TO_OPEN_SPEC_FILE: &str = "Specification file couldn't be opened."; + pub(crate) const MISSING_ATTRIBUTE: &str = "Specification lacks a mandatory attribute."; + pub(crate) const MUTUALLY_EXCLUSIVE_KEYWORDS: &str = "Specification contains mutually exclusive keywords."; - const NON_UNIQUE_NAME: &str = "A name is not unique."; - const SPEC_FILE_NOT_FOUND: &str = "Specification file couldn't be found."; - const NAME_COLLISION: &str = "Collision of names between different items."; + pub(crate) const NON_UNIQUE_NAME: &str = "A name is not unique."; + pub(crate) const SPEC_FILE_NOT_FOUND: &str = "Specification file couldn't be found."; + pub(crate) const NAME_COLLISION: &str = "Collision of names between different items."; pub static A: [&str; 19] = [ BAD_IDENTIFIER_CHARACTERS, BAD_IDENTIFIER_CASE, @@ -355,17 +357,17 @@ mod _d { ]; } - mod coder { - const CATEGORY_REQUIRED: &str = "At least one category must be defined."; - const ERROR_REQUIRED: &str = "At least one error must be defined."; - const FAILED_TO_PARSE_TOKENS: &str = "Generated code tokens couldn't be parsed."; - const FAILED_TO_READ_OUTPUT_FILE: &str = "Output file couldn't be read."; - const FAILED_TO_WRITE_OUTPUT_FILE: &str = "Output file couldn't be written."; - const RUSTFMT_FAILED: &str = "Rustfmt tool exited with an error."; - const RUSTFMT_NOT_FOUND: &str = "Rustfmt tool isn't found."; - const TOO_MANY_BITS: &str = + pub(crate) mod coder { + pub(crate) const CATEGORY_REQUIRED: &str = "At least one category must be defined."; + pub(crate) const ERROR_REQUIRED: &str = "At least one error must be defined."; + pub(crate) const FAILED_TO_PARSE_TOKENS: &str = "Generated code tokens couldn't be parsed."; + pub(crate) const FAILED_TO_READ_OUTPUT_FILE: &str = "Output file couldn't be read."; + pub(crate) const FAILED_TO_WRITE_OUTPUT_FILE: &str = "Output file couldn't be written."; + pub(crate) const RUSTFMT_FAILED: &str = "Rustfmt tool exited with an error."; + pub(crate) const RUSTFMT_NOT_FOUND: &str = "Rustfmt tool isn't found."; + pub(crate) const TOO_MANY_BITS: &str = "The number of required bits exceeds the largest supported type u64."; - const OUTPUT_PATH_NOT_DIRECTORY: &str = "Output path is not a directory."; + pub(crate) const OUTPUT_PATH_NOT_DIRECTORY: &str = "Output path is not a directory."; pub static A: [&str; 9] = [ CATEGORY_REQUIRED, ERROR_REQUIRED, @@ -394,6 +396,7 @@ mod _p { const _: () = assert!(KIND_BITS <= R::BITS as usize); const _: () = assert!(CAT_BITS <= usize::BITS as usize); pub(super) struct Ident<'a>(pub(super) &'a str); + impl<'a> core::fmt::Debug for Ident<'a> { #[inline] fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { diff --git a/crates/tighterror-build/src/parser/helpers.rs b/crates/tighterror-build/src/parser/helpers.rs index 26d5a86..71e369e 100644 --- a/crates/tighterror-build/src/parser/helpers.rs +++ b/crates/tighterror-build/src/parser/helpers.rs @@ -3,7 +3,7 @@ use crate::{ common::casing, errors::{kind::parser::*, TbError}, parser::kws, - spec::ModuleSpec, + spec::{ErrorSpec, ModuleSpec}, }; use convert_case::Case; use regex::Regex; @@ -87,6 +87,10 @@ pub fn check_category_name(name: &str) -> Result<(), TbError> { check_name(name, "CategoryObject::name", Case::UpperCamel) } +pub fn check_variant_type_name(name: &str) -> Result<(), TbError> { + check_name(name, "ErrorObject::variant_type", Case::UpperCamel) +} + pub fn check_module_name(name: &str) -> Result<(), TbError> { if name.is_empty() { log::error!("module name cannot be an empty string"); @@ -162,6 +166,7 @@ where pub fn check_name_collisions(m: &ModuleSpec) -> Result<(), TbError> { check_struct_names_collision(m)?; + check_variant_type_names_collision(m)?; Ok(()) } @@ -182,3 +187,44 @@ pub fn check_struct_names_collision(m: &ModuleSpec) -> Result<(), TbError> { } Ok(()) } + +fn check_variant_type_names_collision(m: &ModuleSpec) -> Result<(), TbError> { + for c in &m.categories { + for e in &c.errors { + check_variant_type_names_collision_impl(m, e)?; + } + } + Ok(()) +} + +fn check_variant_type_names_collision_impl(m: &ModuleSpec, e: &ErrorSpec) -> Result<(), TbError> { + let name = e.variant_type_name(); + + if name == m.err_name() { + log::error!( + "{} equals error type name: {} {}", + kws::ERR_NAME, + e.name, + name + ); + return NAME_COLLISION.into(); + } else if name == m.err_cat_name() { + log::error!( + "{} equals error category name: {} {}", + kws::ERR_CAT_NAME, + e.name, + name + ); + return NAME_COLLISION.into(); + } else if name == m.err_kind_name() { + log::error!( + "{} equals error kind name: {} {}", + kws::ERR_KIND_NAME, + e.name, + name + ); + return NAME_COLLISION.into(); + } + + Ok(()) +} diff --git a/crates/tighterror-build/src/parser/kws.rs b/crates/tighterror-build/src/parser/kws.rs index 10f5e98..e420966 100644 --- a/crates/tighterror-build/src/parser/kws.rs +++ b/crates/tighterror-build/src/parser/kws.rs @@ -20,6 +20,7 @@ pub const MODULES: &str = "modules"; pub const CATEGORY: &str = "category"; pub const CATEGORIES: &str = "categories"; pub const FLAT_KINDS: &str = "flat_kinds"; +pub const VARIANT_TYPE: &str = "variant_type"; pub const ROOT_KWS: [&str; 6] = [MAIN, ERRORS, MODULE, MODULES, CATEGORY, CATEGORIES]; pub const REQUIRED_ROOT_KWS: [&str; 3] = [ERRORS, CATEGORIES, MODULES]; @@ -31,7 +32,7 @@ pub const MUTUALLY_EXCLUSIVE_ROOT_KWS: [(&str, &str); 6] = [ (CATEGORIES, MODULES), (MODULE, MODULES), ]; -pub const ALL_KWS: [&str; 22] = [ +pub const ALL_KWS: [&str; 23] = [ ERR_CAT_DOC, DISPLAY, DOC, @@ -54,6 +55,7 @@ pub const ALL_KWS: [&str; 22] = [ CATEGORY, CATEGORIES, FLAT_KINDS, + VARIANT_TYPE, ]; #[inline] diff --git a/crates/tighterror-build/src/parser/toml.rs b/crates/tighterror-build/src/parser/toml.rs index c80acbd..d31432f 100644 --- a/crates/tighterror-build/src/parser/toml.rs +++ b/crates/tighterror-build/src/parser/toml.rs @@ -276,6 +276,10 @@ impl ModuleParser { mod_spec.flat_kinds = Some(v2bool(v, kws::FLAT_KINDS)?); } + if let Some(v) = t.remove(kws::VARIANT_TYPE) { + mod_spec.oes.variant_type = Some(v2bool(v, kws::VARIANT_TYPE)?); + } + if let Some((k, _)) = t.into_iter().next() { let key = check_key(&k)?; log::error!("invalid ModuleObject attribute: {}", key); @@ -410,6 +414,24 @@ impl ErrorParser { err_spec.oes.doc_from_display = Some(v2bool(v, kws::DOC_FROM_DISPLAY)?); } + if let Some(v) = t.remove(kws::VARIANT_TYPE) { + match v { + Value::Boolean(b) => err_spec.oes.variant_type = Some(b), + Value::String(s) => { + check_variant_type_name(&s)?; + err_spec.oes.variant_type = Some(true); + err_spec.variant_type_name = Some(s); + } + _ => { + log::error!( + "ErrorObject::{} must be a String or a Boolean: deserialized {v:?}", + kws::VARIANT_TYPE + ); + return BAD_VALUE_TYPE.into(); + } + } + } + if let Some((k, _)) = t.into_iter().next() { let key = check_key(&k)?; log::error!("invalid ErrorObject attribute: {}", key); @@ -469,6 +491,10 @@ impl CategoryParser { cat_spec.errors = ErrorListParser::value(v)?; } + if let Some(v) = t.remove(kws::VARIANT_TYPE) { + cat_spec.oes.variant_type = Some(v2bool(v, kws::VARIANT_TYPE)?); + } + if let Some((k, _)) = t.into_iter().next() { log::error!("invalid CategoryObject attribute: {}", k); return BAD_OBJECT_ATTRIBUTE.into(); diff --git a/crates/tighterror-build/src/parser/toml/test_toml_parser.rs b/crates/tighterror-build/src/parser/toml/test_toml_parser.rs index 093e2ba..b25cbd6 100644 --- a/crates/tighterror-build/src/parser/toml/test_toml_parser.rs +++ b/crates/tighterror-build/src/parser/toml/test_toml_parser.rs @@ -105,6 +105,7 @@ fn test_err_doc_from_display() { name: "TEST_ERROR".into(), oes: OverridableErrorSpec { doc_from_display: Some(good.1), + ..Default::default() }, ..Default::default() }; @@ -250,7 +251,9 @@ errors = [ display: Some("An error description.".into()), oes: OverridableErrorSpec { doc_from_display: Some(false), + ..Default::default() }, + ..Default::default() }; let spec = spec_from_err(err); @@ -312,7 +315,9 @@ doc_from_display = true display: Some("An error description.".into()), oes: OverridableErrorSpec { doc_from_display: Some(false), + ..Default::default() }, + ..Default::default() }; let err4 = ErrorSpec { name: "ERR2".into(), @@ -324,6 +329,7 @@ doc_from_display = true display: Some("A third one.".into()), oes: OverridableErrorSpec { doc_from_display: Some(true), + ..Default::default() }, ..Default::default() }; @@ -368,6 +374,7 @@ fn test_module_doc_from_display() { let module = ModuleSpec { oes: OverridableErrorSpec { doc_from_display: Some(good.1), + ..Default::default() }, ..Default::default() }; @@ -973,6 +980,7 @@ fn test_category_doc_from_display() { name: IMPLICIT_CATEGORY_NAME.into(), oes: OverridableErrorSpec { doc_from_display: Some(good.1), + ..Default::default() }, ..Default::default() }; @@ -1120,6 +1128,7 @@ errors = ["DUMMY_ERR2"] doc: Some("First category.".into()), oes: OverridableErrorSpec { doc_from_display: Some(false), + ..Default::default() }, errors: vec![ErrorSpec { name: "DUMMY_ERR".into(), @@ -1131,6 +1140,7 @@ errors = ["DUMMY_ERR2"] name: "Cat2".into(), oes: OverridableErrorSpec { doc_from_display: Some(true), + ..Default::default() }, errors: vec![ErrorSpec { name: "DUMMY_ERR2".into(), @@ -1442,3 +1452,116 @@ errors = ["ANOTHER_ERR"] "#; assert!(TomlParser::parse_str(s).is_ok()); } + +#[test] +fn test_error_variant_type() { + log_init(); + + let s = r#" +[[errors]] +name = "MY_ERR" +variant_type = "MyErr" +"#; + + let err = ErrorSpec { + name: "MY_ERR".into(), + oes: OverridableErrorSpec { + variant_type: Some(true), + ..Default::default() + }, + variant_type_name: Some("MyErr".into()), + ..Default::default() + }; + let spec = spec_from_err(err); + let res = TomlParser::parse_str(s).unwrap(); + assert_eq!(res, spec); + + for val in [true, false] { + let s = format!("[[errors]]\nname = \"MY_ERR\"\nvariant_type = {val}"); + let err = ErrorSpec { + name: "MY_ERR".into(), + oes: OverridableErrorSpec { + variant_type: Some(val), + ..Default::default() + }, + ..Default::default() + }; + let spec = spec_from_err(err); + let res = TomlParser::parse_str(&s).unwrap(); + assert_eq!(res, spec); + } +} + +#[test] +fn test_error_variant_type_bad_name() { + log_init(); + + let test_cases = &[ + ("MY_ERR", BAD_IDENTIFIER_CHARACTERS), + ("my_err", BAD_IDENTIFIER_CHARACTERS), + ("myErr", BAD_IDENTIFIER_CASE), + ("myerr", BAD_IDENTIFIER_CASE), + ]; + + for tc in test_cases { + let s = format!("[[errors]]\nname = \"MY_ERR\"\nvariant_type = \"{}\"", tc.0); + assert_eq!(TomlParser::parse_str(&s).unwrap_err().kind(), tc.1); + } +} + +#[test] +fn test_category_variant_type() { + log_init(); + + for val in [true, false] { + let s = format!("[category]\nvariant_type = {val}\n[[errors]]\nname = \"DUMMY_ERR\""); + let cat = CategorySpec { + name: "General".into(), + oes: OverridableErrorSpec { + variant_type: Some(val), + ..Default::default() + }, + ..Default::default() + }; + let spec = spec_from_category(cat); + let res = TomlParser::parse_str(&s).unwrap(); + assert_eq!(res, spec); + } + + for bad in BAD_BOOLEANS { + let s = format!( + "[category]\nvariant_type = {}\n[[errors]]\nname = \"DUMMY_ERR\"", + bad.0 + ); + let res = TomlParser::parse_str(&s); + assert_eq!(res.unwrap_err().kind(), bad.1); + } +} + +#[test] +fn test_module_variant_type() { + log_init(); + + for val in [true, false] { + let s = format!("[module]\nvariant_type = {val}\n[[errors]]\nname = \"DUMMY_ERR\""); + let module = ModuleSpec { + oes: OverridableErrorSpec { + variant_type: Some(val), + ..Default::default() + }, + ..Default::default() + }; + let spec = spec_from_module(module); + let res = TomlParser::parse_str(&s).unwrap(); + assert_eq!(res, spec); + } + + for bad in BAD_BOOLEANS { + let s = format!( + "[module]\nvariant_type = {}\n[[errors]]\nname = \"DUMMY_ERR\"", + bad.0 + ); + let res = TomlParser::parse_str(&s); + assert_eq!(res.unwrap_err().kind(), bad.1); + } +} diff --git a/crates/tighterror-build/src/parser/yaml.rs b/crates/tighterror-build/src/parser/yaml.rs index 9920bea..5c8e068 100644 --- a/crates/tighterror-build/src/parser/yaml.rs +++ b/crates/tighterror-build/src/parser/yaml.rs @@ -285,6 +285,10 @@ impl ModuleParser { mod_spec.flat_kinds = Some(v2bool(v, kws::FLAT_KINDS)?); } + if let Some(v) = m.remove(kws::VARIANT_TYPE) { + mod_spec.oes.variant_type = Some(v2bool(v, kws::VARIANT_TYPE)?); + } + if let Some((k, _)) = m.into_iter().next() { let key = v2key(k)?; error!("invalid ModuleObject attribute: {}", key); @@ -476,6 +480,24 @@ impl ErrorParser { err_spec.oes.doc_from_display = Some(v2bool(v, kws::DOC_FROM_DISPLAY)?); } + if let Some(v) = m.remove(kws::VARIANT_TYPE) { + match v { + Value::Bool(b) => err_spec.oes.variant_type = Some(b), + Value::String(s) => { + check_variant_type_name(&s)?; + err_spec.oes.variant_type = Some(true); + err_spec.variant_type_name = Some(s); + } + _ => { + error!( + "ErrorObject::{} must be a String or a Bool: deserialized {v:?}", + kws::VARIANT_TYPE + ); + return BAD_VALUE_TYPE.into(); + } + } + } + if let Some((k, _)) = m.into_iter().next() { let key = v2key(k)?; error!("invalid ErrorObject attribute: {}", key); @@ -535,6 +557,10 @@ impl CategoryParser { cat_spec.errors = ErrorListParser::value(v)?; } + if let Some(v) = m.remove(kws::VARIANT_TYPE) { + cat_spec.oes.variant_type = Some(v2bool(v, kws::VARIANT_TYPE)?); + } + if let Some((k, _)) = m.into_iter().next() { let key = v2key(k)?; error!("invalid CategoryObject attribute: {}", key); diff --git a/crates/tighterror-build/src/parser/yaml/test_yaml_parser.rs b/crates/tighterror-build/src/parser/yaml/test_yaml_parser.rs index c2421bd..3b6dc88 100644 --- a/crates/tighterror-build/src/parser/yaml/test_yaml_parser.rs +++ b/crates/tighterror-build/src/parser/yaml/test_yaml_parser.rs @@ -110,6 +110,7 @@ fn test_err_doc_from_display() { name: "TEST_ERROR".into(), oes: OverridableErrorSpec { doc_from_display: Some(good.1), + ..Default::default() }, ..Default::default() }; @@ -253,7 +254,9 @@ errors: display: Some("An error description.".into()), oes: OverridableErrorSpec { doc_from_display: Some(false), + ..Default::default() }, + ..Default::default() }; let spec = spec_from_err(err); let res = YamlParser::parse_str(s).unwrap(); @@ -293,7 +296,9 @@ errors: display: Some("An error description.".into()), oes: OverridableErrorSpec { doc_from_display: Some(false), + ..Default::default() }, + ..Default::default() }; let err4 = ErrorSpec { name: "ERR2".into(), @@ -305,6 +310,7 @@ errors: display: Some("A third one.".into()), oes: OverridableErrorSpec { doc_from_display: Some(true), + ..Default::default() }, ..Default::default() }; @@ -360,6 +366,7 @@ fn test_module_doc_from_display() { let module = ModuleSpec { oes: OverridableErrorSpec { doc_from_display: Some(good.1), + ..Default::default() }, ..Default::default() }; @@ -993,6 +1000,7 @@ fn test_category_doc_from_display() { name: IMPLICIT_CATEGORY_NAME.into(), oes: OverridableErrorSpec { doc_from_display: Some(good.1), + ..Default::default() }, ..Default::default() }; @@ -1154,6 +1162,7 @@ categories: doc: Some("First category.".into()), oes: OverridableErrorSpec { doc_from_display: Some(false), + ..Default::default() }, errors: vec![ErrorSpec { name: "DUMMY_ERR".into(), @@ -1165,6 +1174,7 @@ categories: name: "Cat2".into(), oes: OverridableErrorSpec { doc_from_display: Some(true), + ..Default::default() }, errors: vec![ErrorSpec { name: "DUMMY_ERR2".into(), @@ -1482,3 +1492,111 @@ modules: "#; assert!(YamlParser::parse_str(s).is_ok()); } + +#[test] +fn test_error_variant_type() { + log_init(); + + let s = r#" +--- +errors: + - name: MY_ERR + variant_type: MyErr +"#; + + let err = ErrorSpec { + name: "MY_ERR".into(), + oes: OverridableErrorSpec { + variant_type: Some(true), + ..Default::default() + }, + variant_type_name: Some("MyErr".into()), + ..Default::default() + }; + let spec = spec_from_err(err); + let res = YamlParser::parse_str(s).unwrap(); + assert_eq!(res, spec); + + for val in [true, false] { + let s = format!("---\nerrors:\n - name: MY_ERR\n variant_type: {val}"); + let err = ErrorSpec { + name: "MY_ERR".into(), + oes: OverridableErrorSpec { + variant_type: Some(val), + ..Default::default() + }, + ..Default::default() + }; + let spec = spec_from_err(err); + let res = YamlParser::parse_str(&s).unwrap(); + assert_eq!(res, spec); + } +} + +#[test] +fn test_error_variant_type_bad_name() { + log_init(); + + let test_cases = &[ + ("MY_ERR", BAD_IDENTIFIER_CHARACTERS), + ("my_err", BAD_IDENTIFIER_CHARACTERS), + ("myErr", BAD_IDENTIFIER_CASE), + ("myerr", BAD_IDENTIFIER_CASE), + ]; + + for tc in test_cases { + let s = format!("---\nerrors:\n - name: MY_ERR\n variant_type: {}", tc.0); + assert_eq!(YamlParser::parse_str(&s).unwrap_err().kind(), tc.1); + } +} + +#[test] +fn test_category_variant_type() { + log_init(); + + for val in [true, false] { + let s = format!("---\ncategory:\n variant_type: {val}\nerrors:\n - DUMMY_ERR"); + let cat = CategorySpec { + name: "General".into(), + oes: OverridableErrorSpec { + variant_type: Some(val), + ..Default::default() + }, + ..Default::default() + }; + let spec = spec_from_category(cat); + let res = YamlParser::parse_str(&s).unwrap(); + assert_eq!(res, spec); + } + + for bad in BAD_BOOLEANS { + let s = format!("---\ncategory:\n variant_type: {bad}\nerrors:\n - DUMMY_ERR"); + let res = YamlParser::parse_str(&s); + assert_eq!(res.unwrap_err().kind(), BAD_VALUE_TYPE); + } +} + +#[test] +fn test_module_variant_type() { + log_init(); + + for val in [true, false] { + let s = format!("---\nmodule:\n variant_type: {val}\nerrors:\n - DUMMY_ERR"); + let module = ModuleSpec { + oes: OverridableErrorSpec { + variant_type: Some(val), + ..Default::default() + }, + ..Default::default() + }; + let spec = spec_from_module(module); + let res = YamlParser::parse_str(&s).unwrap(); + assert_eq!(res, spec); + } + + for bad in BAD_BOOLEANS { + let s = format!("---\nmodule:\n variant_type: {bad}\nerrors:\n - DUMMY_ERR"); + let res = YamlParser::parse_str(&s); + assert_eq!(res.unwrap_err().kind(), BAD_VALUE_TYPE); + } +} diff --git a/crates/tighterror-build/src/spec/definitions.rs b/crates/tighterror-build/src/spec/definitions.rs index 6b3dda6..2d4b08b 100644 --- a/crates/tighterror-build/src/spec/definitions.rs +++ b/crates/tighterror-build/src/spec/definitions.rs @@ -18,3 +18,4 @@ pub const DEFAULT_UPDATE_MODE: bool = false; pub const DEFAULT_NO_STD: bool = false; pub const DEFAULT_FLAT_KINDS: bool = false; pub const DEFAULT_SEPARATE_FILES: bool = false; +pub const DEFAULT_VARIANT_TYPE: bool = false; diff --git a/crates/tighterror-build/src/spec/error.rs b/crates/tighterror-build/src/spec/error.rs index 43c5a13..989da2d 100644 --- a/crates/tighterror-build/src/spec/error.rs +++ b/crates/tighterror-build/src/spec/error.rs @@ -1,6 +1,10 @@ +use crate::common::casing; +use convert_case::Case; + #[derive(Clone, Debug, Default, Eq, PartialEq)] pub struct OverridableErrorSpec { pub doc_from_display: Option, + pub variant_type: Option, } #[derive(Clone, Debug, Default, Eq, PartialEq)] @@ -8,5 +12,16 @@ pub struct ErrorSpec { pub name: String, pub display: Option, pub doc: Option, + pub variant_type_name: Option, pub oes: OverridableErrorSpec, } + +impl ErrorSpec { + pub fn variant_type_name(&self) -> String { + if let Some(ref vtn) = self.variant_type_name { + vtn.clone() + } else { + casing::convert_case(&self.name, Case::UpperSnake, Case::UpperCamel) + } + } +} diff --git a/crates/tighterror-build/src/spec/module.rs b/crates/tighterror-build/src/spec/module.rs index 365a130..adf938e 100644 --- a/crates/tighterror-build/src/spec/module.rs +++ b/crates/tighterror-build/src/spec/module.rs @@ -144,6 +144,36 @@ impl ModuleSpec { pub fn flat_kinds(&self) -> bool { self.flat_kinds.unwrap_or(DEFAULT_FLAT_KINDS) } + + pub fn has_variant_types(&self) -> bool { + self.categories + .iter() + .any(|c| self.cat_has_variant_types(c)) + } + + pub fn has_display_variant_types(&self) -> bool { + self.categories + .iter() + .any(|c| self.cat_has_display_variant_types(c)) + } + + pub fn cat_has_variant_types(&self, c: &CategorySpec) -> bool { + c.errors.iter().any(|e| self.err_has_variant_type(c, e)) + } + + pub fn cat_has_display_variant_types(&self, c: &CategorySpec) -> bool { + c.errors + .iter() + .any(|e| e.display.is_some() && self.err_has_variant_type(c, e)) + } + + pub fn err_has_variant_type(&self, c: &CategorySpec, e: &ErrorSpec) -> bool { + e.oes + .variant_type + .or(c.oes.variant_type) + .or(self.oes.variant_type) + .unwrap_or(DEFAULT_VARIANT_TYPE) + } } pub struct ModuleSpecErrorIter<'a> { diff --git a/crates/tighterror-build/tests/multiple_modules/src/lib.rs b/crates/tighterror-build/tests/multiple_modules/src/lib.rs index 32cf2fd..a3326a1 100644 --- a/crates/tighterror-build/tests/multiple_modules/src/lib.rs +++ b/crates/tighterror-build/tests/multiple_modules/src/lib.rs @@ -14,9 +14,22 @@ pub mod internal_errors { include!(concat!(env!("OUT_DIR"), "/internal_errors.rs")); } +/// FlatKinds module. +pub mod flat_kinds_mod { + include!(concat!(env!("OUT_DIR"), "/flat_kinds_mod.rs")); +} + +/// FlatKinds module without display attributes. +pub mod flat_kinds_mod_without_display { + include!(concat!( + env!("OUT_DIR"), + "/flat_kinds_mod_without_display.rs" + )); +} + #[cfg(test)] mod tests { - use crate::{errors, internal_errors}; + use crate::{errors, flat_kinds_mod, internal_errors}; #[test] fn test_kind_constants_are_placed_in_different_modules() { @@ -29,4 +42,20 @@ mod tests { internal_errors::kind::processor::BAD_FILE, ); } + + #[test] + fn test_variant_types() { + assert_eq!( + format!("{}", flat_kinds_mod::variant::types::CatOneErrOne), + "CatOne error #1" + ); + assert_eq!( + format!("{}", flat_kinds_mod::variant::types::CatOneWithoutDisplay), + "CatOneWithoutDisplay" + ); + assert_eq!( + format!("{}", flat_kinds_mod::variant::types::CatTwoSpecial), + "CatTwoSpecial" + ); + } } diff --git a/crates/tighterror-build/tests/multiple_modules/tighterror.yaml b/crates/tighterror-build/tests/multiple_modules/tighterror.yaml index 091ef12..af66b70 100644 --- a/crates/tighterror-build/tests/multiple_modules/tighterror.yaml +++ b/crates/tighterror-build/tests/multiple_modules/tighterror.yaml @@ -37,6 +37,10 @@ modules: - QUEUE_FULL: Processing queue is full. - name: WITHOUT_DISPLAY doc: An error without display string. + variant_type: true + - name: WITHOUT_DOC + display: An error without doc string. + variant_type: true - name: General doc: General errors category. @@ -59,3 +63,48 @@ modules: let e: Error = BAD_ARG.into(); ``` - TIMEOUT: Operation timed out. + + - name: flat_kinds_mod + flat_kinds: true + doc_from_display: true + variant_type: true + categories: + - name: CatOne + doc: "Category #1" + errors: + - CAT_ONE_ERR_ONE: "CatOne error #1" + - CAT_ONE_ERR_TWO: "CatOne error #2" + - name: CAT_ONE_WITHOUT_DISPLAY + doc: A CatOne error without display string. + - name: CAT_ONE_WITHOUT_DOC + display: An error without doc string. + - name: CatTwo + doc: "Category #2" + errors: + - CAT_TWO_ERR_ONE: "CatTwo error #1" + - CAT_TWO_ERR_TWO: "CatTwo error #2" + - name: CAT_TWO_WITHOUT_DISPLAY + doc: A CatTwo error without display string. + variant_type: CatTwoSpecial + + - name: flat_kinds_mod_without_display + flat_kinds: true + variant_type: true + categories: + - name: CatOne + doc: "Category #1" + errors: + - name: CAT_ONE_ERR_ONE + doc: "CatOne error #1" + - name: CAT_ONE_ERR_TWO + doc: "CatOne error #2" + - name: CatTwo + doc: "Category #2" + errors: + - name: CAT_TWO_ERR_ONE + doc: "CatTwo error #1" + - name: CAT_TWO_ERR_TWO + doc: "CatTwo error #2" + - name: CAT_TWO_CUSTOM_NAME + doc: A CatTwo error without display string. + variant_type: CatTwoCustomVariantTypeName diff --git a/src/lib.rs b/src/lib.rs index 6b1817a..9fd3d9e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -20,6 +20,7 @@ //! # Table Of Contents //! //! 1. [High-Level Overview](#high-level-overview) +//! * [Variant Types](#variant-types) //! 1. [Implementation Details](#implementation-details) //! 1. [Specification File Reference](#specification-file-reference) //! * [Filename](#filename) @@ -143,6 +144,59 @@ //! # } //! ``` //! +//! ### Variant Types +//! +//! There are many cases when a function has only a single error condition. +//! Returning an `Error` from such function may be excessive because it causes +//! a caller to handle a non-exhaustive list of errors (because an Error can +//! contain any of the error kinds defined in a module). +//! +//! Enabling a *variant type* on such error causes creation of a dedicated +//! error type (a ZST) to be used as the error type of the function. +//! This way a caller of the function needs to handle a single error condition, +//! making error handling simpler. +//! +//! Like *error kind*, *variant type* is convertible to `Error`, `ErrorKind`, +//! and `Result`. Hence, it is seamless to propagate through layers +//! of abstraction using the `?` operator. +//! +//! When at least one *variant type* is enabled *tighterror* creates a submodule +//! `variant::types` that contains per-category modules (subject to +//! `flat_kinds` module setting) with their corresponding variant types. +//! +//! ```yaml +//! --- +//! errors: +//! - name: OPERATION_TIMEOUT +//! variant_type: true +//! ``` +//! +//! ```rust +//! pub mod kind { +//! pub mod general { +//! // Error kind constants defined here. +//! // ... +//! // pub const OPERATION_TIMEOUT: ErrorKind = ErrorKind::new(GENERAL, 0); +//! } +//! } +//! +//! pub mod variant { +//! pub mod types { +//! // Variant types defined here. +//! pub mod general { +//! #[non_exhaustive] +//! pub struct OperationTimeout; +//! +//! // impl tighterror::VariantType for OperationTimeout { /* ... */ } +//! // impl Debug for OperationTimeout { /* ... */ } +//! // impl Display for OperationTimeout { /* ... */ } +//! // impl OperationTimeout { /* ... */ } +//! // impl std::error::Error for OperationTimeout {} +//! } +//! } +//! } +//! ``` +//! //! [newtype]: https://doc.rust-lang.org/rust-by-example/generics/new_types.html //! //! @@ -240,7 +294,28 @@ //! errors in the category, or in [module object](#module-doc-from-display) //! to affect all errors in the module. //! Values defined on lower levels win.
-//! Default: `false`
+//! Default: `false`

+//! +//! * `variant_type` - bool|string (optional) +//! +//! Enables creation of a Variant Type for this error. +//! +//! When specified as a boolean `true|false` it enables/disables creation +//! of a Variant Type for this error. When enabled the name of the Variant +//! Type is derived by converting the error name case from UPPER_SNAKE to +//! UpperCamel, e.g., `OPERATION_TIMEOUT` becomes `OperationTimeout`. +//! +//! To customize the Variant Type name it is possible to specify this +//! attribute as a string. In this case the value of the attribute becomes +//! the name of the Variant Type. It must be a valid Rust struct name +//! specified in UpperCamel case. +//! +//! This attribute can be enabled on a higher level in +//! [category object](#category-variant-type) or in +//! [module object](#module-variant-type). +//! Values defined on lower levels win. +//! +//! Default: `false`

//! //! ### Error Object Examples //! @@ -434,6 +509,18 @@ //! (see below) this attribute is forbidden, and the error list must be //! defined as a root-level attribute.

//! +//! * `variant_type` - bool (optional) +//! +//! Sets a default value for the [`variant_type`](#err-obj-variant-type) +//! *error object* attribute. This affects errors belonging to this category +//! only. The value is used for all errors that do not define this property +//! explicitly as part of *error object* specification. +//! +//! Customization of Variant Type name is possible only in the *error object* +//! specification. +//! +//! Default: `false`

+//! //! ### Category Object Examples //! //! A specification with a single implicit category `General`. Note, the error @@ -753,6 +840,20 @@ //! to create a `Result` from `ErrorKind`.
//! Default: `true`

//! +//! * `variant_type` - bool (optional) +//! +//! Sets a default value for the [`variant_type`](#err-obj-variant-type) +//! *error object* attribute. This affects errors belonging to all categories +//! in this module. The value is used for all categories and errors that do +//! not define this property explicitly as part of +//! [category object](#category-variant-type) or +//! [error object](#err-obj-variant-type) specification. +//! +//! Customization of Variant Type name is possible only in the *error object* +//! specification. +//! +//! Default: `false`

+//! //! ### Module Object Examples //! //! YAML @@ -1322,3 +1423,6 @@ pub use error::*; mod location; pub use location::*; + +mod variant_type; +pub use variant_type::*; diff --git a/src/variant_type.rs b/src/variant_type.rs new file mode 100644 index 0000000..c24d29d --- /dev/null +++ b/src/variant_type.rs @@ -0,0 +1,49 @@ +use crate::{Category, Kind}; +use core::fmt::{Debug, Display}; + +/// The trait of variant types. +pub trait VariantType: Clone + Debug + Display { + /// The underlying Rust type of error kind. + /// + /// A concrete builtin type, e.g., `u8`. + type R; + + /// The error category concrete type. + type Category: Category; + + /// The error kind concrete type. + type Kind: Kind; + + /// The error category of the variant type. + const CATEGORY: Self::Category; + + /// The error kind of the variant type. + const KIND: Self::Kind; + + /// The concrete type name. + const NAME: &'static str; + + /// Returns the error category of the variant type. + /// + /// This is a convenience method that returns [CATEGORY](Self::CATEGORY). + #[inline] + fn category(&self) -> Self::Category { + Self::CATEGORY + } + + /// Returns the error kind of the variant type. + /// + /// This is a convenience method that returns [KIND](Self::KIND). + #[inline] + fn kind(&self) -> Self::Kind { + Self::KIND + } + + /// Returns the variant type name. + /// + /// This is a convenience method that returns [NAME](Self::NAME). + #[inline] + fn name(&self) -> &'static str { + Self::NAME + } +}