refactor(units): use derive macro

This commit is contained in:
Kristofers Solo 2025-07-07 22:16:24 +03:00
parent 7d58d1b74c
commit fe3ddf165e
Signed by: kristoferssolo
GPG Key ID: 8687F2D3EEE6F0ED
7 changed files with 405 additions and 103 deletions

250
derive_macro/Cargo.lock generated
View File

@ -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",
]

View File

@ -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"

View File

@ -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)
}

134
derive_macro/src/unit.rs Normal file
View File

@ -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<Type> = 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<String> 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<Self, Self::Error> {
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<Vec<Type>, Error> {
let mut types = Vec::new();
attr.parse_nested_meta(|meta| {
if let Ok(ty) = meta.value()?.parse::<Type>() {
types.push(ty);
}
Ok(())
})?;
Ok(types)
}
fn parse_error_types(attr: &Attribute) -> Result<Type, Error> {
let mut error_type = None;
attr.parse_nested_meta(|meta| {
if let Ok(ty) = meta.value()?.parse::<Type>() {
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")
}

View File

@ -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<Self, Self::Error> {
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);

View File

@ -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<Self, Self::Error> {
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);

View File

@ -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<Self, Self::Error> {
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);