add support for nested json syntax

This commit is contained in:
ducaale 2021-12-29 11:49:52 +02:00
parent 94fc80e513
commit 27a10f400b
6 changed files with 231 additions and 42 deletions

153
src/json_form.rs Normal file
View File

@ -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<String> {
let mut delims: Vec<usize> = 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::<Vec<_>>()
}
// TODO: write comments + tests for this function
pub fn set_value<T: AsRef<str>>(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::<usize>().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<String, Value>, 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<Value>, 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<Value>) -> Map<String, Value> {
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<Value>, index: usize) -> Option<Value> {
if index < arr.len() {
Some(mem::replace(&mut arr[index], Value::Null))
} else {
None
}
}

View File

@ -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<i32> {
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)

View File

@ -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<String, serde_json::Value>),
Json(serde_json::Value),
Form(Vec<(String, String)>),
Multipart(multipart::Form),
Raw(Vec<u8>),
@ -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<Body> {
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(..) => {}

View File

@ -361,11 +361,11 @@ pub fn translate(args: Cli) -> Result<Command> {
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);
}

View File

@ -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<Request> {
if let Some(b) = request.body_mut().as_mut() {
b.buffer()?;

View File

@ -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 {