Skip to main content

pick/formats/
logfmt.rs

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        // Parse key
28        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..]; // consume =
43
44            if remaining.starts_with('"') {
45                // Quoted value
46                remaining = &remaining[1..]; // consume opening quote
47                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                        // Handle escape sequences
56                        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                // Unquoted value
88                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            // Boolean flag (key without value)
95            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}