mirror of
https://github.com/ducaale/xh.git
synced 2025-05-12 18:54: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 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)
|
||||
|
@ -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(..) => {}
|
||||
|
@ -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);
|
||||
}
|
||||
|
27
src/utils.rs
27
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<Request> {
|
||||
if let Some(b) = request.body_mut().as_mut() {
|
||||
b.buffer()?;
|
||||
|
23
tests/cli.rs
23
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 {
|
||||
|
Loading…
x
Reference in New Issue
Block a user