use serde_json::Value;
use crate::entry::LogEntry;
pub fn parse_line(line: &str) -> Option<LogEntry> {
if line.trim().is_empty() {
return None;
}
let value: Value = serde_json::from_str(line).ok()?;
let obj = match value {
Value::Object(map) => map,
_ => return None,
};
let mut entry = LogEntry::new(line);
for (key, value) in obj {
match key.as_str() {
"timestamp" => match coerce_scalar_to_string(&value) {
Some(s) => entry.timestamp = Some(s),
None => {
entry.fields.insert(key, value);
}
},
"level" => match coerce_scalar_to_string(&value) {
Some(s) => entry.level = Some(s),
None => {
entry.fields.insert(key, value);
}
},
"message" => match coerce_scalar_to_string(&value) {
Some(s) => entry.message = Some(s),
None => {
entry.fields.insert(key, value);
}
},
"tag" => match coerce_scalar_to_string(&value) {
Some(s) => entry.tag = Some(s),
None => {
entry.fields.insert(key, value);
}
},
_ => {
entry.fields.insert(key, value);
}
}
}
Some(entry)
}
fn coerce_scalar_to_string(v: &Value) -> Option<String> {
match v {
Value::String(s) => Some(s.clone()),
Value::Number(n) => Some(n.to_string()),
Value::Bool(b) => Some(b.to_string()),
Value::Null => Some("null".to_string()),
Value::Object(_) | Value::Array(_) => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn parses_a_fully_populated_line() {
let line = r#"{"timestamp":"2026-04-19T10:00:00Z","level":"error","message":"boom","service":"payments","req_id":42}"#;
let e = parse_line(line).expect("should parse");
assert_eq!(e.timestamp.as_deref(), Some("2026-04-19T10:00:00Z"));
assert_eq!(e.level.as_deref(), Some("error"));
assert_eq!(e.message.as_deref(), Some("boom"));
assert!(e.tag.is_none());
assert_eq!(e.fields.get("service"), Some(&json!("payments")));
assert_eq!(e.fields.get("req_id"), Some(&json!(42)));
assert_eq!(e.raw, line);
}
#[test]
fn missing_known_fields_become_none_without_panic() {
let e = parse_line(r#"{"level":"info"}"#).expect("should parse");
assert_eq!(e.level.as_deref(), Some("info"));
assert!(e.timestamp.is_none());
assert!(e.message.is_none());
assert!(e.tag.is_none());
assert!(e.fields.is_empty());
}
#[test]
fn malformed_json_returns_none() {
assert!(parse_line(r#"{"level": "error""#).is_none()); assert!(parse_line("not json at all").is_none());
assert!(parse_line("{this is broken}").is_none());
}
#[test]
fn empty_and_whitespace_lines_return_none() {
assert!(parse_line("").is_none());
assert!(parse_line(" ").is_none());
assert!(parse_line("\t\n").is_none());
}
#[test]
fn valid_json_but_not_an_object_returns_none() {
assert!(parse_line("42").is_none());
assert!(parse_line(r#""hello""#).is_none());
assert!(parse_line("[1,2,3]").is_none());
assert!(parse_line("true").is_none());
assert!(parse_line("null").is_none());
}
#[test]
fn unknown_keys_land_in_fields_map() {
let e =
parse_line(r#"{"user_id":"u-1","duration_ms":123,"ok":true}"#).expect("should parse");
assert_eq!(e.fields.len(), 3);
assert_eq!(e.fields.get("user_id"), Some(&json!("u-1")));
assert_eq!(e.fields.get("duration_ms"), Some(&json!(123)));
assert_eq!(e.fields.get("ok"), Some(&json!(true)));
}
#[test]
fn numeric_level_is_stringified() {
let e = parse_line(r#"{"level":3}"#).expect("should parse");
assert_eq!(e.level.as_deref(), Some("3"));
assert!(e.fields.is_empty());
}
#[test]
fn boolean_and_null_known_fields_are_stringified() {
let e = parse_line(r#"{"tag":true,"message":null}"#).expect("should parse");
assert_eq!(e.tag.as_deref(), Some("true"));
assert_eq!(e.message.as_deref(), Some("null"));
}
#[test]
fn object_valued_known_field_is_preserved_in_fields_map() {
let line = r#"{"message":{"code":500,"text":"err"}}"#;
let e = parse_line(line).expect("should parse");
assert!(e.message.is_none());
assert_eq!(
e.fields.get("message"),
Some(&json!({"code": 500, "text": "err"}))
);
}
#[test]
fn array_valued_known_field_is_preserved_in_fields_map() {
let e = parse_line(r#"{"tag":["a","b"]}"#).expect("should parse");
assert!(e.tag.is_none());
assert_eq!(e.fields.get("tag"), Some(&json!(["a", "b"])));
}
#[test]
fn raw_is_preserved_verbatim_including_whitespace() {
let line = " {\"level\":\"info\"} ";
let e = parse_line(line).expect("should parse");
assert_eq!(e.raw, line);
}
#[test]
fn empty_json_object_is_a_valid_entry() {
let e = parse_line("{}").expect("should parse");
assert!(e.timestamp.is_none());
assert!(e.level.is_none());
assert!(e.message.is_none());
assert!(e.tag.is_none());
assert!(e.fields.is_empty());
assert_eq!(e.raw, "{}");
}
}