Skip to main content

log_io/
value.rs

1//! Field values.
2//!
3//! [`Value`] is a borrowed sum type for the data attached to a log
4//! [`crate::Field`]. Borrowing keeps the fast path allocation-free; if
5//! a caller needs to construct a value from owned data, it should
6//! borrow at the call site.
7
8use core::fmt;
9
10/// A field value attached to a log record.
11///
12/// All variants borrow their data. Construct a `Value` at the call site
13/// from a borrowed source so the record carries no heap allocation.
14#[derive(Debug, Clone, Copy, PartialEq, Default)]
15pub enum Value<'a> {
16    /// Absence of a value. Serializes as `null` in JSON, the empty
17    /// string in logfmt, and `<null>` in the human-readable format.
18    #[default]
19    Null,
20    /// A boolean.
21    Bool(bool),
22    /// A signed 64-bit integer.
23    I64(i64),
24    /// An unsigned 64-bit integer.
25    U64(u64),
26    /// A 64-bit float. `NaN` and `inf` are serialized as JSON `null`
27    /// per RFC 8259 since they have no JSON representation; logfmt and
28    /// the human format emit the lowercase debug representation.
29    F64(f64),
30    /// A borrowed string slice.
31    Str(&'a str),
32    /// A borrowed character. Serialized as a one-character string.
33    Char(char),
34}
35
36impl<'a> Value<'a> {
37    /// Returns `true` if this value is [`Self::Null`].
38    pub const fn is_null(&self) -> bool {
39        matches!(self, Self::Null)
40    }
41
42    /// Returns the static name of the variant. Useful in diagnostics.
43    pub const fn variant_name(&self) -> &'static str {
44        match self {
45            Self::Null => "null",
46            Self::Bool(_) => "bool",
47            Self::I64(_) => "i64",
48            Self::U64(_) => "u64",
49            Self::F64(_) => "f64",
50            Self::Str(_) => "str",
51            Self::Char(_) => "char",
52        }
53    }
54}
55
56impl<'a> fmt::Display for Value<'a> {
57    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
58        match self {
59            Self::Null => f.write_str("null"),
60            Self::Bool(b) => fmt::Display::fmt(b, f),
61            Self::I64(n) => fmt::Display::fmt(n, f),
62            Self::U64(n) => fmt::Display::fmt(n, f),
63            Self::F64(n) => fmt::Display::fmt(n, f),
64            Self::Str(s) => f.write_str(s),
65            Self::Char(c) => fmt::Display::fmt(c, f),
66        }
67    }
68}
69
70// Ergonomic conversions for call sites that don't want to spell out
71// the variant.
72
73impl<'a> From<&'a str> for Value<'a> {
74    fn from(value: &'a str) -> Self {
75        Self::Str(value)
76    }
77}
78
79impl From<bool> for Value<'_> {
80    fn from(value: bool) -> Self {
81        Self::Bool(value)
82    }
83}
84
85impl From<char> for Value<'_> {
86    fn from(value: char) -> Self {
87        Self::Char(value)
88    }
89}
90
91macro_rules! from_int {
92    ($($ty:ty => $variant:ident),* $(,)?) => {
93        $(
94            impl From<$ty> for Value<'_> {
95                fn from(value: $ty) -> Self {
96                    #[allow(clippy::cast_lossless)]
97                    Self::$variant(value as _)
98                }
99            }
100        )*
101    };
102}
103
104from_int!(
105    i8 => I64, i16 => I64, i32 => I64, i64 => I64, isize => I64,
106    u8 => U64, u16 => U64, u32 => U64, u64 => U64, usize => U64,
107);
108
109impl From<f32> for Value<'_> {
110    fn from(value: f32) -> Self {
111        Self::F64(f64::from(value))
112    }
113}
114
115impl From<f64> for Value<'_> {
116    fn from(value: f64) -> Self {
117        Self::F64(value)
118    }
119}
120
121impl<'a, T> From<Option<T>> for Value<'a>
122where
123    T: Into<Value<'a>>,
124{
125    fn from(value: Option<T>) -> Self {
126        match value {
127            Some(v) => v.into(),
128            None => Self::Null,
129        }
130    }
131}
132
133#[cfg(all(test, feature = "std"))]
134mod tests {
135    use super::*;
136
137    #[test]
138    fn from_str_is_borrowed() {
139        let s = String::from("hello");
140        let v: Value<'_> = s.as_str().into();
141        assert_eq!(v, Value::Str("hello"));
142    }
143
144    #[test]
145    fn integer_conversions() {
146        let v: Value<'_> = 42_i32.into();
147        assert_eq!(v, Value::I64(42));
148        let v: Value<'_> = 42_u32.into();
149        assert_eq!(v, Value::U64(42));
150        let v: Value<'_> = (-1_i64).into();
151        assert_eq!(v, Value::I64(-1));
152    }
153
154    #[test]
155    fn option_into_value() {
156        let some: Value<'_> = Some(7_u32).into();
157        assert_eq!(some, Value::U64(7));
158        let none: Value<'_> = Option::<u32>::None.into();
159        assert!(none.is_null());
160    }
161
162    #[test]
163    fn variant_name_covers_all() {
164        for v in [
165            Value::Null,
166            Value::Bool(true),
167            Value::I64(0),
168            Value::U64(0),
169            Value::F64(0.0),
170            Value::Str(""),
171            Value::Char('a'),
172        ] {
173            assert!(!v.variant_name().is_empty());
174        }
175    }
176
177    #[test]
178    fn display_renders_each_variant() {
179        assert_eq!(Value::Null.to_string(), "null");
180        assert_eq!(Value::Bool(true).to_string(), "true");
181        assert_eq!(Value::I64(-5).to_string(), "-5");
182        assert_eq!(Value::U64(5).to_string(), "5");
183        assert_eq!(Value::Str("x").to_string(), "x");
184        assert_eq!(Value::Char('y').to_string(), "y");
185    }
186}