sofie_values/
log_fields.rs1use std::borrow::Cow;
4
5use sofie_core::Instant;
6
7use crate::{Currency, Email, Money, Secret};
8
9#[derive(Clone, Debug, PartialEq)]
10pub enum LogValue<'a> {
11 Integer(i64),
12 Float(f64),
13 Bool(bool),
14 Text(Cow<'a, str>),
15}
16
17pub trait LogFields {
18 fn log_fields(&self, field: &mut dyn FnMut(Option<&'static str>, LogValue<'_>));
20}
21
22pub trait SensitiveFields {
24 const SENSITIVE_FIELDS: &'static [&'static str];
25}
26
27impl<T: SensitiveFields + ?Sized> SensitiveFields for &T {
28 const SENSITIVE_FIELDS: &'static [&'static str] = T::SENSITIVE_FIELDS;
29}
30
31impl<T: SensitiveFields + ?Sized> SensitiveFields for &mut T {
32 const SENSITIVE_FIELDS: &'static [&'static str] = T::SENSITIVE_FIELDS;
33}
34
35macro_rules! log_whole {
36 ($($value:ty),+ => |$self_:ident| $logged:expr) => {$(
37 impl LogFields for $value {
38 fn log_fields(&self, field: &mut dyn FnMut(Option<&'static str>, LogValue<'_>)) {
39 let $self_ = self;
40 field(None, $logged);
41 }
42 }
43 )+};
44}
45
46log_whole!(i64 => |number| LogValue::Integer(*number));
47log_whole!(i16, i32, u16, u32 => |number| LogValue::Integer(i64::from(*number)));
48log_whole!(bool => |flag| LogValue::Bool(*flag));
49log_whole!(String => |text| LogValue::Text(Cow::Borrowed(text)));
50log_whole!(Currency => |code| LogValue::Text(Cow::Borrowed(code.as_str())));
51log_whole!(Instant => |instant| LogValue::Text(Cow::Owned(instant.to_string())));
52log_whole!(Email => |email| masked(email));
53
54fn masked(value: &impl std::fmt::Debug) -> LogValue<'static> {
55 LogValue::Text(Cow::Owned(format!("{value:?}")))
56}
57
58impl<T> LogFields for Secret<T> {
59 fn log_fields(&self, field: &mut dyn FnMut(Option<&'static str>, LogValue<'_>)) {
60 field(None, masked(self));
61 }
62}
63
64impl<V: crate::SurfacesFromInner<Inner: LogFields> + std::fmt::Debug> LogFields for V {
65 fn log_fields(&self, field: &mut dyn FnMut(Option<&'static str>, LogValue<'_>)) {
66 if <V as crate::SurfacesFromInner>::SENSITIVE {
67 field(None, masked(self));
68 } else {
69 self.inner().log_fields(field);
70 }
71 }
72}
73
74impl<T: LogFields> LogFields for Option<T> {
75 fn log_fields(&self, field: &mut dyn FnMut(Option<&'static str>, LogValue<'_>)) {
76 if let Some(value) = self {
77 value.log_fields(field);
78 }
79 }
80}
81
82impl LogFields for Money {
83 fn log_fields(&self, field: &mut dyn FnMut(Option<&'static str>, LogValue<'_>)) {
84 field(Some("minor"), LogValue::Integer(self.minor()));
85 field(
86 Some("currency"),
87 LogValue::Text(Cow::Borrowed(self.currency().as_str())),
88 );
89 }
90}
91
92#[cfg(feature = "time")]
93log_whole!(crate::Date => |day| LogValue::Text(Cow::Owned(day.to_string())));
94#[cfg(feature = "time")]
95log_whole!(crate::TimeZone => |zone| LogValue::Text(Cow::Borrowed(zone.name())));
96#[cfg(feature = "time")]
97log_whole!(crate::TimeOfDay => |time| LogValue::Text(Cow::Owned(time.to_string())));
98#[cfg(feature = "time")]
99log_whole!(crate::Weekday => |weekday| LogValue::Text(Cow::Borrowed(sofie_core::Variants::name(*weekday))));
100#[cfg(feature = "phone")]
101log_whole!(crate::PhoneNumber => |phone| masked(phone));
102#[cfg(feature = "person")]
103log_whole!(crate::PersonName => |name| masked(name));
104#[cfg(feature = "percentage")]
105log_whole!(crate::Percentage => |share| LogValue::Integer(share.basis_points()));
106#[cfg(feature = "web")]
107log_whole!(crate::Slug, crate::Locale, crate::Url => |text| LogValue::Text(Cow::Borrowed(text.as_str())));
108
109#[cfg(feature = "time")]
110impl LogFields for crate::LocalDateTime {
111 fn log_fields(&self, field: &mut dyn FnMut(Option<&'static str>, LogValue<'_>)) {
112 field(
113 Some("local"),
114 LogValue::Text(Cow::Owned(self.local().to_string())),
115 );
116 field(
117 Some("zone"),
118 LogValue::Text(Cow::Borrowed(self.zone().name())),
119 );
120 }
121}
122
123#[cfg(feature = "geo")]
125impl LogFields for crate::Coordinate {
126 fn log_fields(&self, field: &mut dyn FnMut(Option<&'static str>, LogValue<'_>)) {
127 let rounded = |degrees: f64| LogValue::Float((degrees * 100.0).round() / 100.0);
128 field(Some("latitude"), rounded(self.latitude()));
129 field(Some("longitude"), rounded(self.longitude()));
130 }
131}
132
133#[cfg(feature = "measure")]
134impl<U: crate::Unit> LogFields for crate::Measurement<U> {
135 fn log_fields(&self, field: &mut dyn FnMut(Option<&'static str>, LogValue<'_>)) {
136 field(
137 Some("quantity"),
138 LogValue::Text(Cow::Owned(self.quantity())),
139 );
140 field(
141 Some("unit"),
142 LogValue::Text(Cow::Borrowed(self.unit().name())),
143 );
144 }
145}
146
147#[cfg(test)]
148mod tests {
149 use super::*;
150
151 fn logged(value: &impl LogFields) -> Vec<(Option<&'static str>, String)> {
152 let mut fields = Vec::new();
153 value.log_fields(&mut |member, value| fields.push((member, format!("{value:?}"))));
154 fields
155 }
156
157 #[test]
158 fn money_logs_its_minor_units_and_currency() {
159 let total = Money::new(14990, Currency::parse("EUR").unwrap());
160 assert_eq!(
161 logged(&total),
162 [
163 (Some("minor"), "Integer(14990)".to_owned()),
164 (Some("currency"), "Text(\"EUR\")".to_owned())
165 ]
166 );
167 }
168
169 #[cfg(feature = "geo")]
170 #[test]
171 fn a_coordinate_logs_rounded_to_about_a_kilometre() {
172 let home = crate::Coordinate::try_new(41.008_237, -28.978_359).unwrap();
173 assert_eq!(
174 logged(&home),
175 [
176 (Some("latitude"), "Float(41.01)".to_owned()),
177 (Some("longitude"), "Float(-28.98)".to_owned())
178 ]
179 );
180 }
181
182 struct Pin(String);
183
184 impl std::fmt::Debug for Pin {
185 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
186 sofie_core::value::write_masked(formatter, "Pin")
187 }
188 }
189
190 impl sofie_core::Value for Pin {
191 type Inner = String;
192
193 fn inner(&self) -> &String {
194 &self.0
195 }
196
197 fn try_from_inner(inner: String) -> Result<Self, impl std::fmt::Display> {
198 Ok::<_, std::convert::Infallible>(Self(inner))
199 }
200 }
201
202 impl crate::SurfacesFromInner for Pin {
203 const SENSITIVE: bool = true;
204 }
205
206 #[test]
207 fn a_sensitive_derived_value_logs_its_mask_and_filters_whole() {
208 assert_eq!(
209 logged(&Pin("1234".to_owned())),
210 [(None, "Text(\"Pin(***)\")".to_owned())]
211 );
212 #[cfg(feature = "filter")]
213 assert_eq!(<Pin as crate::Filter>::OPERATORS, crate::EXACT);
214 }
215
216 #[test]
217 fn sensitive_values_log_only_their_mask() {
218 let email = Email::parse("jane@example.com").unwrap();
219 assert_eq!(
220 logged(&email),
221 [(None, "Text(\"j***@example.com\")".to_owned())]
222 );
223 let key = Secret::new("sk_live_1".to_owned());
224 assert_eq!(logged(&key), [(None, "Text(\"Secret(***)\")".to_owned())]);
225 assert_eq!(logged(&None::<Email>), []);
226 }
227}