dev_kit/command/json/
json.rs

1use super::{DiffTool, Json};
2use crate::command::http_parser::HttpRequest;
3use anyhow::anyhow;
4use itertools::Itertools;
5use jsonpath_rust::JsonPath;
6use lazy_static::lazy_static;
7use std::io::Read;
8use std::path::PathBuf;
9use std::process::Command;
10use std::str::FromStr;
11use std::sync::Arc;
12use std::{env, fs};
13
14impl Json {
15    pub fn keys(&self, query: Option<&str>) -> crate::Result<Vec<String>> {
16        let json = Arc::<serde_json::Value>::try_from(self)?;
17        let result = if let Some(query) = query {
18            json.query(query)?
19        } else {
20            vec![&*json]
21        };
22        let keys = result.iter().flat_map(|it| match it {
23            serde_json::Value::Object(map) => map.keys().map(|k| k.to_string()).collect_vec(),
24            serde_json::Value::Array(_) => vec!["*".to_string()],
25            _ => vec![]
26        }).unique_by(|it| it.clone()).collect_vec();
27        Ok(keys)
28    }
29    pub fn beautify(&self, query: Option<&str>) -> crate::Result<String> {
30        let json = Arc::<serde_json::Value>::try_from(self)?;
31        let result = if let Some(query) = query {
32            let json = json.query(query)?;
33            serde_json::to_string_pretty(&json)
34        } else {
35            serde_json::to_string_pretty(&*json)
36        };
37        Ok(result.map_err(|err| {
38            log::debug!("{}",err);
39            anyhow!("Invalid json format")
40        })?)
41    }
42
43    pub fn query(&self, query: &str, beauty: bool) -> crate::Result<Vec<String>> {
44        let json = Arc::<serde_json::Value>::try_from(self)?;
45        let query_result = json.query(query).map_err(|err| {
46            log::debug!("{}",err);
47            anyhow!("Invalid json path: {query}")
48        })?;
49        let arr = query_result.iter().flat_map(|&it| {
50            if beauty {
51                serde_json::to_string_pretty(&it)
52            } else {
53                serde_json::to_string(&it)
54            }.map_err(|err| {
55                log::debug!("{}",err);
56                anyhow!("Invalid json format")
57            })
58        }).collect_vec();
59        Ok(arr)
60    }
61
62    pub fn diff(&self, other: &Self, query: Option<&str>, diff_tool: Option<DiffTool>) -> crate::Result<()> {
63        let tmp_dir = env::temp_dir().join("jsondiff").join(chrono::Local::now().format("%Y%m%d%H%M%S%f").to_string());
64        if tmp_dir.exists() {
65            fs::remove_dir_all(&tmp_dir)?;
66        }
67        let left = self;
68        let right = other;
69        let _ = fs::create_dir_all(&tmp_dir)?;
70        let left = left.diff_prepare(query.as_deref())?;
71        let left_path = tmp_dir.join("left.json");
72        fs::write(&left_path, left)?;
73        println!("write left to file {}", left_path.display());
74        let right = right.diff_prepare(query.as_deref())?;
75        let right_path = tmp_dir.join("right.json");
76        fs::write(&right_path, right)?;
77        println!("write right to file {}", right_path.display());
78        let diff_tool = diff_tool.unwrap_or_default();
79        if diff_tool.is_available() {
80            println!("diff with {}", diff_tool);
81            diff_tool.diff(&left_path, &right_path)?;
82        } else {
83            eprintln!("diff tool {} is not installed", diff_tool);
84            println!(
85                r#"
86install {} command-line interface, see:
87{}"#,
88                diff_tool, diff_tool.how_to_install()
89            )
90        }
91        Ok(())
92    }
93}
94
95impl Json {
96    fn diff_prepare(&self, query: Option<&str>) -> crate::Result<String> {
97        let json = Arc::<serde_json::Value>::try_from(self)?;
98        if let Some(query) = query {
99            let array = json.query(query)?;
100            let pretty = serde_json::to_string_pretty(&array)?;
101            Ok(pretty)
102        } else {
103            Ok(serde_json::to_string_pretty(&*json)?)
104        }
105    }
106}
107
108lazy_static! {
109    static ref ASYNC_RT: tokio::runtime::Runtime = {
110         tokio::runtime::Builder::new_multi_thread()
111                        .worker_threads(1usize)
112                        .enable_all()
113        .build().unwrap()
114    };
115}
116
117impl TryFrom<&Json> for Arc<serde_json::Value> {
118    type Error = anyhow::Error;
119
120    fn try_from(input: &Json) -> Result<Self, Self::Error> {
121        let json = match input {
122            Json::Cmd(input) | Json::String(input) => {
123                let json = serde_json::from_str::<serde_json::Value>(&input).map_err(|err| {
124                    log::debug!("{}",err);
125                    anyhow!("Invalid json format")
126                })?;
127                Arc::new(json)
128            }
129            Json::Path(path) => {
130                let file = fs::File::open(&path).map_err(|err| anyhow!("open file {} failed, {}", path.display(), err))?;
131                let json = serde_json::from_reader::<_, serde_json::Value>(file).map_err(|err| {
132                    log::debug!("{}",err);
133                    anyhow!("Invalid json format")
134                })?;
135                Arc::new(json)
136            }
137            Json::HttpRequest(http_request) => {
138                Arc::new(http_request.try_into()?)
139            }
140            Json::JsonValue(val) => Arc::clone(val)
141        };
142        Ok(json)
143    }
144}
145
146lazy_static! {
147    static ref CMD_SPLIT_PATTERN: regex::Regex = {
148        regex::RegexBuilder::new(r"^([\w\d]+).*")
149        .multi_line(true)
150        .case_insensitive(true)
151        .build().unwrap()
152    };
153}
154impl FromStr for Json {
155    type Err = anyhow::Error;
156
157    fn from_str(value: &str) -> Result<Self, Self::Err> {
158        let result = if value.eq("-") {
159            let mut string = String::new();
160            let _ = std::io::stdin().lock().read_to_string(&mut string)
161                .map_err(|err| anyhow!("read from stdin failed, {}", err))?;
162            match string.trim() {
163                "-" => Err(anyhow!("Not a valid input")),
164                _ => Ok(Self::from_str(&string)?)
165            }
166        } else if let Ok(http_request) = HttpRequest::from_str(value) {
167            Ok(Json::HttpRequest(http_request))
168        } else if let Some(_cmd_path) = CMD_SPLIT_PATTERN.captures(&value)
169            .map(|c| c.extract())
170            .and_then(|(_, [cmd])| which::which(cmd).ok()) {
171            Ok(Json::Cmd(run_cmd(value)?))
172        } else if let Ok(path) = {
173            let path = PathBuf::from_str(value)?;
174            if fs::exists(&path).unwrap_or(false) {
175                Ok(path)
176            } else {
177                Err(anyhow!("Not a valid path: {}", value))
178            }
179        } {
180            Ok(Json::Path(path))
181        } else {
182            Ok(Json::String(value.to_string()))
183        };
184        match result {
185            Ok(json) => {
186                log::debug!("guess str to Json ok, {} => str: {value}", json.name());
187                Ok(json)
188            }
189            Err(err) => {
190                log::debug!("guess str to Json failed, str: {value}, err: {err}");
191                Err(err)
192            }
193        }
194    }
195}
196
197impl TryFrom<&String> for Json {
198    type Error = anyhow::Error;
199
200    fn try_from(value: &String) -> Result<Self, Self::Error> {
201        Self::from_str(value)
202    }
203}
204
205fn run_cmd(value: &str) -> crate::Result<String> {
206    let output = Command::new("sh")
207        .arg("-c")
208        .arg(value)
209        .output()
210        .map_err(|err| anyhow!(r#"
211failed to execute command: {}
212{}
213"#, err, value
214                ))?;
215    if output.status.success() {
216        let stdout = String::from_utf8(output.stdout).map_err(|err|
217            anyhow!("failed to parse output as UTF-8: {}", err)
218        )?;
219        Ok(stdout)
220    } else {
221        let stderr = String::from_utf8_lossy(&output.stderr);
222        Err(anyhow!("run command failed: {}", stderr))
223    }
224}
225
226#[cfg(test)]
227mod tests {
228    use super::*;
229    use std::io::Write;
230    use tempfile::NamedTempFile;
231    use url::Url;
232
233    #[test]
234    fn test_json_from_str_string() {
235        let input = r#"{"a": 1}"#;
236        let json = Json::from_str(input).unwrap();
237        match json {
238            Json::String(s) => assert_eq!(s, input),
239            _ => panic!("Expected Json::String, got {:?}", json),
240        }
241    }
242
243    #[test]
244    fn test_json_from_str_path() {
245        let mut file = NamedTempFile::new().unwrap();
246        let content = r#"{"a": 1}"#;
247        writeln!(file, "{}", content).unwrap();
248        let path_str = file.path().to_str().unwrap();
249
250        let json = Json::from_str(path_str).unwrap();
251        match json {
252            Json::Path(p) => assert_eq!(p, file.path()),
253            _ => panic!("Expected Json::Path, got {:?}", json),
254        }
255    }
256
257    #[test]
258    fn test_json_from_str_url_http() {
259        let input = "http://example.com/api.json";
260        let json = Json::from_str(input).unwrap();
261        match json {
262            Json::HttpRequest(http_request) => {
263                let url = Url::try_from(&http_request).unwrap();
264                assert_eq!(url.as_str(), input)
265            }
266            _ => panic!("Expected Json::Uri, got {:?}", json),
267        }
268    }
269
270    #[test]
271    fn test_json_from_str_cmd() {
272        // Assume 'echo' is available
273        let input = "echo '{\"a\": 1}'";
274        let json = Json::from_str(input).unwrap();
275        match json {
276            Json::Cmd(s) => assert!(s.contains("\"a\": 1")),
277            _ => panic!("Expected Json::Cmd, got {:?}", json),
278        }
279    }
280
281    #[test]
282    fn test_json_beautify() {
283        let input = r#"{"a":1,"b":2}"#;
284        let json = Json::String(input.to_string());
285        let beautified = json.beautify(None).unwrap();
286        assert!(beautified.contains("\n  \"a\": 1,"));
287        assert!(beautified.contains("\n  \"b\": 2"));
288    }
289
290    #[test]
291    fn test_json_query() {
292        let input = r#"{"a":{"b":1},"c":2}"#;
293        let json = Json::String(input.to_string());
294        let result = json.query("$.a.b", false).unwrap();
295        assert_eq!(result, vec!["1"]);
296
297        let result = json.query("$.a", false).unwrap();
298        assert_eq!(result, vec![r#"{"b":1}"#]);
299    }
300
301    #[test]
302    fn test_json_diff_prepare() {
303        let input = r#"{"a":1,"b":2}"#;
304        let json = Json::String(input.to_string());
305
306        // No query
307        let prepared = json.diff_prepare(None).unwrap();
308        assert!(prepared.contains("\"a\": 1"));
309
310        // With query
311        let prepared = json.diff_prepare(Some("$.a")).unwrap();
312        assert_eq!(prepared, "[\n  1\n]");
313    }
314
315    #[test]
316    fn test_run_cmd_success() {
317        let result = run_cmd("echo 'hello'").unwrap();
318        assert_eq!(result.trim(), "hello");
319    }
320
321    #[test]
322    fn test_try_from_json_for_value() {
323        // Test Json::String
324        let json_str = Json::String(r#"{"a": 1}"#.to_string());
325        let value = Arc::<serde_json::Value>::try_from(&json_str).unwrap();
326        assert_eq!(value["a"], 1);
327
328        // Test Json::Path
329        let mut file = NamedTempFile::new().unwrap();
330        writeln!(file, r#"{{"b": 2}}"#).unwrap();
331        let json_path = Json::Path(file.path().to_path_buf());
332        let value = Arc::<serde_json::Value>::try_from(&json_path).unwrap();
333        assert_eq!(value["b"], 2);
334
335        // Test Json::Cmd
336        let json_cmd = Json::Cmd(r#"{"c": 3}"#.to_string());
337        let value = Arc::<serde_json::Value>::try_from(&json_cmd).unwrap();
338        assert_eq!(value["c"], 3);
339    }
340
341    #[test]
342    fn test_json_from_str_invalid() {
343        // This will be treated as Json::String because it's not a valid path, url or cmd
344        let input = "invalid json";
345        let json = Json::from_str(input).unwrap();
346        match json {
347            Json::String(s) => assert_eq!(s, input),
348            _ => panic!("Expected Json::String"),
349        }
350
351        // Test parsing invalid json from Json::String
352        let json_obj = Json::String(input.to_string());
353        let result = Arc::<serde_json::Value>::try_from(&json_obj);
354        assert!(result.is_err());
355    }
356
357    #[test]
358    fn test_run_cmd_error_output() {
359        // command exists but fails
360        let result = run_cmd("ls /non_existent_directory_12345");
361        assert!(result.is_err());
362        assert!(result.unwrap_err().to_string().contains("run command failed"));
363    }
364}