From fe3ddf165e257acbc0431beff530323fc9cd65b1 Mon Sep 17 00:00:00 2001 From: Kristofers Solo Date: Mon, 7 Jul 2025 22:16:24 +0300 Subject: [PATCH] refactor(units): use derive macro --- derive_macro/Cargo.lock | 250 ++++++++++++++++++++++++++++++++++++++ derive_macro/Cargo.toml | 3 + derive_macro/src/lib.rs | 6 + derive_macro/src/unit.rs | 134 ++++++++++++++++++++ src/app/utils/filesize.rs | 39 +----- src/app/utils/netspeed.rs | 39 +----- src/app/utils/unit.rs | 37 +----- 7 files changed, 405 insertions(+), 103 deletions(-) create mode 100644 derive_macro/src/unit.rs diff --git a/derive_macro/Cargo.lock b/derive_macro/Cargo.lock index 3a90cca..34135f0 100644 --- a/derive_macro/Cargo.lock +++ b/derive_macro/Cargo.lock @@ -9,8 +9,49 @@ dependencies = [ "proc-macro2", "quote", "syn", + "trybuild", ] +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "glob" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2" + +[[package]] +name = "hashbrown" +version = "0.15.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5971ac85611da7067dbfcabef3c70ebb5606018acd9e2a3903a0da507521e0d5" + +[[package]] +name = "indexmap" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe4cd85333e22411419a0bcae1297d25e58c9443848b11dc6a86fefe8c78a661" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "itoa" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" + +[[package]] +name = "memchr" +version = "2.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" + [[package]] name = "proc-macro2" version = "1.0.95" @@ -29,6 +70,53 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "ryu" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" + +[[package]] +name = "serde" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.140" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" +dependencies = [ + "itoa", + "memchr", + "ryu", + "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 = "syn" version = "2.0.104" @@ -40,8 +128,170 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "target-triple" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ac9aa371f599d22256307c24a9d748c041e548cbf599f35d890f9d365361790" + +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "toml" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + +[[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_edit" +version = "0.22.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime", + "toml_write", + "winnow", +] + +[[package]] +name = "toml_write" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" + +[[package]] +name = "trybuild" +version = "1.0.105" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c9bf9513a2f4aeef5fdac8677d7d349c79fdbcc03b9c86da6e9d254f1e43be2" +dependencies = [ + "glob", + "serde", + "serde_derive", + "serde_json", + "target-triple", + "termcolor", + "toml", +] + [[package]] name = "unicode-ident" version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" + +[[package]] +name = "winapi-util" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "winnow" +version = "0.7.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74c7b26e3480b707944fc872477815d29a8e429d2f93a1ce000f5fa84a15cbcd" +dependencies = [ + "memchr", +] diff --git a/derive_macro/Cargo.toml b/derive_macro/Cargo.toml index 4cbb5a8..27dc71a 100644 --- a/derive_macro/Cargo.toml +++ b/derive_macro/Cargo.toml @@ -10,3 +10,6 @@ crate-type = ["proc-macro"] proc-macro2 = "1.0" quote = "1.0" syn = { version = "2.0", features = ["full", "extra-traits"] } + +[dev-dependencies] +trybuild = "1.0" diff --git a/derive_macro/src/lib.rs b/derive_macro/src/lib.rs index bfbd8e2..5a8bc98 100644 --- a/derive_macro/src/lib.rs +++ b/derive_macro/src/lib.rs @@ -1,4 +1,5 @@ mod merge; +mod unit; use proc_macro::TokenStream; use syn::{DeriveInput, parse_macro_input}; @@ -9,3 +10,8 @@ pub fn merge_derive(input: TokenStream) -> TokenStream { merge::impl_merge_derive(input) } +#[proc_macro_derive(UnitConversions, attributes(units, error))] +pub fn unit_derive(input: TokenStream) -> TokenStream { + let input = parse_macro_input!(input as DeriveInput); + unit::impl_unit_conversions(input) +} diff --git a/derive_macro/src/unit.rs b/derive_macro/src/unit.rs new file mode 100644 index 0000000..ba8a392 --- /dev/null +++ b/derive_macro/src/unit.rs @@ -0,0 +1,134 @@ +use proc_macro::TokenStream; +use quote::quote; +use syn::{Attribute, DeriveInput, Error, Type, parse_quote}; + +pub fn impl_unit_conversions(input: DeriveInput) -> TokenStream { + let name = &input.ident; + + let mut unsigned_types = Vec::new(); + let mut signed_types = Vec::new(); + let mut error_type: Option = None; + + for attr in &input.attrs { + if attr.path().is_ident("units") { + if let Ok(types) = parse_unit_types(attr) { + for ty in types { + let type_str = quote!(#ty).to_string(); + if is_signed_type(&type_str) { + signed_types.push(ty); + } else { + unsigned_types.push(ty); + } + } + } else if attr.path().is_ident("error") { + if let Ok(err_type) = parse_error_types(attr) { + error_type = Some(err_type); + } + } + } + } + + if unsigned_types.is_empty() && signed_types.is_empty() { + unsigned_types = vec![ + parse_quote!(u8), + parse_quote!(u16), + parse_quote!(u32), + parse_quote!(u64), + parse_quote!(usize), + ]; + signed_types = vec![ + parse_quote!(i8), + parse_quote!(i16), + parse_quote!(i32), + parse_quote!(i64), + parse_quote!(isize), + ]; + } + + let error_type = error_type.unwrap_or_else(|| parse_quote!(String)); + let is_string_error = quote!(#error_type).to_string() == "String"; + + let from_impls = unsigned_types.iter().map(|ty| { + let conversion_expr = if name == "Unit" { + quote! { Self(value as u64) } + } else { + quote! { Self(crate::app::utils::unit::Unit::new(value as u64)) } + }; + quote! { + impl From<#ty> for #name { + fn from(value: #ty) -> Self { + #conversion_expr + } + } + } + }); + + let try_from_impls = signed_types.iter().map(|ty| { + let error_creation = if is_string_error { + quote! { + format!("Cannot convert negative value {} to {}", value, stringify!(#name)) + } + } else { + // For custom error types, try to construct from a string message + // This assumes the error type implements From or similar + quote! { + #error_type::from(format!("Cannot convert negative value {} to {}", value, stringify!(#name))) + } + }; + + let conversion_expr = if name == "Unit" { + quote! { Ok(Self(value as u64)) } + } else { + quote! { Ok(Self(crate::app::utils::unit::Unit::try_from(value)?)) } + }; + + quote! { + impl TryFrom<#ty> for #name { + type Error = #error_type; + + fn try_from(value: #ty) -> Result { + if value < 0 { + return Err(#error_creation); + } + #conversion_expr + } + } + } + }); + + let expanded = quote! { + #(#from_impls)* + #(#try_from_impls)* + }; + + TokenStream::from(expanded) +} + +fn parse_unit_types(attr: &Attribute) -> Result, Error> { + let mut types = Vec::new(); + + attr.parse_nested_meta(|meta| { + if let Ok(ty) = meta.value()?.parse::() { + types.push(ty); + } + Ok(()) + })?; + Ok(types) +} + +fn parse_error_types(attr: &Attribute) -> Result { + let mut error_type = None; + + attr.parse_nested_meta(|meta| { + if let Ok(ty) = meta.value()?.parse::() { + error_type = Some(ty); + } + Ok(()) + })?; + + error_type.ok_or_else(|| Error::new_spanned(attr, "Expected error type")) +} + +fn is_signed_type(type_str: &str) -> bool { + matches!(type_str, "i8" | "i16" | "i32" | "i64" | "isize") +} diff --git a/src/app/utils/filesize.rs b/src/app/utils/filesize.rs index 96b56c8..b83a451 100644 --- a/src/app/utils/filesize.rs +++ b/src/app/utils/filesize.rs @@ -1,8 +1,9 @@ -use super::unit::{Unit, UnitError}; -use crate::app::utils::unit::UnitDisplay; +use super::unit::{Unit, UnitDisplay}; +use derive_macro::UnitConversions; use std::fmt::Display; -#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Default)] +#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Default, UnitConversions)] +#[error(UnitError)] pub struct FileSize(Unit); impl FileSize { @@ -17,35 +18,3 @@ impl Display for FileSize { write!(f, "{}", UnitDisplay::new(&self.0, UNITS)) } } - -macro_rules! impl_from_unsigned { - ($type:ty, $($t:ty), *) => { - $( - impl From<$t> for $type { - fn from(value: $t) -> Self { - Self(Unit::from(value)) - } - } - )* - }; -} - -macro_rules! impl_try_from_signed { - ($type:ty, $error:ty, $($t:ty), *) => { - $( - impl TryFrom<$t> for $type { - type Error = $error; - - fn try_from(value: $t) -> Result { - if value < 0 { - return Err(UnitError::NegativeValue { value: value as i64 }); - } - Ok(Self(Unit::try_from(value)?)) - } - } - )* - }; -} - -impl_from_unsigned!(FileSize, u8, u16, u32, u64, usize); -impl_try_from_signed!(FileSize, UnitError, i8, i16, i32, i64, isize); diff --git a/src/app/utils/netspeed.rs b/src/app/utils/netspeed.rs index 4c6a709..fe99b2e 100644 --- a/src/app/utils/netspeed.rs +++ b/src/app/utils/netspeed.rs @@ -1,8 +1,9 @@ -use super::unit::{Unit, UnitError}; -use crate::app::utils::unit::UnitDisplay; +use super::unit::{Unit, UnitDisplay}; +use derive_macro::UnitConversions; use std::fmt::Display; -#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Default)] +#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Default, UnitConversions)] +#[error(UnitError)] pub struct NetSpeed(Unit); impl NetSpeed { @@ -17,35 +18,3 @@ impl Display for NetSpeed { write!(f, "{}", UnitDisplay::new(&self.0, UNITS)) } } - -macro_rules! impl_from_unsigned { - ($type:ty, $($t:ty), *) => { - $( - impl From<$t> for $type { - fn from(value: $t) -> Self { - Self(Unit::from(value)) - } - } - )* - }; -} - -macro_rules! impl_try_from_signed { - ($type:ty, $error:ty, $($t:ty), *) => { - $( - impl TryFrom<$t> for $type { - type Error = $error; - - fn try_from(value: $t) -> Result { - if value < 0 { - return Err(UnitError::NegativeValue { value: value as i64 }); - } - Ok(Self(Unit::try_from(value)?)) - } - } - )* - }; -} - -impl_from_unsigned!(NetSpeed, u8, u16, u32, u64, usize); -impl_try_from_signed!(NetSpeed, UnitError, i8, i16, i32, i64, isize); diff --git a/src/app/utils/unit.rs b/src/app/utils/unit.rs index 3b46163..1fa9e82 100644 --- a/src/app/utils/unit.rs +++ b/src/app/utils/unit.rs @@ -1,3 +1,4 @@ +use derive_macro::UnitConversions; use std::fmt::Display; use thiserror::Error; @@ -11,7 +12,8 @@ pub enum UnitError { InvalidValue { reason: String }, } -#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Default)] +#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Default, UnitConversions)] +#[error(UnitError)] pub struct Unit(u64); impl Unit { @@ -24,6 +26,7 @@ impl Unit { } } +#[derive(Debug)] pub struct UnitDisplay<'a> { unit: &'a Unit, units: &'a [&'a str], @@ -55,35 +58,3 @@ impl<'a> Display for UnitDisplay<'a> { write!(f, "{:.2} {}", size, self.units[unit_index]) } } - -macro_rules! impl_from_unsigned { - ($type:ty, $($t:ty), *) => { - $( - impl From<$t> for $type { - fn from(value: $t) -> Self { - Self(value as u64) - } - } - )* - }; -} - -macro_rules! impl_try_from_signed { - ($type:ty, $error:ty, $($t:ty), *) => { - $( - impl TryFrom<$t> for $type { - type Error = $error; - - fn try_from(value: $t) -> Result { - if value < 0 { - return Err(UnitError::NegativeValue { value: value as i64 }); - } - Ok(Self(value as u64)) - } - } - )* - }; -} - -impl_from_unsigned!(Unit, u8, u16, u32, u64, usize); -impl_try_from_signed!(Unit, UnitError, i8, i16, i32, i64, isize);