From 27a10f400b51d1633c701f58d4503de89e06be68 Mon Sep 17 00:00:00 2001 From: ducaale Date: Wed, 29 Dec 2021 11:49:52 +0200 Subject: [PATCH] add support for nested json syntax --- src/json_form.rs | 153 +++++++++++++++++++++++++++++++++++++++++++ src/main.rs | 6 +- src/request_items.rs | 60 +++++++---------- src/to_curl.rs | 4 +- src/utils.rs | 27 ++++++++ tests/cli.rs | 23 +++++++ 6 files changed, 231 insertions(+), 42 deletions(-) create mode 100644 src/json_form.rs diff --git a/src/json_form.rs b/src/json_form.rs new file mode 100644 index 0000000..5b71a42 --- /dev/null +++ b/src/json_form.rs @@ -0,0 +1,153 @@ +use std::mem; + +use serde_json::map::Map; +use serde_json::Value; + +use crate::utils::unescape; + +pub fn parse_path(raw_json_path: &str) -> Vec { + let mut delims: Vec = vec![]; + let mut backslashes = 0; + + for (i, ch) in raw_json_path.chars().enumerate() { + if ch == '\\' { + backslashes += 1; + } else { + if (ch == '[' || ch == ']') && backslashes % 2 == 0 { + delims.push(i); + } + backslashes = 0; + } + } + + if delims.is_empty() { + return vec![raw_json_path.to_string()]; + } + + // Missing preliminary checks + // 1. make sure every opening bracket is followed by a closing bracket + // 2. make sure number of delims is an even number + + let mut json_path = vec![]; + if delims[0] > 0 { + json_path.push(&raw_json_path[0..delims[0]]); + } + for pair in delims.chunks_exact(2) { + json_path.push(&raw_json_path[pair[0] + 1..pair[1]]); + } + + json_path + .iter() + .map(|p| unescape(p, "[]")) + .collect::>() +} + +// TODO: write comments + tests for this function +pub fn set_value>(root: Value, path: &[T], value: Value) -> Value { + debug_assert!(!path.is_empty(), "path should not be empty"); + match root { + Value::Object(mut obj) => { + let value = if path.len() == 1 { + value + } else { + let temp = obj.remove(path[0].as_ref()).unwrap_or(Value::Null); + set_value(temp, &path[1..], value) + }; + obj_append(&mut obj, path[0].as_ref().to_string(), value); + Value::Object(obj) + } + Value::Array(mut arr) => { + let index = if path[0].as_ref() == "" { + Some(arr.len()) + } else { + path[0].as_ref().parse().ok() + }; + if let Some(index) = index { + let value = if path.len() == 1 { + value + } else { + let temp1 = remove_from_arr(&mut arr, index).unwrap_or(Value::Null); + set_value(temp1, &path[1..], value) + }; + arr_append(&mut arr, index, value); + Value::Array(arr) + } else { + set_value(Value::Object(arr_to_obj(arr)), path, value) + } + } + Value::Null => { + if path[0].as_ref().parse::().is_ok() || path[0].as_ref() == "" { + set_value(Value::Array(vec![]), path, value) + } else { + set_value(Value::Object(Map::new()), path, value) + } + } + root => { + let mut obj = Map::new(); + let value = if path.len() == 1 { + value + } else { + let temp1 = obj.remove(path[0].as_ref()).unwrap_or(Value::Null); + set_value(temp1, &path[1..], value) + }; + obj.insert("".to_string(), root); + obj.insert(path[0].as_ref().to_string(), value); + Value::Object(obj) + } + } +} + +/// Insert a value into object without overwriting existing value +fn obj_append(obj: &mut Map, key: String, value: Value) { + let old_value = obj.remove(&key).unwrap_or(Value::Null); + match old_value { + Value::Null => { + obj.insert(key, value); + } + Value::Array(mut arr) => { + arr.push(value); + obj.insert(key, Value::Array(arr)); + } + old_value => { + obj.insert(key, Value::Array(vec![old_value, value])); + } + } +} + +/// Insert into array at any index and without overwriting existing value +fn arr_append(arr: &mut Vec, index: usize, value: Value) { + while index >= arr.len() { + arr.push(Value::Null); + } + let old_value = mem::replace(&mut arr[index], Value::Null); + match old_value { + Value::Null => { + arr[index] = value; + } + Value::Array(mut temp_arr) => { + temp_arr.push(value); + arr[index] = Value::Array(temp_arr); + } + old_value => { + arr[index] = Value::Array(vec![old_value, value]); + } + } +} + +/// Convert array to object by using indices as keys +fn arr_to_obj(mut arr: Vec) -> Map { + let mut obj = Map::new(); + for (i, v) in arr.drain(..).enumerate() { + obj.insert(i.to_string(), v); + } + obj +} + +/// Remove an element from array and replace it with `Value::Null` +fn remove_from_arr(arr: &mut Vec, index: usize) -> Option { + if index < arr.len() { + Some(mem::replace(&mut arr[index], Value::Null)) + } else { + None + } +} diff --git a/src/main.rs b/src/main.rs index 454de85..2b6a196 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,6 +4,7 @@ mod buffer; mod cli; mod download; mod formatting; +mod json_form; mod middleware; mod netrc; mod printer; @@ -343,9 +344,10 @@ fn run(args: Cli) -> Result { Body::Form(body) => request_builder.form(&body), Body::Multipart(body) => request_builder.multipart(body), Body::Json(body) => { - // An empty JSON body would produce "{}" instead of "", so + // TODO: update the comment below + // An empty JSON body would produce {} instead of "", so // this is the one kind of body that needs an is_empty() check - if !body.is_empty() { + if !body.is_null() { request_builder .header(ACCEPT, HeaderValue::from_static(JSON_ACCEPT)) .json(&body) diff --git a/src/request_items.rs b/src/request_items.rs index 219a2e7..3f7fe41 100644 --- a/src/request_items.rs +++ b/src/request_items.rs @@ -11,7 +11,8 @@ use reqwest::header::{HeaderMap, HeaderName, HeaderValue}; use reqwest::{blocking::multipart, Method}; use crate::cli::BodyType; -use crate::utils::expand_tilde; +use crate::json_form; +use crate::utils::{expand_tilde, unescape}; pub const FORM_CONTENT_TYPE: &str = "application/x-www-form-urlencoded"; pub const JSON_CONTENT_TYPE: &str = "application/json"; @@ -40,33 +41,6 @@ impl FromStr for RequestItem { const SPECIAL_CHARS: &str = "=@:;\\"; const SEPS: &[&str] = &["=@", ":=@", "==", ":=", "=", "@", ":"]; - fn unescape(text: &str) -> String { - let mut out = String::new(); - let mut chars = text.chars(); - while let Some(ch) = chars.next() { - if ch == '\\' { - match chars.next() { - Some(next) if SPECIAL_CHARS.contains(next) => { - // Escape this character - out.push(next); - } - Some(next) => { - // Do not escape this character, treat backslash - // as ordinary character - out.push(ch); - out.push(next); - } - None => { - out.push(ch); - } - } - } else { - out.push(ch); - } - } - out - } - fn split(request_item: &str) -> Option<(String, &'static str, String)> { let mut char_inds = request_item.char_indices(); while let Some((ind, ch)) = char_inds.next() { @@ -81,7 +55,11 @@ impl FromStr for RequestItem { for sep in SEPS { if let Some(value) = request_item[ind..].strip_prefix(sep) { let key = &request_item[..ind]; - return Some((unescape(key), sep, unescape(value))); + return Some(( + unescape(key, SPECIAL_CHARS), + sep, + unescape(value, SPECIAL_CHARS), + )); } } } @@ -215,7 +193,7 @@ pub struct RequestItems { } pub enum Body { - Json(serde_json::Map), + Json(serde_json::Value), Form(Vec<(String, String)>), Multipart(multipart::Form), Raw(Vec), @@ -229,7 +207,7 @@ pub enum Body { impl Body { pub fn is_empty(&self) -> bool { match self { - Body::Json(map) => map.is_empty(), + Body::Json(value) => value.is_null(), Body::Form(items) => items.is_empty(), // A multipart form without items isn't empty, and we can't read // a body from stdin because it has to match the header, so we @@ -298,22 +276,28 @@ impl RequestItems { } fn body_as_json(self) -> Result { - let mut body = serde_json::Map::new(); + use serde_json::Value; + + let mut body = Value::Null; for item in self.items { match item { RequestItem::JsonField(key, value) => { - body.insert(key, value); + let json_path = json_form::parse_path(&key); + body = json_form::set_value(body, &json_path, value); } RequestItem::JsonFieldFromFile(key, value) => { - let path = expand_tilde(value); - body.insert(key, serde_json::from_str(&fs::read_to_string(path)?)?); + let value = serde_json::from_str(&fs::read_to_string(expand_tilde(value))?)?; + let json_path = json_form::parse_path(&key); + body = json_form::set_value(body, &json_path, value); } RequestItem::DataField(key, value) => { - body.insert(key, serde_json::Value::String(value)); + let json_path = json_form::parse_path(&key); + body = json_form::set_value(body, &json_path, Value::String(value)); } RequestItem::DataFieldFromFile(key, value) => { - let path = expand_tilde(value); - body.insert(key, serde_json::Value::String(fs::read_to_string(path)?)); + let value = fs::read_to_string(expand_tilde(value))?; + let json_path = json_form::parse_path(&key); + body = json_form::set_value(body, &json_path, Value::String(value)); } RequestItem::FormFile { .. } => unreachable!(), RequestItem::HttpHeader(..) => {} diff --git a/src/to_curl.rs b/src/to_curl.rs index 8fb329d..2640fd6 100644 --- a/src/to_curl.rs +++ b/src/to_curl.rs @@ -361,11 +361,11 @@ pub fn translate(args: Cli) -> Result { cmd.arg(encoded); } } - Body::Json(map) if !map.is_empty() => { + Body::Json(value) if !value.is_null() => { cmd.header("content-type", JSON_CONTENT_TYPE); cmd.header("accept", JSON_ACCEPT); - let json_string = serde_json::Value::from(map).to_string(); + let json_string = value.to_string(); cmd.opt("-d", "--data"); cmd.arg(json_string); } diff --git a/src/utils.rs b/src/utils.rs index 63d0cf1..e506018 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -6,6 +6,33 @@ use anyhow::Result; use reqwest::blocking::Request; use url::{Host, Url}; +pub fn unescape(text: &str, special_chars: &'static str) -> String { + let mut out = String::new(); + let mut chars = text.chars(); + while let Some(ch) = chars.next() { + if ch == '\\' { + match chars.next() { + Some(next) if special_chars.contains(next) => { + // Escape this character + out.push(next); + } + Some(next) => { + // Do not escape this character, treat backslash + // as ordinary character + out.push(ch); + out.push(next); + } + None => { + out.push(ch); + } + } + } else { + out.push(ch); + } + } + out +} + pub fn clone_request(request: &mut Request) -> Result { if let Some(b) = request.body_mut().as_mut() { b.buffer()?; diff --git a/tests/cli.rs b/tests/cli.rs index 667205d..665ed95 100644 --- a/tests/cli.rs +++ b/tests/cli.rs @@ -202,6 +202,29 @@ fn multiline_value() { .success(); } +#[test] +fn nested_json() { + let server = server::http(|req| async move { + assert_eq!( + req.body_as_string().await, + r#"{"object":{"":"scalar","0":"array 1","key":"key key"},"array":[1,2,3],"wow":{"such":{"deep":[null,null,null,{"much":{"power":{"!":"Amaze"}}}]}}}"# + ); + hyper::Response::default() + }); + + get_command() + .args(&["post", &server.base_url()]) + .arg("object=scalar") + .arg("object[0]=array 1") + .arg("object[key]=key key") + .arg("array:=1") + .arg("array:=2") + .arg("array[]:=3") + .arg("wow[such][deep][3][much][power][!]=Amaze") + .assert() + .success(); +} + #[test] fn header() { let server = server::http(|req| async move {