1use std::io::{self, Read};
2
3use clap::Parser;
4use serde_json::Value;
5
6use crate::{Args, Error, OutputFormat};
7
8pub fn setup() -> Result<(Value, String, OutputFormat), Error> {
9 let args = Args::parse();
10
11 let content = if let Some(path) = args.path {
12 std::fs::read_to_string(path)?
13 } else {
14 let mut buffer = String::new();
15 io::stdin().read_to_string(&mut buffer)?;
16 buffer
17 };
18
19 let input_format = if args.text {
20 InputFormat::Text
21 } else {
22 detect_input_format(&content)
23 };
24
25 let data = parse_content(&content, input_format)?;
26 let query = args.query;
27
28 let format = args
29 .format
30 .parse::<OutputFormat>()
31 .map_err(|e| Error::InvalidFormat(e.to_string()))?;
32
33 Ok((data, query, format))
36}
37
38#[derive(Debug)]
39enum InputFormat {
40 Json,
41 Yaml,
42 Csv,
43 Text,
44}
45
46fn detect_input_format(content: &str) -> InputFormat {
47 let trimmed = content.trim();
48
49 if is_likely_csv(trimmed) {
51 return InputFormat::Csv;
52 }
53
54 if (trimmed.starts_with('{') && trimmed.ends_with('}'))
56 || (trimmed.starts_with('[') && trimmed.ends_with(']'))
57 {
58 if serde_json::from_str::<serde_json::Value>(trimmed).is_ok() {
60 return InputFormat::Json;
61 }
62 }
63
64 if is_structured_yaml(trimmed) {
66 return InputFormat::Yaml;
67 }
68
69 InputFormat::Text
71}
72
73fn is_structured_yaml(content: &str) -> bool {
75 let lines: Vec<&str> = content.lines().collect();
76
77 if lines.is_empty() {
78 return false;
79 }
80
81 if content.contains("apiVersion:") || content.contains("kind:")
83 || content.contains("version:") || content.contains("services:") {
84 return true;
85 }
86
87 let mut yaml_indicators = 0;
88 let mut total_meaningful_lines = 0;
89
90 for line in lines {
91 let trimmed = line.trim();
92
93 if trimmed.is_empty() || trimmed.starts_with('#') {
95 continue;
96 }
97
98 total_meaningful_lines += 1;
99
100 if is_valid_yaml_line(trimmed) {
102 yaml_indicators += 1;
103 }
104 }
105
106 if total_meaningful_lines < 3 {
108 return false;
109 }
110
111 (yaml_indicators as f64 / total_meaningful_lines as f64) > 0.8
113}
114
115fn is_valid_yaml_line(line: &str) -> bool {
117 if line.starts_with("- ") {
119 return true;
120 }
121
122 if let Some(colon_pos) = line.find(':') {
124 let key_part = line[..colon_pos].trim();
125 let value_part = line[colon_pos + 1..].trim();
126
127 if key_part.is_empty() {
129 return false;
130 }
131
132 if key_part.contains(' ') && !key_part.starts_with('"') && !key_part.starts_with('\'') {
134 return false;
135 }
136
137 if line.starts_with(" ") || line.starts_with("\t") {
139 return true;
140 }
141
142 if value_part.is_empty()
144 || value_part.starts_with('[')
145 || value_part.starts_with('{')
146 || value_part == "true"
147 || value_part == "false"
148 || value_part.parse::<f64>().is_ok() {
149 return true;
150 }
151
152 if value_part.contains('/') && value_part.len() > 10 {
154 return false;
155 }
156
157 return true;
158 }
159
160 false
161}
162
163fn parse_content(content: &str, format: InputFormat) -> Result<Value, Error> {
164 match format {
165 InputFormat::Json => serde_json::from_str(content).map_err(Error::Json),
166 InputFormat::Yaml => {
167 if content.contains("---") {
169 parse_multi_document_yaml(content)
170 } else {
171 serde_yaml::from_str(content).map_err(Error::Yaml)
172 }
173 }
174 InputFormat::Csv => parse_csv_to_json(content),
175 InputFormat::Text => parse_text_to_json(content),
176 }
177}
178
179fn parse_text_to_json(content: &str) -> Result<Value, Error> {
180 let lines: Vec<Value> = content
182 .lines()
183 .map(|line| Value::String(line.to_string()))
184 .collect();
185
186 Ok(Value::Array(lines))
188}
189
190fn parse_multi_document_yaml(content: &str) -> Result<Value, Error> {
191 let documents: Vec<&str> = content
192 .split("---")
193 .map(|doc| doc.trim())
194 .filter(|doc| !doc.is_empty())
195 .collect();
196
197 let mut parsed_docs = Vec::new();
198
199 for doc in documents {
200 let parsed: Value = serde_yaml::from_str(doc).map_err(Error::Yaml)?;
201 parsed_docs.push(parsed);
202 }
203
204 Ok(Value::Array(parsed_docs))
206}
207
208fn is_likely_csv(content: &str) -> bool {
209 let lines: Vec<&str> = content.lines().take(5).collect();
210
211 if lines.is_empty() {
212 return false;
213 }
214
215 let first_line = lines[0];
217 let comma_count = first_line.matches(',').count();
218
219 if comma_count > 0 {
221 lines.iter().skip(1).all(|line| {
223 let line_comma_count = line.matches(',').count();
224 (line_comma_count as i32 - comma_count as i32).abs() <= 1
225 })
226 } else {
227 false
228 }
229}
230
231fn parse_csv_to_json(content: &str) -> Result<Value, Error> {
232 let mut reader = csv::Reader::from_reader(content.as_bytes());
233
234 let headers: Vec<String> = reader
236 .headers()
237 .map_err(Error::Csv)?
238 .iter()
239 .map(|h| h.trim().to_string())
240 .collect();
241
242 let mut records = Vec::new();
243
244 for result in reader.records() {
245 let record = result.map_err(Error::Csv)?;
246 let mut object = serde_json::Map::new();
247
248 for (i, field) in record.iter().enumerate() {
249 if let Some(header) = headers.get(i) {
250 let value = infer_value_type(field.trim());
251 object.insert(header.clone(), value);
252 }
253 }
254
255 records.push(Value::Object(object));
256 }
257
258 Ok(Value::Array(records))
260}
261
262fn infer_value_type(field: &str) -> Value {
263 if field.is_empty() {
265 return Value::Null;
266 }
267
268 match field.to_lowercase().as_str() {
270 "true" => return Value::Bool(true),
271 "false" => return Value::Bool(false),
272 _ => {}
273 }
274
275 if let Ok(int_val) = field.parse::<i64>() {
277 return Value::Number(serde_json::Number::from(int_val));
278 }
279
280 if let Ok(float_val) = field.parse::<f64>() {
282 if let Some(num) = serde_json::Number::from_f64(float_val) {
283 return Value::Number(num);
284 }
285 }
286
287 Value::String(field.to_string())
289}
290
291pub fn text_to_json_values(content: &str) -> Result<Vec<Value>, Error> {
293 let lines: Vec<Value> = content
294 .lines()
295 .map(|line| Value::String(line.to_string()))
296 .collect();
297 Ok(lines)
298}
299
300#[cfg(test)]
301mod tests {
302 use super::*;
303
304 #[test]
305 fn test_text_parsing() {
306 let content = "line1\nline2\nERROR: something happened";
307 let result = parse_text_to_json(content).unwrap();
308
309 if let Value::Array(lines) = result {
310 assert_eq!(lines.len(), 3);
311 assert_eq!(lines[0], Value::String("line1".to_string()));
312 assert_eq!(lines[1], Value::String("line2".to_string()));
313 assert_eq!(lines[2], Value::String("ERROR: something happened".to_string()));
314 } else {
315 panic!("Expected array result");
316 }
317 }
318
319 #[test]
320 fn test_yaml_detection() {
321 use super::{is_structured_yaml, is_valid_yaml_line};
322
323 assert!(is_structured_yaml("apiVersion: v1\nkind: Pod"));
325 assert!(is_structured_yaml("key: value\nother: data\nnested:\n sub: item"));
326
327 assert!(!is_structured_yaml("2024-01-01 10:00:00 INFO Starting"));
329 assert!(!is_structured_yaml("plain text\nwith some: colons"));
330 assert!(!is_structured_yaml("ServerName: localhost\nServerPort: 8080")); assert!(is_valid_yaml_line("key: value"));
334 assert!(is_valid_yaml_line(" nested: item"));
335 assert!(is_valid_yaml_line("- list_item"));
336 assert!(!is_valid_yaml_line("2024-01-01 10:00:00 INFO message"));
337 assert!(!is_valid_yaml_line("random text line"));
338 }
339}