Skip to main content

pick/formats/
text.rs

1use crate::error::PickError;
2use serde_json::Value;
3
4pub fn parse(input: &str) -> Result<Value, PickError> {
5    let mut map = serde_json::Map::new();
6
7    for line in input.lines() {
8        let line = line.trim();
9        if line.is_empty() {
10            continue;
11        }
12
13        // Try key=value
14        if let Some(eq_pos) = line.find('=') {
15            let key = line[..eq_pos].trim();
16            let value = line[eq_pos + 1..].trim();
17            if !key.is_empty()
18                && key
19                    .chars()
20                    .all(|c| c.is_alphanumeric() || c == '_' || c == '-' || c == '.')
21            {
22                map.insert(key.to_string(), Value::String(value.to_string()));
23                continue;
24            }
25        }
26
27        // Try key: value
28        if let Some(colon_pos) = line.find(':') {
29            let key = line[..colon_pos].trim();
30            let value = line[colon_pos + 1..].trim();
31            if !key.is_empty()
32                && !key.contains(' ')
33                && key
34                    .chars()
35                    .all(|c| c.is_alphanumeric() || c == '_' || c == '-' || c == '.')
36            {
37                map.insert(key.to_string(), Value::String(value.to_string()));
38                continue;
39            }
40        }
41
42        // Try key<tab>value
43        if let Some(tab_pos) = line.find('\t') {
44            let key = line[..tab_pos].trim();
45            let value = line[tab_pos + 1..].trim();
46            if !key.is_empty()
47                && key
48                    .chars()
49                    .all(|c| c.is_alphanumeric() || c == '_' || c == '-' || c == '.')
50            {
51                map.insert(key.to_string(), Value::String(value.to_string()));
52                continue;
53            }
54        }
55    }
56
57    if !map.is_empty() {
58        Ok(Value::Object(map))
59    } else {
60        // Fallback: return array of lines for index-based access
61        let lines: Vec<Value> = input
62            .lines()
63            .map(|l| Value::String(l.to_string()))
64            .collect();
65        Ok(Value::Array(lines))
66    }
67}
68
69/// Search for a selector string in unstructured text as a fallback
70/// when normal extraction fails on text format.
71pub fn search_text(input: &str, query: &str) -> Option<Value> {
72    // First try exact key match in key=value or key: value patterns
73    for line in input.lines() {
74        let line = line.trim();
75        if line.is_empty() {
76            continue;
77        }
78
79        // key=value
80        if let Some(eq_pos) = line.find('=') {
81            let key = line[..eq_pos].trim();
82            if key == query {
83                let value = line[eq_pos + 1..].trim();
84                return Some(Value::String(value.to_string()));
85            }
86        }
87
88        // key: value
89        if let Some(colon_pos) = line.find(':') {
90            let key = line[..colon_pos].trim();
91            if key == query {
92                let value = line[colon_pos + 1..].trim();
93                return Some(Value::String(value.to_string()));
94            }
95        }
96    }
97
98    // Substring search fallback
99    let matching: Vec<Value> = input
100        .lines()
101        .filter(|line| line.contains(query))
102        .map(|l| Value::String(l.to_string()))
103        .collect();
104
105    match matching.len() {
106        0 => None,
107        1 => Some(matching.into_iter().next().unwrap()),
108        _ => Some(Value::Array(matching)),
109    }
110}
111
112#[cfg(test)]
113mod tests {
114    use super::*;
115    use serde_json::json;
116
117    #[test]
118    fn parse_kv_equals() {
119        let v = parse("name=Alice\nage=30").unwrap();
120        assert_eq!(v["name"], json!("Alice"));
121        assert_eq!(v["age"], json!("30"));
122    }
123
124    #[test]
125    fn parse_kv_colon() {
126        let v = parse("name: Alice\nage: 30").unwrap();
127        assert_eq!(v["name"], json!("Alice"));
128        assert_eq!(v["age"], json!("30"));
129    }
130
131    #[test]
132    fn parse_kv_tab() {
133        let v = parse("name\tAlice\nage\t30").unwrap();
134        assert_eq!(v["name"], json!("Alice"));
135        assert_eq!(v["age"], json!("30"));
136    }
137
138    #[test]
139    fn parse_mixed_formats() {
140        let v = parse("name=Alice\nage: 30").unwrap();
141        assert_eq!(v["name"], json!("Alice"));
142        assert_eq!(v["age"], json!("30"));
143    }
144
145    #[test]
146    fn parse_plain_text_fallback() {
147        let v = parse("just some text\nanother line").unwrap();
148        assert!(v.is_array());
149        assert_eq!(v[0], json!("just some text"));
150        assert_eq!(v[1], json!("another line"));
151    }
152
153    #[test]
154    fn parse_empty_lines_skipped() {
155        let v = parse("\nname=Alice\n\nage=30\n").unwrap();
156        assert_eq!(v["name"], json!("Alice"));
157    }
158
159    #[test]
160    fn parse_key_with_dots() {
161        let v = parse("server.host=localhost").unwrap();
162        assert_eq!(v["server.host"], json!("localhost"));
163    }
164
165    #[test]
166    fn parse_key_with_hyphens() {
167        let v = parse("content-type: text/html").unwrap();
168        assert_eq!(v["content-type"], json!("text/html"));
169    }
170
171    // search_text tests
172
173    #[test]
174    fn search_exact_key_equals() {
175        let result = search_text("name=Alice\nage=30", "name").unwrap();
176        assert_eq!(result, json!("Alice"));
177    }
178
179    #[test]
180    fn search_exact_key_colon() {
181        let result = search_text("name: Alice\nage: 30", "age").unwrap();
182        assert_eq!(result, json!("30"));
183    }
184
185    #[test]
186    fn search_substring_single() {
187        let result = search_text("hello world\ngoodbye world", "hello").unwrap();
188        assert_eq!(result, json!("hello world"));
189    }
190
191    #[test]
192    fn search_substring_multiple() {
193        let result = search_text("error in foo\nerror in bar\ninfo ok", "error").unwrap();
194        assert!(result.is_array());
195        assert_eq!(result.as_array().unwrap().len(), 2);
196    }
197
198    #[test]
199    fn search_no_match() {
200        assert!(search_text("hello world", "nothere").is_none());
201    }
202}