Skip to main content

humanbyte_derive/
lib.rs

1use proc_macro::TokenStream;
2use proc_macro2::Span;
3use quote::quote;
4use syn::{DeriveInput, parse_macro_input};
5
6#[proc_macro_derive(HumanByte)]
7pub fn humanbyte(input: TokenStream) -> TokenStream {
8    let input = parse_macro_input!(input as DeriveInput);
9    let name = &input.ident;
10
11    let mut combined = constructor_tokens(name);
12    combined.extend(display_tokens(name));
13    combined.extend(parse_tokens(name));
14    combined.extend(ops_tokens(name));
15    combined.extend(fromstr_tokens(name));
16    if cfg!(feature = "serde") {
17        combined.extend(serde_tokens(name));
18    }
19    if cfg!(feature = "schemars") {
20        combined.extend(schemars_tokens(name));
21    }
22    TokenStream::from(combined)
23}
24
25#[proc_macro_derive(HumanByteConstructor)]
26pub fn humanbyte_constructor(input: TokenStream) -> TokenStream {
27    let input = parse_macro_input!(input as DeriveInput);
28    TokenStream::from(constructor_tokens(&input.ident))
29}
30
31fn constructor_tokens(name: &syn::Ident) -> proc_macro2::TokenStream {
32    // Define units with their multipliers and descriptions
33    let units = vec![
34        ("b", "1", "bytes"),
35        ("kb", "::humanbyte::KB", "kilobytes"),
36        ("kib", "::humanbyte::KIB", "kibibytes"),
37        ("mb", "::humanbyte::MB", "megabytes"),
38        ("mib", "::humanbyte::MIB", "mebibytes"),
39        ("gb", "::humanbyte::GB", "gigabytes"),
40        ("gib", "::humanbyte::GIB", "gibibytes"),
41        ("tb", "::humanbyte::TB", "terabytes"),
42        ("tib", "::humanbyte::TIB", "tebibytes"),
43        ("pb", "::humanbyte::PB", "petabytes"),
44        ("pib", "::humanbyte::PIB", "pebibytes"),
45        ("eb", "::humanbyte::EB", "exabytes"),
46        ("eib", "::humanbyte::EIB", "exbibytes"),
47    ];
48
49    // Generate methods
50    let methods = units.iter().map(|(fn_name, multiplier, description)| {
51        // Create an identifier for the method name
52        let method_name = syn::Ident::new(fn_name, Span::call_site());
53
54        // Parse the multiplier into an expression
55        let multiplier_expr: syn::Expr = syn::parse_str(multiplier).unwrap();
56
57        // Generate the documentation comment
58        let doc_comment = format!(
59            "Construct `{}` given an amount of {}.\n\nPanics if the total byte count overflows `u64`.",
60            name, description
61        );
62
63        // Generate the method using quote!
64        quote! {
65            #[doc = #doc_comment]
66            #[inline(always)]
67            pub const fn #method_name(size: u64) -> Self {
68                match size.checked_mul(#multiplier_expr) {
69                    Some(bytes) => Self(bytes),
70                    None => panic!("byte size overflows u64"),
71                }
72            }
73        }
74    });
75
76    // Generate float accessors (skipping bytes, which has as_u64 in the parse derive)
77    let accessors = units[1..].iter().map(|(unit, multiplier, description)| {
78        let method_name = syn::Ident::new(&format!("as_{}", unit), Span::call_site());
79        let multiplier_expr: syn::Expr = syn::parse_str(multiplier).unwrap();
80        let doc_comment = format!("Returns the size in {} as a float.", description);
81
82        quote! {
83            #[doc = #doc_comment]
84            #[inline(always)]
85            pub fn #method_name(&self) -> f64 {
86                self.0 as f64 / #multiplier_expr as f64
87            }
88        }
89    });
90
91    quote! {
92        impl #name {
93            #(#methods)*
94            #(#accessors)*
95        }
96
97        impl From<u64> for #name {
98            fn from(size: u64) -> #name {
99                Self(size)
100            }
101        }
102    }
103}
104
105#[proc_macro_derive(HumanByteOps)]
106pub fn humanbyte_ops(input: TokenStream) -> TokenStream {
107    let input = parse_macro_input!(input as DeriveInput);
108    TokenStream::from(ops_tokens(&input.ident))
109}
110
111fn ops_tokens(name: &syn::Ident) -> proc_macro2::TokenStream {
112    quote! {
113        impl core::ops::Add<#name> for #name {
114            type Output = #name;
115
116            #[inline(always)]
117            fn add(self, rhs: #name) -> #name {
118                #name(self.0 + rhs.0)
119            }
120        }
121
122        impl core::ops::AddAssign<#name> for #name {
123            #[inline(always)]
124            fn add_assign(&mut self, rhs: #name) {
125                self.0 += rhs.0
126            }
127        }
128
129        impl<T> core::ops::Add<T> for #name
130        where
131            T: Into<u64>,
132        {
133            type Output = #name;
134            #[inline(always)]
135            fn add(self, rhs: T) -> #name {
136                #name(self.0 + (rhs.into()))
137            }
138        }
139
140        impl<T> core::ops::AddAssign<T> for #name
141        where
142            T: Into<u64>,
143        {
144            #[inline(always)]
145            fn add_assign(&mut self, rhs: T) {
146                self.0 += rhs.into();
147            }
148        }
149
150        impl core::ops::Sub<#name> for #name {
151            type Output = #name;
152
153            #[inline(always)]
154            fn sub(self, rhs: #name) -> #name {
155                #name(self.0 - rhs.0)
156            }
157        }
158
159        impl core::ops::SubAssign<#name> for #name {
160            #[inline(always)]
161            fn sub_assign(&mut self, rhs: #name) {
162                self.0 -= rhs.0
163            }
164        }
165
166        impl<T> core::ops::Sub<T> for #name
167        where
168            T: Into<u64>,
169        {
170            type Output = #name;
171
172            #[inline(always)]
173            fn sub(self, rhs: T) -> #name {
174                #name(self.0 - (rhs.into()))
175            }
176        }
177
178        impl<T> core::ops::SubAssign<T> for #name
179        where
180            T: Into<u64>,
181        {
182            #[inline(always)]
183            fn sub_assign(&mut self, rhs: T) {
184                self.0 -= rhs.into();
185            }
186        }
187
188        impl<T> core::ops::Mul<T> for #name
189        where
190            T: Into<u64>,
191        {
192            type Output = #name;
193            #[inline(always)]
194            fn mul(self, rhs: T) -> #name {
195                #name(self.0 * rhs.into())
196            }
197        }
198
199        impl<T> core::ops::MulAssign<T> for #name
200        where
201            T: Into<u64>,
202        {
203            #[inline(always)]
204            fn mul_assign(&mut self, rhs: T) {
205                self.0 *= rhs.into();
206            }
207        }
208
209        impl core::ops::Div<#name> for #name {
210            /// Dividing two byte sizes yields a dimensionless count,
211            /// e.g. `file_size / chunk_size` chunks.
212            type Output = u64;
213
214            #[inline(always)]
215            fn div(self, rhs: #name) -> u64 {
216                self.0 / rhs.0
217            }
218        }
219
220        impl<T> core::ops::Div<T> for #name
221        where
222            T: Into<u64>,
223        {
224            type Output = #name;
225            #[inline(always)]
226            fn div(self, rhs: T) -> #name {
227                #name(self.0 / rhs.into())
228            }
229        }
230
231        impl<T> core::ops::DivAssign<T> for #name
232        where
233            T: Into<u64>,
234        {
235            #[inline(always)]
236            fn div_assign(&mut self, rhs: T) {
237                self.0 /= rhs.into();
238            }
239        }
240
241        impl core::ops::Rem<#name> for #name {
242            type Output = #name;
243
244            #[inline(always)]
245            fn rem(self, rhs: #name) -> #name {
246                #name(self.0 % rhs.0)
247            }
248        }
249
250        impl core::iter::Sum<#name> for #name {
251            fn sum<I: Iterator<Item = #name>>(iter: I) -> #name {
252                iter.fold(#name(0), |acc, x| #name(acc.0 + x.0))
253            }
254        }
255
256        impl<'a> core::iter::Sum<&'a #name> for #name {
257            fn sum<I: Iterator<Item = &'a #name>>(iter: I) -> #name {
258                iter.fold(#name(0), |acc, x| #name(acc.0 + x.0))
259            }
260        }
261
262        impl core::ops::Add<#name> for u64 {
263            type Output = #name;
264            #[inline(always)]
265            fn add(self, rhs: #name) -> #name {
266                #name(rhs.0 + self)
267            }
268        }
269
270        impl core::ops::Add<#name> for u32 {
271            type Output = #name;
272            #[inline(always)]
273            fn add(self, rhs: #name) -> #name {
274                #name(rhs.0 + (self as u64))
275            }
276        }
277
278        impl core::ops::Add<#name> for u16 {
279            type Output = #name;
280            #[inline(always)]
281            fn add(self, rhs: #name) -> #name {
282                #name(rhs.0 + (self as u64))
283            }
284        }
285
286        impl core::ops::Add<#name> for u8 {
287            type Output = #name;
288            #[inline(always)]
289            fn add(self, rhs: #name) -> #name {
290                #name(rhs.0 + (self as u64))
291            }
292        }
293
294        impl core::ops::Mul<#name> for u64 {
295            type Output = #name;
296            #[inline(always)]
297            fn mul(self, rhs: #name) -> #name {
298                #name(rhs.0 * self)
299            }
300        }
301
302        impl core::ops::Mul<#name> for u32 {
303            type Output = #name;
304            #[inline(always)]
305            fn mul(self, rhs: #name) -> #name {
306                #name(rhs.0 * (self as u64))
307            }
308        }
309
310        impl core::ops::Mul<#name> for u16 {
311            type Output = #name;
312            #[inline(always)]
313            fn mul(self, rhs: #name) -> #name {
314                #name(rhs.0 * (self as u64))
315            }
316        }
317
318        impl core::ops::Mul<#name> for u8 {
319            type Output = #name;
320            #[inline(always)]
321            fn mul(self, rhs: #name) -> #name {
322                #name(rhs.0 * (self as u64))
323            }
324        }
325
326        #[cfg(target_pointer_width = "64")]
327        impl core::ops::Add<#name> for usize {
328            type Output = #name;
329            #[inline(always)]
330            fn add(self, rhs: #name) -> #name {
331                #name(rhs.0 + (self as u64))
332            }
333        }
334
335
336        #[cfg(target_pointer_width = "64")]
337        impl core::ops::Sub<#name> for usize {
338            type Output = #name;
339            #[inline(always)]
340            fn sub(self, rhs: #name) -> #name {
341                #name(self as u64 - rhs.0)
342            }
343        }
344
345        #[cfg(target_pointer_width = "64")]
346        impl core::ops::Mul<#name> for usize {
347            type Output = #name;
348            #[inline(always)]
349            fn mul(self, rhs: #name) -> #name {
350                #name(rhs.0 * (self as u64))
351            }
352        }
353
354        impl #name {
355            /// Provides `HumanByteRange` with explicit lower and upper bounds.
356            pub fn range<I: Into<Self>>(start: I, stop: I) -> ::humanbyte::HumanByteRange<Self> {
357                ::humanbyte::HumanByteRange::new(Some(start), Some(stop))
358            }
359
360            /// Provides `HumanByteRange` with explicit lower bound. Upper bound is set to `u64::MAX`.
361            pub fn range_start<I: Into<Self>>(start: I) -> ::humanbyte::HumanByteRange<Self> {
362                ::humanbyte::HumanByteRange::new(Some(start), None)
363            }
364
365            /// Provides `HumanByteRange` with explicit upper bound. Lower bound is set to `0`.
366            pub fn range_stop<I: Into<Self>>(stop: I) -> ::humanbyte::HumanByteRange<Self> {
367                ::humanbyte::HumanByteRange::new(None, Some(stop.into()))
368            }
369        }
370    }
371}
372
373#[proc_macro_derive(HumanByteDisplay)]
374pub fn humanbyte_display(input: TokenStream) -> TokenStream {
375    let input = parse_macro_input!(input as DeriveInput);
376    TokenStream::from(display_tokens(&input.ident))
377}
378
379fn display_tokens(name: &syn::Ident) -> proc_macro2::TokenStream {
380    quote! {
381        impl core::fmt::Display for #name {
382            fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
383                use core::fmt::Write as _;
384
385                // Interpret formatter precision as decimals: "{:.2}" is "1.50 KiB".
386                // Width/alignment are handled manually because Formatter::pad
387                // would reinterpret the precision as string truncation.
388                let precision = f.precision().unwrap_or(1);
389                let s = ::humanbyte::to_string_with_precision(
390                    self.0,
391                    ::humanbyte::Format::IEC,
392                    precision,
393                );
394                let pad = f.width().unwrap_or(0).saturating_sub(s.len());
395                let (left, right) = match f.align() {
396                    Some(core::fmt::Alignment::Right) => (pad, 0),
397                    Some(core::fmt::Alignment::Center) => (pad / 2, pad - pad / 2),
398                    // strings left-align by default
399                    _ => (0, pad),
400                };
401                for _ in 0..left {
402                    f.write_char(f.fill())?;
403                }
404                f.write_str(&s)?;
405                for _ in 0..right {
406                    f.write_char(f.fill())?;
407                }
408                Ok(())
409            }
410        }
411
412        impl core::fmt::Debug for #name {
413            fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
414                write!(f, "{}", self)
415            }
416        }
417    }
418}
419
420#[proc_macro_derive(HumanByteFromStr)]
421pub fn humanbyte_fromstr(input: TokenStream) -> TokenStream {
422    let input = parse_macro_input!(input as DeriveInput);
423    TokenStream::from(fromstr_tokens(&input.ident))
424}
425
426fn fromstr_tokens(name: &syn::Ident) -> proc_macro2::TokenStream {
427    quote! {
428        impl core::str::FromStr for #name {
429            type Err = ::humanbyte::ParseError;
430
431            fn from_str(value: &str) -> core::result::Result<Self, Self::Err> {
432                ::humanbyte::parse(value).map(Self)
433            }
434        }
435    }
436}
437
438#[proc_macro_derive(HumanByteParse)]
439pub fn humanbyte_parse(input: TokenStream) -> TokenStream {
440    let input = parse_macro_input!(input as DeriveInput);
441    TokenStream::from(parse_tokens(&input.ident))
442}
443
444fn parse_tokens(name: &syn::Ident) -> proc_macro2::TokenStream {
445    quote! {
446        impl #name {
447            /// Returns the size as a string with an optional SI unit.
448            #[inline(always)]
449            pub fn to_string_as(&self, format: ::humanbyte::Format) -> ::humanbyte::String {
450                ::humanbyte::to_string(self.0, format)
451            }
452
453            /// Returns the size as a string with the given number of decimals.
454            #[inline(always)]
455            pub fn to_string_with_precision(
456                &self,
457                format: ::humanbyte::Format,
458                precision: usize,
459            ) -> ::humanbyte::String {
460                ::humanbyte::to_string_with_precision(self.0, format, precision)
461            }
462
463            /// Returns the inner u64 value.
464            #[inline(always)]
465            pub const fn as_u64(&self) -> u64 {
466                self.0
467            }
468
469            /// Returns the inner value as usize.
470            #[cfg(target_pointer_width = "64")]
471            #[inline(always)]
472            pub const fn as_usize(&self) -> usize {
473                self.0 as usize
474            }
475        }
476    }
477}
478
479#[proc_macro_derive(HumanByteSerde)]
480pub fn humanbyte_serde(input: TokenStream) -> TokenStream {
481    let input = parse_macro_input!(input as DeriveInput);
482    TokenStream::from(serde_tokens(&input.ident))
483}
484
485fn serde_tokens(name: &syn::Ident) -> proc_macro2::TokenStream {
486    quote! {
487        impl<'de> ::humanbyte::serde_crate::Deserialize<'de> for #name {
488            fn deserialize<D>(deserializer: D) -> core::result::Result<Self, D::Error>
489            where
490                D: ::humanbyte::serde_crate::Deserializer<'de>,
491            {
492                struct ByteSizeVisitor;
493
494                impl<'de> ::humanbyte::serde_crate::de::Visitor<'de> for ByteSizeVisitor {
495                    type Value = #name;
496
497                    fn expecting(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
498                        formatter.write_str("an integer or string")
499                    }
500
501                    fn visit_i64<E: ::humanbyte::serde_crate::de::Error>(self, value: i64) -> core::result::Result<Self::Value, E> {
502                        if let Ok(val) = u64::try_from(value) {
503                            Ok(#name(val))
504                        } else {
505                            Err(E::invalid_value(
506                                ::humanbyte::serde_crate::de::Unexpected::Signed(value),
507                                &"integer overflow",
508                            ))
509                        }
510                    }
511
512                    fn visit_u64<E: ::humanbyte::serde_crate::de::Error>(self, value: u64) -> core::result::Result<Self::Value, E> {
513                        Ok(#name(value))
514                    }
515
516                    fn visit_str<E: ::humanbyte::serde_crate::de::Error>(self, value: &str) -> core::result::Result<Self::Value, E> {
517                        if let Ok(val) = value.parse() {
518                            Ok(val)
519                        } else {
520                            Err(E::invalid_value(
521                                ::humanbyte::serde_crate::de::Unexpected::Str(value),
522                                &"parsable string",
523                            ))
524                        }
525                    }
526                }
527
528                if deserializer.is_human_readable() {
529                    deserializer.deserialize_any(ByteSizeVisitor)
530                } else {
531                    deserializer.deserialize_u64(ByteSizeVisitor)
532                }
533            }
534        }
535        impl ::humanbyte::serde_crate::Serialize for #name {
536            fn serialize<S>(&self, serializer: S) -> core::result::Result<S::Ok, S::Error>
537            where
538                S: ::humanbyte::serde_crate::Serializer,
539            {
540                if serializer.is_human_readable() {
541                    <str>::serialize(self.to_string().as_str(), serializer)
542                } else {
543                    self.0.serialize(serializer)
544                }
545            }
546        }
547    }
548}
549
550#[proc_macro_derive(HumanByteSchema)]
551pub fn humanbyte_schema(input: TokenStream) -> TokenStream {
552    let input = parse_macro_input!(input as DeriveInput);
553    TokenStream::from(schemars_tokens(&input.ident))
554}
555
556fn schemars_tokens(name: &syn::Ident) -> proc_macro2::TokenStream {
557    quote! {
558        impl ::humanbyte::schemars_crate::JsonSchema for #name {
559            fn schema_name() -> ::humanbyte::Cow<'static, str> {
560                ::humanbyte::Cow::Borrowed(stringify!(#name))
561            }
562
563            fn schema_id() -> ::humanbyte::Cow<'static, str> {
564                ::humanbyte::Cow::Borrowed(concat!(module_path!(), "::", stringify!(#name)))
565            }
566
567            fn json_schema(
568                _generator: &mut ::humanbyte::schemars_crate::SchemaGenerator,
569            ) -> ::humanbyte::schemars_crate::Schema {
570                ::humanbyte::schemars_crate::json_schema!({
571                    "type": ["string", "integer"],
572                    "description": "A byte size, as either a human-readable string (e.g. \"1.5 KiB\") or a number of bytes",
573                })
574            }
575        }
576    }
577}