Skip to main content

smart_format/combinator/
mod.rs

1mod conditional;
2mod pad;
3mod truncate;
4mod wrap;
5
6use core::fmt;
7
8pub use self::conditional::*;
9pub use self::pad::*;
10pub use self::truncate::*;
11pub use self::wrap::*;
12
13/// Extension trait that adds composable formatting combinators to any `Display` type.
14///
15/// Every method returns a lightweight wrapper struct that itself implements `Display`.
16/// This means combinators chain without intermediate allocations:
17///
18/// ```
19/// use smart_format::prelude::*;
20///
21/// let output = "hello".display_suffix("!").display_prefix("> ").to_string();
22/// assert_eq!(output, "> hello!");
23/// ```
24///
25/// # Design
26///
27/// Each combinator is a struct wrapping `T: Display` — a zero-allocation adapter by
28/// construction. The pattern is the same one used by iterator adapters: lazy evaluation,
29/// composed through trait objects (`fmt::Formatter`), with no heap allocation until
30/// someone explicitly materializes the result (`.to_string()`, `write!`, etc.).
31///
32/// Width-sensitive operations (`display_truncate`, `display_pad_left`, `display_pad_right`)
33/// count Unicode scalar values (`char`), not bytes or grapheme clusters. This is a known
34/// limitation documented on each method.
35pub trait SmartFormat: fmt::Display + Sized {
36    /// Wrap with a prefix and suffix: `prefix + self + suffix`.
37    fn display_wrap<P: fmt::Display, S: fmt::Display>(
38        self,
39        prefix: P,
40        suffix: S,
41    ) -> Wrap<Self, P, S> {
42        Wrap::new(self, prefix, suffix)
43    }
44
45    /// Prepend a prefix: `prefix + self`.
46    fn display_prefix<P: fmt::Display>(self, prefix: P) -> Prefix<Self, P> {
47        Prefix::new(self, prefix)
48    }
49
50    /// Append a suffix: `self + suffix`.
51    fn display_suffix<S: fmt::Display>(self, suffix: S) -> Suffix<Self, S> {
52        Suffix::new(self, suffix)
53    }
54
55    /// Conditionally display: if `condition` is true, display self; otherwise write nothing.
56    fn display_if(self, condition: bool) -> If<Self> {
57        If::new(self, condition)
58    }
59
60    /// Conditionally choose: if `use_fallback` is false, display self; otherwise display `fallback`.
61    ///
62    /// This uses a `bool` flag rather than inspecting the inner value for emptiness.
63    /// Value-inspection-based fallback would require either allocation or a `DisplayMeta`
64    /// trait (planned for a future version).
65    fn display_or_if<U: fmt::Display>(self, use_fallback: bool, fallback: U) -> Or<Self, U> {
66        Or::new(self, use_fallback, fallback)
67    }
68
69    /// Truncate to at most `max_chars` Unicode scalar values.
70    ///
71    /// If the formatted output is shorter than `max_chars`, it passes through unchanged.
72    /// If longer, it is cut at the char boundary before the limit. Output is guaranteed
73    /// to be `<= max_chars` characters.
74    ///
75    /// Uses `char` count, not grapheme clusters or terminal display width.
76    fn display_truncate(self, max_chars: usize) -> Truncate<Self> {
77        Truncate::new(self, max_chars)
78    }
79
80    /// Truncate with a tail indicator (e.g., "...").
81    ///
82    /// `max_chars` is the **total output budget** including the tail. Inner content is
83    /// truncated at `max_chars - tail.len_in_chars()` chars, then the tail is appended.
84    /// If the inner content fits within the budget, no truncation occurs and the tail
85    /// is not shown.
86    fn display_truncate_with(self, max_chars: usize, tail: &'static str) -> TruncateWith<Self> {
87        TruncateWith::new(self, max_chars, tail)
88    }
89
90    /// Right-pad to `width` characters using `fill`.
91    ///
92    /// If the formatted output is already `>= width` characters, no padding is added.
93    /// Uses `char` count, not grapheme clusters or terminal display width.
94    fn display_pad_right(self, width: usize, fill: char) -> PadRight<Self> {
95        PadRight::new(self, width, fill)
96    }
97
98    /// Left-pad to `width` characters using `fill`.
99    ///
100    /// If the formatted output is already `>= width` characters, no padding is added.
101    ///
102    /// # Implementation note
103    ///
104    /// Uses two-pass formatting: first pass counts chars (discarding output), second
105    /// pass writes padding then content. This is zero-alloc but formats the inner value
106    /// twice. For the short values where padding typically matters, this cost is negligible.
107    ///
108    /// Assumes `Display::fmt` is deterministic — non-deterministic implementations
109    /// (e.g., values including timestamps) may produce incorrect padding width.
110    fn display_pad_left(self, width: usize, fill: char) -> PadLeft<Self> {
111        PadLeft::new(self, width, fill)
112    }
113}
114
115impl<T: fmt::Display> SmartFormat for T {}