mirror of
https://github.com/ducaale/xh.git
synced 2025-05-13 03:04:27 +00:00
add support for nested json syntax
This commit is contained in:
parent
94fc80e513
commit
27a10f400b
153
src/json_form.rs
Normal file
153
src/json_form.rs
Normal 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
|
||||||
|
}
|
||||||
|
}
|
@ -4,6 +4,7 @@ mod buffer;
|
|||||||
mod cli;
|
mod cli;
|
||||||
mod download;
|
mod download;
|
||||||
mod formatting;
|
mod formatting;
|
||||||
|
mod json_form;
|
||||||
mod middleware;
|
mod middleware;
|
||||||
mod netrc;
|
mod netrc;
|
||||||
mod printer;
|
mod printer;
|
||||||
@ -343,9 +344,10 @@ fn run(args: Cli) -> Result<i32> {
|
|||||||
Body::Form(body) => request_builder.form(&body),
|
Body::Form(body) => request_builder.form(&body),
|
||||||
Body::Multipart(body) => request_builder.multipart(body),
|
Body::Multipart(body) => request_builder.multipart(body),
|
||||||
Body::Json(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
|
// this is the one kind of body that needs an is_empty() check
|
||||||
if !body.is_empty() {
|
if !body.is_null() {
|
||||||
request_builder
|
request_builder
|
||||||
.header(ACCEPT, HeaderValue::from_static(JSON_ACCEPT))
|
.header(ACCEPT, HeaderValue::from_static(JSON_ACCEPT))
|
||||||
.json(&body)
|
.json(&body)
|
||||||
|
@ -11,7 +11,8 @@ use reqwest::header::{HeaderMap, HeaderName, HeaderValue};
|
|||||||
use reqwest::{blocking::multipart, Method};
|
use reqwest::{blocking::multipart, Method};
|
||||||
|
|
||||||
use crate::cli::BodyType;
|
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 FORM_CONTENT_TYPE: &str = "application/x-www-form-urlencoded";
|
||||||
pub const JSON_CONTENT_TYPE: &str = "application/json";
|
pub const JSON_CONTENT_TYPE: &str = "application/json";
|
||||||
@ -40,33 +41,6 @@ impl FromStr for RequestItem {
|
|||||||
const SPECIAL_CHARS: &str = "=@:;\\";
|
const SPECIAL_CHARS: &str = "=@:;\\";
|
||||||
const SEPS: &[&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)> {
|
fn split(request_item: &str) -> Option<(String, &'static str, String)> {
|
||||||
let mut char_inds = request_item.char_indices();
|
let mut char_inds = request_item.char_indices();
|
||||||
while let Some((ind, ch)) = char_inds.next() {
|
while let Some((ind, ch)) = char_inds.next() {
|
||||||
@ -81,7 +55,11 @@ impl FromStr for RequestItem {
|
|||||||
for sep in SEPS {
|
for sep in SEPS {
|
||||||
if let Some(value) = request_item[ind..].strip_prefix(sep) {
|
if let Some(value) = request_item[ind..].strip_prefix(sep) {
|
||||||
let key = &request_item[..ind];
|
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 {
|
pub enum Body {
|
||||||
Json(serde_json::Map<String, serde_json::Value>),
|
Json(serde_json::Value),
|
||||||
Form(Vec<(String, String)>),
|
Form(Vec<(String, String)>),
|
||||||
Multipart(multipart::Form),
|
Multipart(multipart::Form),
|
||||||
Raw(Vec<u8>),
|
Raw(Vec<u8>),
|
||||||
@ -229,7 +207,7 @@ pub enum Body {
|
|||||||
impl Body {
|
impl Body {
|
||||||
pub fn is_empty(&self) -> bool {
|
pub fn is_empty(&self) -> bool {
|
||||||
match self {
|
match self {
|
||||||
Body::Json(map) => map.is_empty(),
|
Body::Json(value) => value.is_null(),
|
||||||
Body::Form(items) => items.is_empty(),
|
Body::Form(items) => items.is_empty(),
|
||||||
// A multipart form without items isn't empty, and we can't read
|
// 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
|
// 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> {
|
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 {
|
for item in self.items {
|
||||||
match item {
|
match item {
|
||||||
RequestItem::JsonField(key, value) => {
|
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) => {
|
RequestItem::JsonFieldFromFile(key, value) => {
|
||||||
let path = expand_tilde(value);
|
let value = serde_json::from_str(&fs::read_to_string(expand_tilde(value))?)?;
|
||||||
body.insert(key, serde_json::from_str(&fs::read_to_string(path)?)?);
|
let json_path = json_form::parse_path(&key);
|
||||||
|
body = json_form::set_value(body, &json_path, value);
|
||||||
}
|
}
|
||||||
RequestItem::DataField(key, 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) => {
|
RequestItem::DataFieldFromFile(key, value) => {
|
||||||
let path = expand_tilde(value);
|
let value = fs::read_to_string(expand_tilde(value))?;
|
||||||
body.insert(key, serde_json::Value::String(fs::read_to_string(path)?));
|
let json_path = json_form::parse_path(&key);
|
||||||
|
body = json_form::set_value(body, &json_path, Value::String(value));
|
||||||
}
|
}
|
||||||
RequestItem::FormFile { .. } => unreachable!(),
|
RequestItem::FormFile { .. } => unreachable!(),
|
||||||
RequestItem::HttpHeader(..) => {}
|
RequestItem::HttpHeader(..) => {}
|
||||||
|
@ -361,11 +361,11 @@ pub fn translate(args: Cli) -> Result<Command> {
|
|||||||
cmd.arg(encoded);
|
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("content-type", JSON_CONTENT_TYPE);
|
||||||
cmd.header("accept", JSON_ACCEPT);
|
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.opt("-d", "--data");
|
||||||
cmd.arg(json_string);
|
cmd.arg(json_string);
|
||||||
}
|
}
|
||||||
|
27
src/utils.rs
27
src/utils.rs
@ -6,6 +6,33 @@ use anyhow::Result;
|
|||||||
use reqwest::blocking::Request;
|
use reqwest::blocking::Request;
|
||||||
use url::{Host, Url};
|
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> {
|
pub fn clone_request(request: &mut Request) -> Result<Request> {
|
||||||
if let Some(b) = request.body_mut().as_mut() {
|
if let Some(b) = request.body_mut().as_mut() {
|
||||||
b.buffer()?;
|
b.buffer()?;
|
||||||
|
23
tests/cli.rs
23
tests/cli.rs
@ -202,6 +202,29 @@ fn multiline_value() {
|
|||||||
.success();
|
.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]
|
#[test]
|
||||||
fn header() {
|
fn header() {
|
||||||
let server = server::http(|req| async move {
|
let server = server::http(|req| async move {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user