dev_kit/command/json/
json.rs

1use super::{DiffTool, Json};
2use crate::command::http_parser::HttpRequest;
3use crate::command::read_stdin;
4use anyhow::anyhow;
5use itertools::Itertools;
6use jsonpath_rust::JsonPath;
7use lazy_static::lazy_static;
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 let Some(string) = read_stdin() {
159            Ok(Self::from_str(&string)?)
160        } else if let Ok(http_request) = HttpRequest::from_str(value) {
161            Ok(Json::HttpRequest(http_request))
162        } else if let Some(_cmd_path) = CMD_SPLIT_PATTERN.captures(&value)
163            .map(|c| c.extract())
164            .and_then(|(_, [cmd])| which::which(cmd).ok()) {
165            Ok(Json::Cmd(run_cmd(value)?))
166        } else if let Ok(path) = {
167            let path = PathBuf::from_str(value)?;
168            if fs::exists(&path).unwrap_or(false) {
169                Ok(path)
170            } else {
171                Err(anyhow!("Not a valid path: {}", value))
172            }
173        } {
174            Ok(Json::Path(path))
175        } else {
176            Ok(Json::String(value.to_string()))
177        };
178        match result {
179            Ok(json) => {
180                log::debug!("guess str to Json ok, {} => str: {value}", json.name());
181                Ok(json)
182            }
183            Err(err) => {
184                log::debug!("guess str to Json failed, str: {value}, err: {err}");
185                Err(err)
186            }
187        }
188    }
189}
190
191impl TryFrom<&String> for Json {
192    type Error = anyhow::Error;
193
194    fn try_from(value: &String) -> Result<Self, Self::Error> {
195        Self::from_str(value)
196    }
197}
198
199fn run_cmd(value: &str) -> crate::Result<String> {
200    let output = Command::new("sh")
201        .arg("-c")
202        .arg(value)
203        .output()
204        .map_err(|err| anyhow!(r#"
205failed to execute command: {}
206{}
207"#, err, value
208                ))?;
209    if output.status.success() {
210        let stdout = String::from_utf8(output.stdout).map_err(|err|
211            anyhow!("failed to parse output as UTF-8: {}", err)
212        )?;
213        Ok(stdout)
214    } else {
215        let stderr = String::from_utf8_lossy(&output.stderr);
216        Err(anyhow!("run command failed: {}", stderr))
217    }
218}
219
220#[cfg(test)]
221mod tests {
222    use super::*;
223    use std::io::Write;
224    use tempfile::NamedTempFile;
225    use url::Url;
226
227    #[test]
228    fn test_json_from_str_string() {
229        let input = r#"{"a": 1}"#;
230        let json = Json::from_str(input).unwrap();
231        match json {
232            Json::String(s) => assert_eq!(s, input),
233            _ => panic!("Expected Json::String, got {:?}", json),
234        }
235    }
236
237    #[test]
238    fn test_json_from_str_path() {
239        let mut file = NamedTempFile::new().unwrap();
240        let content = r#"{"a": 1}"#;
241        writeln!(file, "{}", content).unwrap();
242        let path_str = file.path().to_str().unwrap();
243
244        let json = Json::from_str(path_str).unwrap();
245        match json {
246            Json::Path(p) => assert_eq!(p, file.path()),
247            _ => panic!("Expected Json::Path, got {:?}", json),
248        }
249    }
250
251    #[test]
252    fn test_json_from_str_url_http() {
253        let input = "http://example.com/api.json";
254        let json = Json::from_str(input).unwrap();
255        match json {
256            Json::HttpRequest(http_request) => {
257                let url = Url::try_from(&http_request).unwrap();
258                assert_eq!(url.as_str(), input)
259            }
260            _ => panic!("Expected Json::Uri, got {:?}", json),
261        }
262    }
263
264    #[test]
265    fn test_json_from_str_cmd() {
266        // Assume 'echo' is available
267        let input = "echo '{\"a\": 1}'";
268        let json = Json::from_str(input).unwrap();
269        match json {
270            Json::Cmd(s) => assert!(s.contains("\"a\": 1")),
271            _ => panic!("Expected Json::Cmd, got {:?}", json),
272        }
273    }
274
275    #[test]
276    fn test_json_beautify() {
277        let input = r#"{"a":1,"b":2}"#;
278        let json = Json::String(input.to_string());
279        let beautified = json.beautify(None).unwrap();
280        assert!(beautified.contains("\n  \"a\": 1,"));
281        assert!(beautified.contains("\n  \"b\": 2"));
282    }
283
284    #[test]
285    fn test_json_query() {
286        let input = r#"{"a":{"b":1},"c":2}"#;
287        let json = Json::String(input.to_string());
288        let result = json.query("$.a.b", false).unwrap();
289        assert_eq!(result, vec!["1"]);
290
291        let result = json.query("$.a", false).unwrap();
292        assert_eq!(result, vec![r#"{"b":1}"#]);
293    }
294
295    #[test]
296    fn test_json_diff_prepare() {
297        let input = r#"{"a":1,"b":2}"#;
298        let json = Json::String(input.to_string());
299
300        // No query
301        let prepared = json.diff_prepare(None).unwrap();
302        assert!(prepared.contains("\"a\": 1"));
303
304        // With query
305        let prepared = json.diff_prepare(Some("$.a")).unwrap();
306        assert_eq!(prepared, "[\n  1\n]");
307    }
308
309    #[test]
310    fn test_run_cmd_success() {
311        let result = run_cmd("echo 'hello'").unwrap();
312        assert_eq!(result.trim(), "hello");
313    }
314
315    #[test]
316    fn test_try_from_json_for_value() {
317        // Test Json::String
318        let json_str = Json::String(r#"{"a": 1}"#.to_string());
319        let value = Arc::<serde_json::Value>::try_from(&json_str).unwrap();
320        assert_eq!(value["a"], 1);
321
322        // Test Json::Path
323        let mut file = NamedTempFile::new().unwrap();
324        writeln!(file, r#"{{"b": 2}}"#).unwrap();
325        let json_path = Json::Path(file.path().to_path_buf());
326        let value = Arc::<serde_json::Value>::try_from(&json_path).unwrap();
327        assert_eq!(value["b"], 2);
328
329        // Test Json::Cmd
330        let json_cmd = Json::Cmd(r#"{"c": 3}"#.to_string());
331        let value = Arc::<serde_json::Value>::try_from(&json_cmd).unwrap();
332        assert_eq!(value["c"], 3);
333    }
334
335    #[test]
336    fn test_json_from_str_invalid() {
337        // This will be treated as Json::String because it's not a valid path, url or cmd
338        let input = "invalid json";
339        let json = Json::from_str(input).unwrap();
340        match json {
341            Json::String(s) => assert_eq!(s, input),
342            _ => panic!("Expected Json::String"),
343        }
344
345        // Test parsing invalid json from Json::String
346        let json_obj = Json::String(input.to_string());
347        let result = Arc::<serde_json::Value>::try_from(&json_obj);
348        assert!(result.is_err());
349    }
350
351    #[test]
352    fn test_run_cmd_error_output() {
353        // command exists but fails
354        let result = run_cmd("ls /non_existent_directory_12345");
355        assert!(result.is_err());
356        assert!(result.unwrap_err().to_string().contains("run command failed"));
357    }
358}