use crate::level::Level;
use crate::value::Value;
#[cfg(any(feature = "std", feature = "json", feature = "logfmt"))]
#[inline]
pub(crate) fn is_reserved_key(key: &str) -> bool {
matches!(
key,
"timestamp" | "level" | "target" | "message" | "file" | "line"
)
}
#[cfg(any(feature = "std", feature = "json", feature = "logfmt"))]
pub(crate) const RESERVED_PREFIX: &str = "fields.";
#[cfg(feature = "std")]
#[inline]
pub(crate) fn same_output_key(a: &str, b: &str) -> bool {
let (x, y) = (a.as_bytes(), b.as_bytes());
if x.len() == y.len() {
x.first() == y.first() && x.last() == y.last() && x == y
} else if x.len().abs_diff(y.len()) == RESERVED_PREFIX.len() {
is_prefixed_alias(a, b) || is_prefixed_alias(b, a)
} else {
false
}
}
#[cfg(feature = "std")]
#[inline]
fn is_prefixed_alias(plain: &str, prefixed: &str) -> bool {
prefixed.len() == plain.len() + RESERVED_PREFIX.len()
&& prefixed.strip_prefix(RESERVED_PREFIX) == Some(plain)
&& is_reserved_key(plain)
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Field<'a> {
pub key: &'a str,
pub value: Value<'a>,
}
impl<'a> Field<'a> {
pub fn new<V: Into<Value<'a>>>(key: &'a str, value: V) -> Self {
Self {
key,
value: value.into(),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
#[non_exhaustive]
pub struct Metadata<'a> {
pub level: Level,
pub target: &'a str,
pub file: Option<&'a str>,
pub line: Option<u32>,
pub timestamp_unix_nanos: Option<u128>,
}
impl<'a> Metadata<'a> {
#[must_use]
pub const fn new(level: Level, target: &'a str) -> Self {
Self {
level,
target,
file: None,
line: None,
timestamp_unix_nanos: None,
}
}
#[must_use]
pub const fn with_location(mut self, file: &'a str, line: u32) -> Self {
self.file = Some(file);
self.line = Some(line);
self
}
#[must_use]
pub const fn with_timestamp(mut self, ts_unix_nanos: u128) -> Self {
self.timestamp_unix_nanos = Some(ts_unix_nanos);
self
}
}
#[derive(Debug, Clone, Copy)]
#[non_exhaustive]
pub struct Record<'a> {
pub metadata: Metadata<'a>,
pub message: &'a str,
pub fields: &'a [Field<'a>],
pub context: &'a [Field<'a>],
}
impl<'a> Record<'a> {
#[must_use]
pub const fn new(metadata: Metadata<'a>, message: &'a str, fields: &'a [Field<'a>]) -> Self {
Self {
metadata,
message,
fields,
context: &[],
}
}
#[must_use]
pub const fn with_context(
metadata: Metadata<'a>,
message: &'a str,
fields: &'a [Field<'a>],
context: &'a [Field<'a>],
) -> Self {
Self {
metadata,
message,
fields,
context,
}
}
pub fn all_fields(&self) -> impl Iterator<Item = &Field<'a>> {
self.context.iter().chain(self.fields.iter())
}
}
#[cfg(all(test, feature = "std"))]
mod tests {
use super::*;
#[test]
fn test_field_new_accepts_typed_values() {
let f = Field::new("port", 8080_u32);
assert_eq!(f.key, "port");
assert_eq!(f.value, Value::U64(8080));
}
#[test]
fn test_metadata_chain_setters_populate_options() {
let m = Metadata::new(Level::Info, "tgt")
.with_location("file.rs", 12)
.with_timestamp(1_700_000_000_000_000_000);
assert_eq!(m.file, Some("file.rs"));
assert_eq!(m.line, Some(12));
assert_eq!(m.timestamp_unix_nanos, Some(1_700_000_000_000_000_000));
let bare = Metadata::new(Level::Info, "tgt");
assert_eq!(bare.file, None);
assert_eq!(bare.timestamp_unix_nanos, None);
}
#[test]
fn test_record_all_fields_orders_context_first() {
let ctx = [Field::new("trace_id", "abc")];
let fields = [Field::new("port", 80_u32)];
let record = Record::with_context(Metadata::new(Level::Info, "tgt"), "msg", &fields, &ctx);
let keys: Vec<_> = record.all_fields().map(|f| f.key).collect();
assert_eq!(keys, vec!["trace_id", "port"]);
let plain = Record::new(Metadata::new(Level::Info, "tgt"), "msg", &fields);
assert!(plain.context.is_empty());
}
}