Skip to main content

saf_core/logging/
value.rs

1//! `SafLogValue` trait for formatting Rust values into the SAF log DSL.
2//!
3//! Each type maps to a DSL value type:
4//! - IDs (`EntityId` impls) → `0x` + hex
5//! - Sets (`BTreeSet<T>`) → `{a,b,c}`
6//! - Lists (`Vec<T>`, `&[T]`) → `[a,b,c]`
7//! - Pairs (`SafPair<T>`) → `a->b`
8//! - Ratios (`SafRatio`) → `n/m`
9//! - Deltas (`PtsDelta<T>`) → `+{a,b}` or `-{a,b}`
10//! - Primitives → bare values
11
12use std::collections::BTreeSet;
13use std::fmt::Write as _;
14use std::time::Duration;
15
16use crate::ids::EntityId;
17
18/// Trait for formatting values into the SAF log DSL output.
19pub trait SafLogValue {
20    /// Append the DSL-formatted representation to `buf`.
21    fn fmt_saf_log(&self, buf: &mut String);
22
23    /// Convenience: format to a new `String`.
24    fn to_saf_log(&self) -> String {
25        let mut buf = String::new();
26        self.fmt_saf_log(&mut buf);
27        buf
28    }
29}
30
31// --- Primitives ---
32
33impl SafLogValue for bool {
34    fn fmt_saf_log(&self, buf: &mut String) {
35        buf.push_str(if *self { "true" } else { "false" });
36    }
37}
38
39macro_rules! impl_saf_log_int {
40    ($($ty:ty),*) => {
41        $(
42            impl SafLogValue for $ty {
43                fn fmt_saf_log(&self, buf: &mut String) {
44                    let _ = write!(buf, "{self}");
45                }
46            }
47        )*
48    };
49}
50
51impl_saf_log_int!(u8, u16, u32, u64, usize, i8, i16, i32, i64, isize);
52
53impl SafLogValue for f64 {
54    fn fmt_saf_log(&self, buf: &mut String) {
55        let _ = write!(buf, "{self:.3}");
56    }
57}
58
59// --- Strings ---
60
61impl SafLogValue for str {
62    fn fmt_saf_log(&self, buf: &mut String) {
63        buf.push_str(self);
64    }
65}
66
67impl SafLogValue for String {
68    fn fmt_saf_log(&self, buf: &mut String) {
69        buf.push_str(self);
70    }
71}
72
73impl SafLogValue for &str {
74    fn fmt_saf_log(&self, buf: &mut String) {
75        buf.push_str(self);
76    }
77}
78
79// --- IDs (u128 newtypes via EntityId) ---
80
81impl SafLogValue for u128 {
82    fn fmt_saf_log(&self, buf: &mut String) {
83        let _ = write!(buf, "0x{self:032x}");
84    }
85}
86
87/// Blanket impl for all `EntityId` types (`ValueId`, `LocId`, etc.).
88impl<T: EntityId> SafLogValue for T {
89    fn fmt_saf_log(&self, buf: &mut String) {
90        self.raw().fmt_saf_log(buf);
91    }
92}
93
94// --- Collections ---
95
96impl<T: SafLogValue> SafLogValue for BTreeSet<T> {
97    fn fmt_saf_log(&self, buf: &mut String) {
98        buf.push('{');
99        for (i, item) in self.iter().enumerate() {
100            if i > 0 {
101                buf.push(',');
102            }
103            item.fmt_saf_log(buf);
104        }
105        buf.push('}');
106    }
107}
108
109impl<T: SafLogValue> SafLogValue for Vec<T> {
110    fn fmt_saf_log(&self, buf: &mut String) {
111        self.as_slice().fmt_saf_log(buf);
112    }
113}
114
115impl<T: SafLogValue> SafLogValue for [T] {
116    fn fmt_saf_log(&self, buf: &mut String) {
117        buf.push('[');
118        for (i, item) in self.iter().enumerate() {
119            if i > 0 {
120                buf.push(',');
121            }
122            item.fmt_saf_log(buf);
123        }
124        buf.push(']');
125    }
126}
127
128// --- Duration ---
129
130impl SafLogValue for Duration {
131    fn fmt_saf_log(&self, buf: &mut String) {
132        let _ = write!(buf, "{:.3}s", self.as_secs_f64());
133    }
134}
135
136// --- Option ---
137
138impl<T: SafLogValue> SafLogValue for Option<T> {
139    fn fmt_saf_log(&self, buf: &mut String) {
140        match self {
141            Some(v) => v.fmt_saf_log(buf),
142            None => buf.push_str("none"),
143        }
144    }
145}
146
147// --- Newtype wrappers for ambiguous types ---
148
149/// A directed pair rendered as `a->b`.
150pub struct SafPair<'a, T: SafLogValue>(pub &'a T, pub &'a T);
151
152impl<T: SafLogValue> SafLogValue for SafPair<'_, T> {
153    fn fmt_saf_log(&self, buf: &mut String) {
154        self.0.fmt_saf_log(buf);
155        buf.push_str("->");
156        self.1.fmt_saf_log(buf);
157    }
158}
159
160/// A ratio rendered as `n/m`.
161pub struct SafRatio(pub usize, pub usize);
162
163impl SafLogValue for SafRatio {
164    fn fmt_saf_log(&self, buf: &mut String) {
165        let _ = write!(buf, "{}/{}", self.0, self.1);
166    }
167}
168
169/// A set delta rendered as `+{a,b}` (added) or `-{a,b}` (removed).
170pub enum PtsDelta<'a, T: SafLogValue> {
171    /// Items added.
172    Added(&'a [T]),
173    /// Items removed.
174    Removed(&'a [T]),
175}
176
177impl<T: SafLogValue> SafLogValue for PtsDelta<'_, T> {
178    fn fmt_saf_log(&self, buf: &mut String) {
179        let (prefix, items) = match self {
180            PtsDelta::Added(items) => ("+", *items),
181            PtsDelta::Removed(items) => ("-", *items),
182        };
183        buf.push_str(prefix);
184        buf.push('{');
185        for (i, item) in items.iter().enumerate() {
186            if i > 0 {
187                buf.push(',');
188            }
189            item.fmt_saf_log(buf);
190        }
191        buf.push('}');
192    }
193}
194
195#[cfg(test)]
196mod tests {
197    use super::*;
198
199    #[test]
200    fn test_bool() {
201        assert_eq!(true.to_saf_log(), "true");
202        assert_eq!(false.to_saf_log(), "false");
203    }
204
205    #[test]
206    fn test_integers() {
207        assert_eq!(42_u32.to_saf_log(), "42");
208        assert_eq!((-1_i32).to_saf_log(), "-1");
209        assert_eq!(0_usize.to_saf_log(), "0");
210    }
211
212    #[test]
213    fn test_u128_id() {
214        assert_eq!(
215            0x1a2b_u128.to_saf_log(),
216            "0x00000000000000000000000000001a2b"
217        );
218    }
219
220    #[test]
221    fn test_string() {
222        assert_eq!("main".to_saf_log(), "main");
223        assert_eq!(String::from("foo").to_saf_log(), "foo");
224    }
225
226    #[test]
227    fn test_btreeset() {
228        let set: BTreeSet<u32> = [3, 1, 2].into_iter().collect();
229        assert_eq!(set.to_saf_log(), "{1,2,3}");
230    }
231
232    #[test]
233    fn test_vec() {
234        let v = vec![10_u32, 20, 30];
235        assert_eq!(v.to_saf_log(), "[10,20,30]");
236    }
237
238    #[test]
239    fn test_duration() {
240        let d = Duration::from_secs_f64(1.2345);
241        assert_eq!(d.to_saf_log(), "1.234s");
242    }
243
244    #[test]
245    fn test_option() {
246        let some: Option<u32> = Some(42);
247        let none: Option<u32> = None;
248        assert_eq!(some.to_saf_log(), "42");
249        assert_eq!(none.to_saf_log(), "none");
250    }
251
252    #[test]
253    fn test_pair() {
254        let a = 1_u32;
255        let b = 2_u32;
256        assert_eq!(SafPair(&a, &b).to_saf_log(), "1->2");
257    }
258
259    #[test]
260    fn test_ratio() {
261        assert_eq!(SafRatio(12, 50).to_saf_log(), "12/50");
262    }
263
264    #[test]
265    fn test_delta() {
266        let items = [1_u32, 2, 3];
267        assert_eq!(PtsDelta::Added(&items).to_saf_log(), "+{1,2,3}");
268        assert_eq!(PtsDelta::Removed(&items).to_saf_log(), "-{1,2,3}");
269    }
270}