dev_kit/command/json/
json.rs1use 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 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 let prepared = json.diff_prepare(None).unwrap();
321 assert!(prepared.contains("\"a\": 1"));
322
323 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 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 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 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 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 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 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}