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