cs/parse/
json_parser.rs

1use crate::error::{Result, SearchError};
2use serde_json::Value;
3use std::fs;
4use std::path::{Path, PathBuf};
5
6use super::translation::TranslationEntry;
7
8/// Parser for JSON translation files
9pub struct JsonParser;
10
11impl JsonParser {
12    pub fn parse_file(path: &Path) -> Result<Vec<TranslationEntry>> {
13        let content = fs::read_to_string(path).map_err(|e| {
14            SearchError::json_parse_error(path, format!("Failed to read file: {}", e))
15        })?;
16
17        let root: Value = serde_json::from_str(&content).map_err(|e| {
18            SearchError::json_parse_error(path, format!("Invalid JSON syntax: {}", e))
19        })?;
20
21        let mut entries = Vec::new();
22        Self::flatten_json(&root, String::new(), path, &mut entries);
23
24        Ok(entries)
25    }
26
27    fn flatten_json(
28        value: &Value,
29        prefix: String,
30        file_path: &Path,
31        entries: &mut Vec<TranslationEntry>,
32    ) {
33        match value {
34            Value::Object(map) => {
35                for (key, val) in map {
36                    let new_prefix = if prefix.is_empty() {
37                        key.clone()
38                    } else {
39                        format!("{}.{}", prefix, key)
40                    };
41
42                    Self::flatten_json(val, new_prefix, file_path, entries);
43                }
44            }
45            Value::String(s) => {
46                // Note: JSON doesn't easily give us line numbers with standard serde_json
47                // For now, we'll use 0 as the line number or try to find it if we want to be fancy later
48                // To keep it simple and consistent with the requirement, we'll just store the entry.
49                // If line numbers are critical, we might need a different parser or a second pass.
50                // Given the constraints, 0 is acceptable for MVP JSON support if line numbers are hard.
51                // However, let's try to be better. We can't easily get line numbers from serde_json::Value.
52                // We will accept 0 for now as a limitation of serde_json default deserialization.
53                entries.push(TranslationEntry {
54                    key: prefix,
55                    value: s.clone(),
56                    line: 0, // Placeholder
57                    file: PathBuf::from(file_path),
58                });
59            }
60            Value::Number(n) => {
61                entries.push(TranslationEntry {
62                    key: prefix,
63                    value: n.to_string(),
64                    line: 0,
65                    file: PathBuf::from(file_path),
66                });
67            }
68            Value::Bool(b) => {
69                entries.push(TranslationEntry {
70                    key: prefix,
71                    value: b.to_string(),
72                    line: 0,
73                    file: PathBuf::from(file_path),
74                });
75            }
76            Value::Array(arr) => {
77                for (index, val) in arr.iter().enumerate() {
78                    let new_prefix = if prefix.is_empty() {
79                        index.to_string()
80                    } else {
81                        format!("{}.{}", prefix, index)
82                    };
83                    Self::flatten_json(val, new_prefix, file_path, entries);
84                }
85            }
86            _ => {
87                // Ignore nulls for now
88            }
89        }
90    }
91}
92
93#[cfg(test)]
94mod tests {
95    use super::*;
96    use std::io::Write;
97    use tempfile::NamedTempFile;
98
99    #[test]
100    fn test_parse_simple_json() {
101        let mut file = NamedTempFile::new().unwrap();
102        write!(file, r#"{{"key": "value"}}"#).unwrap();
103
104        let entries = JsonParser::parse_file(file.path()).unwrap();
105        assert_eq!(entries.len(), 1);
106        assert_eq!(entries[0].key, "key");
107        assert_eq!(entries[0].value, "value");
108    }
109
110    #[test]
111    fn test_parse_nested_json() {
112        let mut file = NamedTempFile::new().unwrap();
113        write!(file, r#"{{"parent": {{"child": "value"}}}}"#).unwrap();
114
115        let entries = JsonParser::parse_file(file.path()).unwrap();
116        assert_eq!(entries.len(), 1);
117        assert_eq!(entries[0].key, "parent.child");
118        assert_eq!(entries[0].value, "value");
119    }
120
121    #[test]
122    fn test_parse_json_array() {
123        let mut file = NamedTempFile::new().unwrap();
124        write!(file, r#"{{"list": ["item1", "item2"]}}"#).unwrap();
125
126        let entries = JsonParser::parse_file(file.path()).unwrap();
127        assert_eq!(entries.len(), 2);
128
129        // Check first item
130        let item1 = entries.iter().find(|e| e.value == "item1").unwrap();
131        assert_eq!(item1.key, "list.0");
132
133        // Check second item
134        let item2 = entries.iter().find(|e| e.value == "item2").unwrap();
135        assert_eq!(item2.key, "list.1");
136    }
137}