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 {}