pub mod json;
pub mod logfmt;
pub mod plain;
use crate::entry::LogEntry;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum LogFormat {
#[default]
Json,
Logfmt,
Plain,
}
impl LogFormat {
pub const ALL: &'static [Self] = &[Self::Json, Self::Logfmt, Self::Plain];
pub fn from_name(s: &str) -> Option<Self> {
match s.to_ascii_lowercase().as_str() {
"json" => Some(Self::Json),
"logfmt" => Some(Self::Logfmt),
"plain" => Some(Self::Plain),
_ => None,
}
}
pub fn name(self) -> &'static str {
match self {
Self::Json => "json",
Self::Logfmt => "logfmt",
Self::Plain => "plain",
}
}
}
impl std::fmt::Display for LogFormat {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.name())
}
}
pub fn parse_line(format: LogFormat, line: &str) -> Option<LogEntry> {
match format {
LogFormat::Json => json::parse_line(line),
LogFormat::Logfmt => logfmt::parse_line(line),
LogFormat::Plain => plain::parse_line(line),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn from_name_recognizes_known_formats() {
assert_eq!(LogFormat::from_name("json"), Some(LogFormat::Json));
assert_eq!(LogFormat::from_name("logfmt"), Some(LogFormat::Logfmt));
assert_eq!(LogFormat::from_name("plain"), Some(LogFormat::Plain));
}
#[test]
fn from_name_is_case_insensitive() {
assert_eq!(LogFormat::from_name("JSON"), Some(LogFormat::Json));
assert_eq!(LogFormat::from_name("Logfmt"), Some(LogFormat::Logfmt));
assert_eq!(LogFormat::from_name("PLAIN"), Some(LogFormat::Plain));
}
#[test]
fn from_name_returns_none_for_unknown() {
assert!(LogFormat::from_name("yaml").is_none());
assert!(LogFormat::from_name("plaintext").is_none()); assert!(LogFormat::from_name("").is_none());
}
#[test]
fn name_round_trips_through_from_name() {
for variant in [LogFormat::Json, LogFormat::Logfmt, LogFormat::Plain] {
assert_eq!(LogFormat::from_name(variant.name()), Some(variant));
}
}
#[test]
fn default_is_json() {
assert_eq!(LogFormat::default(), LogFormat::Json);
}
#[test]
fn display_uses_canonical_name() {
assert_eq!(format!("{}", LogFormat::Json), "json");
assert_eq!(format!("{}", LogFormat::Logfmt), "logfmt");
assert_eq!(format!("{}", LogFormat::Plain), "plain");
}
#[test]
fn all_contains_every_variant() {
assert_eq!(LogFormat::ALL.len(), 3);
assert!(LogFormat::ALL.contains(&LogFormat::Json));
assert!(LogFormat::ALL.contains(&LogFormat::Logfmt));
assert!(LogFormat::ALL.contains(&LogFormat::Plain));
}
#[test]
fn all_names_round_trip_through_from_name() {
for format in LogFormat::ALL {
assert_eq!(LogFormat::from_name(format.name()), Some(*format));
}
}
#[test]
fn dispatcher_json_matches_direct_json_call() {
let line = r#"{"timestamp":"2026-04-15T09:00:00Z","level":"info","message":"hi"}"#;
let direct = json::parse_line(line);
let routed = parse_line(LogFormat::Json, line);
assert_eq!(direct, routed);
assert!(direct.is_some(), "fixture line should parse");
}
#[test]
fn dispatcher_logfmt_matches_direct_logfmt_call() {
let line = "level=info service=payments req_id=42";
let direct = logfmt::parse_line(line);
let routed = parse_line(LogFormat::Logfmt, line);
assert_eq!(direct, routed);
assert!(direct.is_some());
}
#[test]
fn dispatcher_plain_matches_direct_plain_call() {
let line = "starting service version 2.4.1";
let direct = plain::parse_line(line);
let routed = parse_line(LogFormat::Plain, line);
assert_eq!(direct, routed);
assert!(direct.is_some());
}
#[test]
fn dispatcher_returns_none_for_empty_line_in_every_format() {
for format in [LogFormat::Json, LogFormat::Logfmt, LogFormat::Plain] {
assert!(parse_line(format, "").is_none(), "format {format} on empty");
assert!(
parse_line(format, " \t ").is_none(),
"format {format} on whitespace"
);
}
}
}