hawk_data/
setup.rs

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    // debug
34    // debug_json_order(&json);
35    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    // CSV判定を最初に行う(シンプルな形式から)
50    if is_likely_csv(trimmed) {
51        return InputFormat::Csv;
52    }
53
54    // JSON判定(厳密にチェック)- YAMLより先に判定
55    if (trimmed.starts_with('{') && trimmed.ends_with('}'))
56        || (trimmed.starts_with('[') && trimmed.ends_with(']'))
57    {
58        // さらに、全体がJSONとして有効かチェック
59        if serde_json::from_str::<serde_json::Value>(trimmed).is_ok() {
60            return InputFormat::Json;
61        }
62    }
63
64    // YAML判定 - より厳格な条件に変更
65    if is_structured_yaml(trimmed) {
66        return InputFormat::Yaml;
67    }
68
69    // 上記のいずれにも該当しない場合はText
70    InputFormat::Text
71}
72
73// 構造化されたYAMLかどうかを厳格に判定
74fn 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    // Kubernetes/Docker Compose等の明確なYAMLマーカー
82    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        // 空行やコメントは除外
94        if trimmed.is_empty() || trimmed.starts_with('#') {
95            continue;
96        }
97        
98        total_meaningful_lines += 1;
99
100        // YAML構造の特徴を検出
101        if is_valid_yaml_line(trimmed) {
102            yaml_indicators += 1;
103        }
104    }
105
106    // 意味のある行が少ない場合はYAMLではない
107    if total_meaningful_lines < 3 {
108        return false;
109    }
110
111    // 80%以上の行がYAML構造ならYAMLと判定
112    (yaml_indicators as f64 / total_meaningful_lines as f64) > 0.8
113}
114
115// 有効なYAML行かどうかを判定
116fn is_valid_yaml_line(line: &str) -> bool {
117    // リスト形式 (- item)
118    if line.starts_with("- ") {
119        return true;
120    }
121
122    // key: value 形式
123    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        // キー部分の検証
128        if key_part.is_empty() {
129            return false;
130        }
131
132        // キーに無効な文字が含まれていない
133        if key_part.contains(' ') && !key_part.starts_with('"') && !key_part.starts_with('\'') {
134            return false;
135        }
136
137        // インデントされたネスト構造
138        if line.starts_with("  ") || line.starts_with("\t") {
139            return true;
140        }
141
142        // 値が明らかにYAML的
143        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        // パス、URL、タイムスタンプなどが含まれていたらYAMLではない可能性が高い
153        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            // 複数ドキュメントに対応
168            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    // テキストを行ごとに分割して配列として扱う
181    let lines: Vec<Value> = content
182        .lines()
183        .map(|line| Value::String(line.to_string()))
184        .collect();
185    
186    // 空のファイルの場合も配列として返す
187    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    // 複数ドキュメントを配列として返す
205    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    // 最初の行をヘッダーとして想定
216    let first_line = lines[0];
217    let comma_count = first_line.matches(',').count();
218
219    // カンマが1個以上あり、他の行も同じような構造
220    if comma_count > 0 {
221        // 他の行も同じようなカンマ数か確認
222        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    // ヘッダーを取得
235    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    // 直接配列を返す(二重配列にしない)
259    Ok(Value::Array(records))
260}
261
262fn infer_value_type(field: &str) -> Value {
263    // 空文字チェック
264    if field.is_empty() {
265        return Value::Null;
266    }
267
268    // 真偽値判定
269    match field.to_lowercase().as_str() {
270        "true" => return Value::Bool(true),
271        "false" => return Value::Bool(false),
272        _ => {}
273    }
274
275    // 整数判定
276    if let Ok(int_val) = field.parse::<i64>() {
277        return Value::Number(serde_json::Number::from(int_val));
278    }
279
280    // 浮動小数点数判定
281    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    // デフォルトは文字列
288    Value::String(field.to_string())
289}
290
291// テキスト処理用のヘルパー関数
292pub 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        // 明確にYAMLとして認識されるべき
324        assert!(is_structured_yaml("apiVersion: v1\nkind: Pod"));
325        assert!(is_structured_yaml("key: value\nother: data\nnested:\n  sub: item"));
326        
327        // YAMLとして認識されないべき
328        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")); // 設定ファイル風だがYAMLではない
331        
332        // 個別行のテスト
333        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}