use std::io::{self, Read};
use clap::Parser;
use serde_json::Value;
use crate::{Args, Error, OutputFormat};
pub fn setup() -> Result<(Value, String, OutputFormat), Error> {
let args = Args::parse();
let content = if let Some(path) = args.path {
std::fs::read_to_string(path)?
} else {
let mut buffer = String::new();
io::stdin().read_to_string(&mut buffer)?;
buffer
};
let input_format = if args.text {
InputFormat::Text
} else {
detect_input_format(&content)
};
let data = parse_content(&content, input_format)?;
let query = args.query;
let format = args
.format
.parse::<OutputFormat>()
.map_err(|e| Error::InvalidFormat(e.to_string()))?;
Ok((data, query, format))
}
#[derive(Debug)]
enum InputFormat {
Json,
Yaml,
Csv,
Text,
}
fn detect_input_format(content: &str) -> InputFormat {
let trimmed = content.trim();
if is_likely_csv(trimmed) {
return InputFormat::Csv;
}
if (trimmed.starts_with('{') && trimmed.ends_with('}'))
|| (trimmed.starts_with('[') && trimmed.ends_with(']'))
{
if serde_json::from_str::<serde_json::Value>(trimmed).is_ok() {
return InputFormat::Json;
}
}
if is_structured_yaml(trimmed) {
return InputFormat::Yaml;
}
InputFormat::Text
}
fn is_structured_yaml(content: &str) -> bool {
let lines: Vec<&str> = content.lines().collect();
if lines.is_empty() {
return false;
}
if content.contains("apiVersion:") || content.contains("kind:")
|| content.contains("version:") || content.contains("services:") {
return true;
}
let mut yaml_indicators = 0;
let mut total_meaningful_lines = 0;
for line in lines {
let trimmed = line.trim();
if trimmed.is_empty() || trimmed.starts_with('#') {
continue;
}
total_meaningful_lines += 1;
if is_valid_yaml_line(trimmed) {
yaml_indicators += 1;
}
}
if total_meaningful_lines < 3 {
return false;
}
(yaml_indicators as f64 / total_meaningful_lines as f64) > 0.8
}
fn is_valid_yaml_line(line: &str) -> bool {
if line.starts_with("- ") {
return true;
}
if let Some(colon_pos) = line.find(':') {
let key_part = line[..colon_pos].trim();
let value_part = line[colon_pos + 1..].trim();
if key_part.is_empty() {
return false;
}
if key_part.contains(' ') && !key_part.starts_with('"') && !key_part.starts_with('\'') {
return false;
}
if line.starts_with(" ") || line.starts_with("\t") {
return true;
}
if value_part.is_empty()
|| value_part.starts_with('[')
|| value_part.starts_with('{')
|| value_part == "true"
|| value_part == "false"
|| value_part.parse::<f64>().is_ok() {
return true;
}
if value_part.contains('/') && value_part.len() > 10 {
return false;
}
return true;
}
false
}
fn parse_content(content: &str, format: InputFormat) -> Result<Value, Error> {
match format {
InputFormat::Json => serde_json::from_str(content).map_err(Error::Json),
InputFormat::Yaml => {
if content.contains("---") {
parse_multi_document_yaml(content)
} else {
serde_yaml::from_str(content).map_err(Error::Yaml)
}
}
InputFormat::Csv => parse_csv_to_json(content),
InputFormat::Text => parse_text_to_json(content),
}
}
fn parse_text_to_json(content: &str) -> Result<Value, Error> {
let lines: Vec<Value> = content
.lines()
.map(|line| Value::String(line.to_string()))
.collect();
Ok(Value::Array(lines))
}
fn parse_multi_document_yaml(content: &str) -> Result<Value, Error> {
let documents: Vec<&str> = content
.split("---")
.map(|doc| doc.trim())
.filter(|doc| !doc.is_empty())
.collect();
let mut parsed_docs = Vec::new();
for doc in documents {
let parsed: Value = serde_yaml::from_str(doc).map_err(Error::Yaml)?;
parsed_docs.push(parsed);
}
Ok(Value::Array(parsed_docs))
}
fn is_likely_csv(content: &str) -> bool {
let lines: Vec<&str> = content.lines().take(5).collect();
if lines.is_empty() {
return false;
}
let first_line = lines[0];
let comma_count = first_line.matches(',').count();
if comma_count > 0 {
lines.iter().skip(1).all(|line| {
let line_comma_count = line.matches(',').count();
(line_comma_count as i32 - comma_count as i32).abs() <= 1
})
} else {
false
}
}
fn parse_csv_to_json(content: &str) -> Result<Value, Error> {
let mut reader = csv::Reader::from_reader(content.as_bytes());
let headers: Vec<String> = reader
.headers()
.map_err(Error::Csv)?
.iter()
.map(|h| h.trim().to_string())
.collect();
let mut records = Vec::new();
for result in reader.records() {
let record = result.map_err(Error::Csv)?;
let mut object = serde_json::Map::new();
for (i, field) in record.iter().enumerate() {
if let Some(header) = headers.get(i) {
let value = infer_value_type(field.trim());
object.insert(header.clone(), value);
}
}
records.push(Value::Object(object));
}
Ok(Value::Array(records))
}
fn infer_value_type(field: &str) -> Value {
if field.is_empty() {
return Value::Null;
}
match field.to_lowercase().as_str() {
"true" => return Value::Bool(true),
"false" => return Value::Bool(false),
_ => {}
}
if let Ok(int_val) = field.parse::<i64>() {
return Value::Number(serde_json::Number::from(int_val));
}
if let Ok(float_val) = field.parse::<f64>() {
if let Some(num) = serde_json::Number::from_f64(float_val) {
return Value::Number(num);
}
}
Value::String(field.to_string())
}
pub fn text_to_json_values(content: &str) -> Result<Vec<Value>, Error> {
let lines: Vec<Value> = content
.lines()
.map(|line| Value::String(line.to_string()))
.collect();
Ok(lines)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_text_parsing() {
let content = "line1\nline2\nERROR: something happened";
let result = parse_text_to_json(content).unwrap();
if let Value::Array(lines) = result {
assert_eq!(lines.len(), 3);
assert_eq!(lines[0], Value::String("line1".to_string()));
assert_eq!(lines[1], Value::String("line2".to_string()));
assert_eq!(lines[2], Value::String("ERROR: something happened".to_string()));
} else {
panic!("Expected array result");
}
}
#[test]
fn test_yaml_detection() {
use super::{is_structured_yaml, is_valid_yaml_line};
assert!(is_structured_yaml("apiVersion: v1\nkind: Pod"));
assert!(is_structured_yaml("key: value\nother: data\nnested:\n sub: item"));
assert!(!is_structured_yaml("2024-01-01 10:00:00 INFO Starting"));
assert!(!is_structured_yaml("plain text\nwith some: colons"));
assert!(!is_structured_yaml("ServerName: localhost\nServerPort: 8080"));
assert!(is_valid_yaml_line("key: value"));
assert!(is_valid_yaml_line(" nested: item"));
assert!(is_valid_yaml_line("- list_item"));
assert!(!is_valid_yaml_line("2024-01-01 10:00:00 INFO message"));
assert!(!is_valid_yaml_line("random text line"));
}
}