mycelium_util/fmt.rs
1//! Text formatting utilities.
2pub use core::fmt::*;
3pub use tracing::field::{debug, display, DebugValue};
4
5/// A wrapper type that formats the wrapped value using a provided function.
6///
7/// This is used to implement the [`fmt::alt`](alt), [`fmt::bin`](bin),
8/// [`fmt::hex`](hex), and [`fmt::ptr`](ptr) functions.
9pub struct FormatWith<T, F = fn(&T, &mut Formatter<'_>) -> Result>
10where
11 F: Fn(&T, &mut Formatter<'_>) -> Result,
12{
13 value: T,
14 fmt: F,
15}
16
17/// Wraps a type implementing [`core::fmt::Write`] so that every newline written to
18/// that writer is indented a given amount.
19#[derive(Debug)]
20pub struct WithIndent<'writer, W> {
21 writer: &'writer mut W,
22 indent: usize,
23}
24
25/// Extension trait adding additional methods to types implementing [`core::fmt::Write`].
26pub trait WriteExt: Write {
27 /// Wraps `self` in a [`WithIndent`] writer that indents every new line
28 /// that's written to it by `indent` spaces.
29 #[must_use]
30 #[inline]
31 fn with_indent(&mut self, indent: usize) -> WithIndent<'_, Self>
32 where
33 Self: Sized,
34 {
35 WithIndent {
36 writer: self,
37 indent,
38 }
39 }
40}
41
42/// A utility to assist with formatting [`Option`] values.
43///
44/// This wraps a reference to an [`Option`] value, and implements formatting
45/// traits by formatting the `Option`'s contents if it is [`Some`]. If the
46/// `Option` is [`None`], the formatting trait implementations emit no text by
47/// default, or a string provided using the [`or_else`](Self::or_else) method.
48///
49/// A `FmtOption` will implement the [`core::fmt::Display`],
50/// [`core::fmt::Debug`], [`core::fmt::Binary`], [`core::fmt::UpperHex`],
51/// [`core::fmt::LowerHex`], and [`core::fmt::Pointer`] formatting traits, if
52/// the inner type implements the corresponding trait.
53///
54/// The [`fmt::opt`](opt) method can be used as shorthand to borrow an `Option`
55/// as a `FmtOption`.
56///
57/// # Examples
58///
59/// Formatting a [`Some`] value emits that value's [`Debug`] and [`Display`] output:
60///
61/// ```
62/// use mycelium_util::fmt;
63///
64/// let value = Some("hello world");
65/// let debug = format!("{:?}", fmt::opt(&value));
66/// assert_eq!(debug, "\"hello world\"");
67///
68/// let display = format!("{}", fmt::opt(&value));
69/// assert_eq!(display, "hello world");
70/// ```
71///
72/// Formatting a [`None`] value generates no text by default:
73///
74/// ```
75/// use mycelium_util::fmt;
76///
77/// let value: Option<&str> = None;
78/// let debug = format!("{:?}", fmt::opt(&value));
79/// assert_eq!(debug, "");
80///
81/// let display = format!("{}", fmt::opt(&value));
82/// assert_eq!(display, "");
83/// ```
84///
85/// The [`or_else`](Self::or_else) method can be used to customize the text that
86/// is emitted when the value is [`None`]:
87///
88/// ```
89/// use mycelium_util::fmt;
90/// use core::ptr::NonNull;
91///
92/// let value: Option<NonNull<u8>> = None;
93/// let debug = format!("{:?}", fmt::opt(&value).or_else("null"));
94/// assert_eq!(debug, "null");
95/// ```
96#[derive(Clone)]
97pub struct FmtOption<'a, T> {
98 opt: Option<&'a T>,
99 or_else: &'a str,
100}
101
102/// Format the provided value using its [`core::fmt::Pointer`] implementation.
103///
104/// # Examples
105/// ```
106/// use mycelium_util::fmt;
107/// use tracing::debug;
108///
109/// let something = "a string";
110/// let some_ref = &something;
111///
112/// debug!(x = ?some_ref); // will format the pointed value ("a string")
113/// debug!(x = fmt::ptr(some_ref)); // will format the address.
114///
115/// ```
116#[inline]
117#[must_use]
118pub fn ptr<T: Pointer>(value: T) -> DebugValue<FormatWith<T>> {
119 tracing::field::debug(FormatWith {
120 value,
121 fmt: Pointer::fmt,
122 })
123}
124
125/// Format the provided value using its [`core::fmt::LowerHex`] implementation.
126///
127/// # Examples
128/// ```
129/// use mycelium_util::fmt;
130/// use tracing::debug;
131///
132/// let n = 0xf00;
133///
134/// debug!(some_number = ?n); // will be formatted as "some_number=3840"
135/// debug!(some_number = fmt::hex(n)); //will be formatted as "some_number=0xf00"
136///
137/// ```
138#[inline]
139#[must_use]
140pub fn hex<T: LowerHex>(value: T) -> DebugValue<FormatWith<T>> {
141 tracing::field::debug(FormatWith {
142 value,
143 fmt: |value, f: &mut Formatter<'_>| write!(f, "{value:#x}"),
144 })
145}
146
147/// Format the provided value using its [`core::fmt::Binary`] implementation.
148///
149/// # Examples
150/// ```
151/// use mycelium_util::fmt;
152/// use tracing::debug;
153///
154/// let n = 42;
155///
156/// debug!(some_number = ?n); // will be formatted as "some_number=42"
157/// debug!(some_number = fmt::bin(n)); //will be formatted as "some_number=0b101010"
158///
159/// ```
160#[must_use]
161#[inline]
162pub fn bin<T: Binary>(value: T) -> DebugValue<FormatWith<T>> {
163 tracing::field::debug(FormatWith {
164 value,
165 fmt: |value, f: &mut Formatter<'_>| write!(f, "{value:#b}"),
166 })
167}
168
169/// Format the provided value using its [`core::fmt::Debug`] implementation,
170/// with "alternate mode" set
171///
172/// # Examples
173/// ```
174/// use mycelium_util::fmt;
175/// use tracing::debug;
176///
177/// #[derive(Debug)]
178/// struct Thing {
179/// question: &'static str,
180/// answer: usize,
181/// }
182/// let thing = Thing {
183/// question: "life, the universe, and everything",
184/// answer: 42,
185/// };
186///
187/// debug!(something = ?thing); // will be formatted on the current line
188/// debug!(something = fmt::alt(&thing)); // will be formatted with newlines and indentation
189///
190/// ```
191#[must_use]
192#[inline]
193pub fn alt<T: Debug>(value: T) -> DebugValue<FormatWith<T>> {
194 tracing::field::debug(FormatWith {
195 value,
196 fmt: |value, f: &mut Formatter<'_>| write!(f, "{value:#?}"),
197 })
198}
199
200/// Borrows an [`Option`] as a [`FmtOption`] that formats the inner value if
201/// the [`Option`] is [`Some`], or emits a customizable string if the [`Option`]
202/// is [`None`].
203///
204/// # Examples
205///
206/// Formatting a [`Some`] value emits that value's [`Debug`] and [`Display`] output:
207///
208/// ```
209/// use mycelium_util::fmt;
210///
211/// let value = Some("hello world");
212/// let debug = format!("{:?}", fmt::opt(&value));
213/// assert_eq!(debug, "\"hello world\"");
214///
215/// let display = format!("{}", fmt::opt(&value));
216/// assert_eq!(display, "hello world");
217/// ```
218///
219/// Formatting a [`None`] value generates no text by default:
220///
221/// ```
222/// use mycelium_util::fmt;
223///
224/// let value: Option<&str> = None;
225/// let debug = format!("{:?}", fmt::opt(&value));
226/// assert_eq!(debug, "");
227///
228/// let display = format!("{}", fmt::opt(&value));
229/// assert_eq!(display, "");
230/// ```
231///
232/// The [`or_else`](FmtOption::or_else) method can be used to customize the text that
233/// is emitted when the value is [`None`]:
234///
235/// ```
236/// use mycelium_util::fmt;
237/// use core::ptr::NonNull;
238///
239/// let value: Option<NonNull<u8>> = None;
240/// let debug = format!("{:?}", fmt::opt(&value).or_else("null"));
241/// assert_eq!(debug, "null");
242/// ```
243#[must_use]
244#[inline]
245pub const fn opt<T>(value: &Option<T>) -> FmtOption<'_, T> {
246 FmtOption::new(value)
247}
248
249/// Formats a list of `F`-typed values to the provided `writer`, delimited with commas.
250pub fn comma_delimited<F: Display>(
251 mut writer: impl Write,
252 values: impl IntoIterator<Item = F>,
253) -> Result {
254 let mut values = values.into_iter();
255 if let Some(value) = values.next() {
256 write!(writer, "{value}")?;
257 for value in values {
258 write!(writer, ", {value}")?;
259 }
260 }
261
262 Ok(())
263}
264
265impl<T, F> Debug for FormatWith<T, F>
266where
267 F: Fn(&T, &mut Formatter<'_>) -> Result,
268{
269 #[inline]
270 fn fmt(&self, f: &mut Formatter<'_>) -> Result {
271 (self.fmt)(&self.value, f)
272 }
273}
274
275impl<W: Write> Write for WithIndent<'_, W> {
276 fn write_str(&mut self, mut s: &str) -> Result {
277 while !s.is_empty() {
278 let (split, nl) = match s.find('\n') {
279 Some(pos) => (pos + 1, true),
280 None => (s.len(), false),
281 };
282 self.writer.write_str(&s[..split])?;
283 if nl {
284 for _ in 0..self.indent {
285 self.writer.write_char(' ')?;
286 }
287 }
288 s = &s[split..];
289 }
290
291 Ok(())
292 }
293}
294
295impl<W> WriteExt for W where W: Write {}
296
297// === impl FmtOption ===
298
299impl<'a, T> FmtOption<'a, T> {
300 /// Returns a new `FmtOption` that formats the provided [`Option`] value.
301 ///
302 /// The [`fmt::opt`](opt) function can be used as shorthand for this.
303 #[must_use]
304 #[inline]
305 pub const fn new(opt: &'a Option<T>) -> Self {
306 Self {
307 opt: opt.as_ref(),
308 or_else: "",
309 }
310 }
311
312 /// Set the text to emit when the value is [`None`].
313 ///
314 /// # Examples
315 ///
316 /// If the value is [`None`], the `or_else` text is emitted:
317 ///
318 /// ```
319 /// use mycelium_util::fmt;
320 /// use core::ptr::NonNull;
321 ///
322 /// let value: Option<NonNull<u8>> = None;
323 /// let debug = format!("{:?}", fmt::opt(&value).or_else("null"));
324 /// assert_eq!(debug, "null");
325 /// ```
326 ///
327 /// If the value is [`Some`], this function does nothing:
328 ///
329 /// ```
330 /// # use mycelium_util::fmt;
331 /// # use core::ptr::NonNull;
332 /// let value = Some("hello world");
333 /// let debug = format!("{}", fmt::opt(&value).or_else("no string"));
334 /// assert_eq!(debug, "hello world");
335 /// ```
336 #[must_use]
337 #[inline]
338 pub fn or_else(self, or_else: &'a str) -> Self {
339 Self { or_else, ..self }
340 }
341}
342
343impl<T: Debug> Debug for FmtOption<'_, T> {
344 #[inline]
345 fn fmt(&self, f: &mut Formatter<'_>) -> Result {
346 match self.opt {
347 Some(val) => val.fmt(f),
348 None => f.write_str(self.or_else),
349 }
350 }
351}
352
353impl<T: Display> Display for FmtOption<'_, T> {
354 #[inline]
355 fn fmt(&self, f: &mut Formatter<'_>) -> Result {
356 match self.opt {
357 Some(val) => val.fmt(f),
358 None => f.write_str(self.or_else),
359 }
360 }
361}
362
363impl<T: Binary> Binary for FmtOption<'_, T> {
364 #[inline]
365 fn fmt(&self, f: &mut Formatter<'_>) -> Result {
366 match self.opt {
367 Some(val) => val.fmt(f),
368 None => f.write_str(self.or_else),
369 }
370 }
371}
372
373impl<T: UpperHex> UpperHex for FmtOption<'_, T> {
374 #[inline]
375 fn fmt(&self, f: &mut Formatter<'_>) -> Result {
376 match self.opt {
377 Some(val) => val.fmt(f),
378 None => f.write_str(self.or_else),
379 }
380 }
381}
382
383impl<T: LowerHex> LowerHex for FmtOption<'_, T> {
384 #[inline]
385 fn fmt(&self, f: &mut Formatter<'_>) -> Result {
386 match self.opt {
387 Some(val) => val.fmt(f),
388 None => f.write_str(self.or_else),
389 }
390 }
391}
392
393impl<T: Pointer> Pointer for FmtOption<'_, T> {
394 #[inline]
395 fn fmt(&self, f: &mut Formatter<'_>) -> Result {
396 match self.opt {
397 Some(val) => val.fmt(f),
398 None => f.write_str(self.or_else),
399 }
400 }
401}