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}