Skip to main content

krabka_units/
serde_units.rs

1//! `serde` adapters, so a config or API struct can hold quantities.
2//!
3//! `uom`'s own `Serialize`/`Deserialize` encodes the raw base-unit float: a
4//! timeout as `30.0`, and a size as `536870912.0`. An operator does not write
5//! that in YAML, and an admin API should not return it. Use these modules through
6//! `#[serde(with = ...)]`. They give a choice of two encodings:
7//!
8//! - [`human`] — the operator-facing string form (`"512MiB"`, `"30s"`,
9//!   `"10MiB/s"`), for config files.
10//! - [`numeric`] — an exact integer in a named unit (`30000` milliseconds,
11//!   `536870912` bytes), for JSON APIs and anything mirroring a Kafka wire field.
12//!
13//! ```
14//! use krabka_units::{prelude::*, serde_units};
15//! use serde::{Deserialize, Serialize};
16//!
17//! #[derive(Serialize, Deserialize)]
18//! struct TopicConfig {
19//!     #[serde(with = "serde_units::human::byte_size")]
20//!     segment_size: ByteSize,
21//!     #[serde(with = "serde_units::numeric::millis_i64")]
22//!     retention: Time,
23//! }
24//!
25//! let parsed: TopicConfig =
26//!     serde_json::from_str(r#"{"segment_size":"512MiB","retention":604800000}"#)?;
27//! assert_eq!(parsed.segment_size, mebibytes(512));
28//! assert_eq!(parsed.retention, days(7));
29//! # Ok::<_, serde_json::Error>(())
30//! ```
31//!
32//! A dimensioned field in [`human`] form must carry its unit. These modules
33//! reject a bare number and never assume seconds or bytes, because that guess is
34//! the failure this crate prevents. [`human::ratio`] is the exception. A
35//! fraction's unit is "none", so it accepts `0.25` as well as `"25%"`.
36
37/// The operator-facing string encoding: `"512MiB"`, `"30s"`, `"10MiB/s"`, `"25%"`.
38pub mod human {
39    use crate::parse;
40
41    /// Defines a `#[serde(with = ...)]` module over a quantity's human form, plus
42    /// an `option_`-prefixed sibling for `Option` fields.
43    macro_rules! human_module {
44        ($(#[$meta:meta])* $name:ident / $option_name:ident, $quantity:ty, $parse:path) => {
45            $(#[$meta])*
46            pub mod $name {
47                use serde::de::{Deserialize as _, Error as _};
48                use serde::{Deserializer, Serializer};
49
50                use crate::fmt::Human as _;
51
52                /// Writes the quantity as its human string form.
53                ///
54                /// # Errors
55                ///
56                /// Whatever the serializer reports for a string.
57                pub fn serialize<S: Serializer>(
58                    value: &$quantity,
59                    serializer: S,
60                ) -> Result<S::Ok, S::Error> {
61                    serializer.collect_str(&value.human())
62                }
63
64                /// Reads the quantity from its human string form.
65                ///
66                /// # Errors
67                ///
68                /// If the value is not a string, or not a quantity of this
69                /// dimension with an explicit unit.
70                pub fn deserialize<'de, D: Deserializer<'de>>(
71                    deserializer: D,
72                ) -> Result<$quantity, D::Error> {
73                    let raw = String::deserialize(deserializer)?;
74                    $parse(&raw).map_err(D::Error::custom)
75                }
76            }
77
78            $(#[$meta])*
79            ///
80            /// The `Option` form of the sibling module: `null` is `None`.
81            pub mod $option_name {
82                use serde::de::{Deserialize as _, Error as _};
83                use serde::{Deserializer, Serializer};
84
85                use crate::fmt::Human as _;
86
87                /// Writes the quantity as its human string form, or `null`.
88                ///
89                /// # Errors
90                ///
91                /// Whatever the serializer reports for an optional string.
92                pub fn serialize<S: Serializer>(
93                    value: &Option<$quantity>,
94                    serializer: S,
95                ) -> Result<S::Ok, S::Error> {
96                    match value {
97                        Some(value) => serializer.collect_str(&value.human()),
98                        None => serializer.serialize_none(),
99                    }
100                }
101
102                /// Reads the quantity from its human string form, or `null`.
103                ///
104                /// # Errors
105                ///
106                /// If the value is neither `null` nor a string holding a quantity
107                /// of this dimension with an explicit unit.
108                pub fn deserialize<'de, D: Deserializer<'de>>(
109                    deserializer: D,
110                ) -> Result<Option<$quantity>, D::Error> {
111                    Option::<String>::deserialize(deserializer)?
112                        .map(|raw| $parse(&raw).map_err(D::Error::custom))
113                        .transpose()
114                }
115            }
116        };
117    }
118
119    human_module!(
120        /// A byte count as `"512MiB"`.
121        byte_size
122            / option_byte_size,
123        crate::ByteSize,
124        super::parse::byte_size
125    );
126    human_module!(
127        /// A time extent as `"30s"`.
128        time / option_time,
129        crate::Time,
130        super::parse::time
131    );
132    human_module!(
133        /// A byte throughput as `"10MiB/s"`.
134        byte_rate
135            / option_byte_rate,
136        crate::ByteRate,
137        super::parse::byte_rate
138    );
139    human_module!(
140        /// An event rate as `"100/s"`.
141        frequency
142            / option_frequency,
143        crate::Frequency,
144        super::parse::frequency
145    );
146
147    /// A dimensionless fraction as `"25%"` or `0.25`.
148    pub mod ratio {
149        use serde::{
150            Deserializer, Serializer,
151            de::{Deserialize as _, Error as _, Unexpected},
152        };
153
154        use crate::{Ratio, fmt::Human as _, fraction};
155
156        /// Writes the fraction as a percentage string.
157        ///
158        /// # Errors
159        ///
160        /// Whatever the serializer reports for a string.
161        pub fn serialize<S: Serializer>(value: &Ratio, serializer: S) -> Result<S::Ok, S::Error> {
162            serializer.collect_str(&value.human())
163        }
164
165        /// Reads the fraction from a percentage string or a bare number.
166        ///
167        /// # Errors
168        ///
169        /// If the value is neither a number nor a string holding a fraction.
170        pub fn deserialize<'de, D: Deserializer<'de>>(deserializer: D) -> Result<Ratio, D::Error> {
171            match Encoded::deserialize(deserializer)? {
172                Encoded::Text(raw) => super::parse::ratio(&raw).map_err(D::Error::custom),
173                Encoded::Number(value) if value.is_finite() => Ok(fraction(value)),
174                Encoded::Number(value) => Err(D::Error::invalid_value(
175                    Unexpected::Float(value),
176                    &"a finite fraction",
177                )),
178            }
179        }
180
181        /// Either encoding a fraction may arrive in.
182        #[derive(serde::Deserialize)]
183        #[serde(untagged)]
184        pub(super) enum Encoded {
185            /// `"25%"`.
186            Text(String),
187            /// `0.25`.
188            Number(f64),
189        }
190    }
191
192    /// A dimensionless fraction as `"25%"` or `0.25`, or `null`.
193    pub mod option_ratio {
194        use serde::{
195            Deserializer, Serializer,
196            de::{Deserialize as _, Error as _, Unexpected},
197        };
198
199        use super::ratio::Encoded;
200        use crate::{Ratio, fmt::Human as _, fraction};
201
202        /// Writes the fraction as a percentage string, or `null`.
203        ///
204        /// # Errors
205        ///
206        /// Whatever the serializer reports for an optional string.
207        pub fn serialize<S: Serializer>(
208            value: &Option<Ratio>,
209            serializer: S,
210        ) -> Result<S::Ok, S::Error> {
211            match value {
212                Some(value) => serializer.collect_str(&value.human()),
213                None => serializer.serialize_none(),
214            }
215        }
216
217        /// Reads the fraction from a percentage string, a bare number, or `null`.
218        ///
219        /// # Errors
220        ///
221        /// If the value is neither `null` nor a number nor a string holding a
222        /// fraction.
223        pub fn deserialize<'de, D: Deserializer<'de>>(
224            deserializer: D,
225        ) -> Result<Option<Ratio>, D::Error> {
226            match Option::<Encoded>::deserialize(deserializer)? {
227                None => Ok(None),
228                Some(Encoded::Text(raw)) => super::parse::ratio(&raw)
229                    .map(Some)
230                    .map_err(D::Error::custom),
231                Some(Encoded::Number(value)) if value.is_finite() => Ok(Some(fraction(value))),
232                Some(Encoded::Number(value)) => Err(D::Error::invalid_value(
233                    Unexpected::Float(value),
234                    &"a finite fraction",
235                )),
236            }
237        }
238    }
239}
240
241/// The exact integer encoding, in an explicitly named unit.
242pub mod numeric {
243    use crate::convert::{ByteRateExt, ByteSizeExt, TimeExt};
244
245    /// Defines a `#[serde(with = ...)]` module encoding a quantity as an integer
246    /// in one named unit, plus an `option_`-prefixed sibling.
247    ///
248    /// `$into` and `$from` are named as fully-qualified trait-method paths, so the
249    /// generated modules need no trait imports.
250    macro_rules! numeric_module {
251        (
252            $(#[$meta:meta])*
253            $name:ident / $option_name:ident,
254            $quantity:ty, $raw:ty, $into:path, $from:path
255        ) => {
256            $(#[$meta])*
257            pub mod $name {
258                use serde::de::Deserialize as _;
259                use serde::{Deserializer, Serialize as _, Serializer};
260
261                /// Writes the quantity as an integer in this module's unit.
262                ///
263                /// # Errors
264                ///
265                /// Whatever the serializer reports for an integer.
266                pub fn serialize<S: Serializer>(
267                    value: &$quantity,
268                    serializer: S,
269                ) -> Result<S::Ok, S::Error> {
270                    let raw: $raw = $into(*value);
271                    raw.serialize(serializer)
272                }
273
274                /// Reads the quantity from an integer in this module's unit.
275                ///
276                /// # Errors
277                ///
278                /// If the value is not an integer of the underlying width.
279                pub fn deserialize<'de, D: Deserializer<'de>>(
280                    deserializer: D,
281                ) -> Result<$quantity, D::Error> {
282                    Ok($from(<$raw>::deserialize(deserializer)?))
283                }
284            }
285
286            $(#[$meta])*
287            ///
288            /// The `Option` form of the sibling module: `null` is `None`.
289            pub mod $option_name {
290                use serde::de::Deserialize as _;
291                use serde::{Deserializer, Serialize as _, Serializer};
292
293                /// Writes the quantity as an integer in this module's unit, or `null`.
294                ///
295                /// # Errors
296                ///
297                /// Whatever the serializer reports for an optional integer.
298                pub fn serialize<S: Serializer>(
299                    value: &Option<$quantity>,
300                    serializer: S,
301                ) -> Result<S::Ok, S::Error> {
302                    value.map($into).serialize(serializer)
303                }
304
305                /// Reads the quantity from an integer in this module's unit, or `null`.
306                ///
307                /// # Errors
308                ///
309                /// If the value is neither `null` nor an integer of the underlying width.
310                pub fn deserialize<'de, D: Deserializer<'de>>(
311                    deserializer: D,
312                ) -> Result<Option<$quantity>, D::Error> {
313                    Ok(Option::<$raw>::deserialize(deserializer)?.map($from))
314                }
315            }
316        };
317    }
318
319    numeric_module!(
320        /// A time extent as whole milliseconds. This is Kafka's unit for
321        /// retention, timeout, and expiry fields.
322        millis_i64
323            / option_millis_i64,
324        crate::Time,
325        i64,
326        super::TimeExt::millis_i64,
327        super::TimeExt::from_millis
328    );
329    numeric_module!(
330        /// A time extent as whole milliseconds, **truncated** on the way out.
331        ///
332        /// Use this module to match an external format that integer-divides and
333        /// does not round, such as Tempo's `durationMs`. Reading is exact, so
334        /// this module is asymmetric on purpose. See
335        /// [`TimeExt::millis_i64_trunc`](crate::convert::TimeExt::millis_i64_trunc).
336        millis_i64_trunc
337            / option_millis_i64_trunc,
338        crate::Time,
339        i64,
340        super::TimeExt::millis_i64_trunc,
341        super::TimeExt::from_millis
342    );
343    numeric_module!(
344        /// A time extent as whole seconds.
345        secs_i64
346            / option_secs_i64,
347        crate::Time,
348        i64,
349        super::TimeExt::secs_i64,
350        super::TimeExt::from_secs
351    );
352    numeric_module!(
353        /// A time extent as whole nanoseconds.
354        nanos_i64
355            / option_nanos_i64,
356        crate::Time,
357        i64,
358        super::TimeExt::nanos_i64,
359        super::TimeExt::from_nanos
360    );
361    numeric_module!(
362        /// A byte count as an unsigned total.
363        bytes_u64
364            / option_bytes_u64,
365        crate::ByteSize,
366        u64,
367        super::ByteSizeExt::bytes_u64,
368        super::ByteSizeExt::from_bytes
369    );
370    numeric_module!(
371        /// A byte count as Kafka's `int64` byte fields.
372        bytes_i64
373            / option_bytes_i64,
374        crate::ByteSize,
375        i64,
376        super::ByteSizeExt::bytes_i64,
377        super::ByteSizeExt::from_bytes_i64
378    );
379    numeric_module!(
380        /// A byte throughput as Kafka's `int64` quota fields.
381        bytes_per_sec_i64
382            / option_bytes_per_sec_i64,
383        crate::ByteRate,
384        i64,
385        super::ByteRateExt::bytes_per_sec_i64,
386        super::ByteRateExt::from_bytes_per_sec
387    );
388}