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