1use crate::error::PickError;
2use serde_json::Value;
3
4pub fn parse(input: &str) -> Result<Value, PickError> {
5 let lines: Vec<&str> = input.lines().filter(|l| !l.trim().is_empty()).collect();
6
7 if lines.is_empty() {
8 return Err(PickError::ParseError("logfmt".into(), "empty input".into()));
9 }
10
11 if lines.len() == 1 {
12 parse_line(lines[0]).map(Value::Object)
13 } else {
14 let entries: Result<Vec<Value>, _> = lines
15 .iter()
16 .map(|line| parse_line(line).map(Value::Object))
17 .collect();
18 Ok(Value::Array(entries?))
19 }
20}
21
22fn parse_line(line: &str) -> Result<serde_json::Map<String, Value>, PickError> {
23 let mut map = serde_json::Map::new();
24 let mut remaining = line.trim();
25
26 while !remaining.is_empty() {
27 let key_end = remaining.find(['=', ' ']).unwrap_or(remaining.len());
29 let key = &remaining[..key_end];
30
31 if key.is_empty() {
32 remaining = remaining.trim_start();
33 if remaining.is_empty() {
34 break;
35 }
36 continue;
37 }
38
39 remaining = &remaining[key_end..];
40
41 if remaining.starts_with('=') {
42 remaining = &remaining[1..]; if remaining.starts_with('"') {
45 remaining = &remaining[1..]; let mut value = String::new();
48 let mut chars = remaining.chars();
49 let mut consumed = 0;
50 let mut found_close = false;
51
52 while let Some(c) = chars.next() {
53 consumed += c.len_utf8();
54 if c == '\\' {
55 if let Some(next) = chars.next() {
57 consumed += next.len_utf8();
58 match next {
59 '"' => value.push('"'),
60 '\\' => value.push('\\'),
61 'n' => value.push('\n'),
62 't' => value.push('\t'),
63 other => {
64 value.push('\\');
65 value.push(other);
66 }
67 }
68 }
69 } else if c == '"' {
70 found_close = true;
71 break;
72 } else {
73 value.push(c);
74 }
75 }
76
77 if !found_close {
78 return Err(PickError::ParseError(
79 "logfmt".into(),
80 "unterminated quoted value".into(),
81 ));
82 }
83
84 map.insert(key.to_string(), Value::String(value));
85 remaining = &remaining[consumed..];
86 } else {
87 let end = remaining.find(' ').unwrap_or(remaining.len());
89 let value = &remaining[..end];
90 map.insert(key.to_string(), Value::String(value.to_string()));
91 remaining = &remaining[end..];
92 }
93 } else {
94 map.insert(key.to_string(), Value::Bool(true));
96 }
97
98 remaining = remaining.trim_start();
99 }
100
101 if map.is_empty() {
102 return Err(PickError::ParseError(
103 "logfmt".into(),
104 "no key-value pairs found".into(),
105 ));
106 }
107
108 Ok(map)
109}
110
111#[cfg(test)]
112mod tests {
113 use super::*;
114 use serde_json::json;
115
116 #[test]
117 fn parse_simple() {
118 let v = parse("level=info msg=hello status=200").unwrap();
119 assert_eq!(v["level"], json!("info"));
120 assert_eq!(v["msg"], json!("hello"));
121 assert_eq!(v["status"], json!("200"));
122 }
123
124 #[test]
125 fn parse_quoted_value() {
126 let v = parse("level=info msg=\"hello world\" status=200").unwrap();
127 assert_eq!(v["msg"], json!("hello world"));
128 }
129
130 #[test]
131 fn parse_boolean_flag() {
132 let v = parse("verbose level=info").unwrap();
133 assert_eq!(v["verbose"], json!(true));
134 assert_eq!(v["level"], json!("info"));
135 }
136
137 #[test]
138 fn parse_multiline() {
139 let input = "level=info msg=req1\nlevel=error msg=req2";
140 let v = parse(input).unwrap();
141 assert!(v.is_array());
142 assert_eq!(v[0]["level"], json!("info"));
143 assert_eq!(v[1]["level"], json!("error"));
144 }
145
146 #[test]
147 fn parse_escaped_quote() {
148 let v = parse(r#"msg="say \"hello\"""#).unwrap();
149 assert_eq!(v["msg"], json!("say \"hello\""));
150 }
151
152 #[test]
153 fn parse_empty_quoted() {
154 let v = parse("key=\"\" other=val").unwrap();
155 assert_eq!(v["key"], json!(""));
156 }
157
158 #[test]
159 fn parse_special_chars_in_value() {
160 let v = parse("url=https://example.com/path?q=1&r=2 status=200").unwrap();
161 assert_eq!(v["url"], json!("https://example.com/path?q=1&r=2"));
162 }
163
164 #[test]
165 fn parse_single_line_result_is_object() {
166 let v = parse("a=1 b=2").unwrap();
167 assert!(v.is_object());
168 }
169
170 #[test]
171 fn parse_empty_input() {
172 assert!(parse("").is_err());
173 }
174
175 #[test]
176 fn parse_whitespace_only() {
177 assert!(parse(" \n \n ").is_err());
178 }
179
180 #[test]
181 fn parse_with_timestamp() {
182 let v = parse("ts=2024-01-15T10:30:00Z level=info msg=started").unwrap();
183 assert_eq!(v["ts"], json!("2024-01-15T10:30:00Z"));
184 }
185
186 #[test]
187 fn parse_escaped_newline() {
188 let v = parse(r#"msg="line1\nline2" level=info"#).unwrap();
189 assert_eq!(v["msg"], json!("line1\nline2"));
190 }
191}