use serde_json::{Map, Value};
pub fn parse_urlencoded(raw: &[u8]) -> Value {
let mut root = Value::Object(Map::new());
for (key, value) in form_urlencoded::parse(raw) {
insert(&mut root, &key, Value::String(value.into_owned()));
}
root
}
pub(crate) fn insert(target: &mut Value, key: &str, value: Value) {
let segments = parse_segments(key);
if !segments.is_empty() {
insert_segments(target, &segments, value);
}
}
fn insert_segments(target: &mut Value, segments: &[Segment], value: Value) {
match segments {
[] => *target = value,
[Segment::Key(name)] => {
let map = as_object(target);
match map.get_mut(name) {
Some(Value::Array(items)) => items.push(value),
Some(existing) => {
let previous = std::mem::replace(existing, Value::Null);
*existing = Value::Array(vec![previous, value]);
}
None => {
map.insert(name.clone(), value);
}
}
}
[Segment::Append] => as_array(target).push(value),
[Segment::Key(name), rest @ ..] => {
let child = as_object(target).entry(name.clone()).or_insert(Value::Null);
insert_segments(child, rest, value);
}
[Segment::Append, rest @ ..] => {
let items = as_array(target);
items.push(Value::Null);
let last = items.last_mut().expect("just pushed");
insert_segments(last, rest, value);
}
}
}
fn as_object(target: &mut Value) -> &mut Map<String, Value> {
if !target.is_object() {
*target = Value::Object(Map::new());
}
target.as_object_mut().expect("just ensured object")
}
fn as_array(target: &mut Value) -> &mut Vec<Value> {
if !target.is_array() {
*target = Value::Array(Vec::new());
}
target.as_array_mut().expect("just ensured array")
}
#[derive(Debug, Clone, PartialEq)]
enum Segment {
Key(String),
Append,
}
fn parse_segments(key: &str) -> Vec<Segment> {
let Some(open) = key.find('[') else {
return vec![Segment::Key(key.to_string())];
};
let mut segments = vec![Segment::Key(key[..open].to_string())];
let mut rest = &key[open..];
while let Some(stripped) = rest.strip_prefix('[') {
let Some(close) = stripped.find(']') else {
segments.push(Segment::Key(stripped.to_string()));
break;
};
let name = &stripped[..close];
segments.push(if name.is_empty() {
Segment::Append
} else {
Segment::Key(name.to_string())
});
rest = &stripped[close + 1..];
}
segments
}
pub fn lookup<'a>(root: &'a Value, path: &str) -> Option<&'a Value> {
let mut cursor = root;
for segment in path.split('.') {
cursor = match cursor {
Value::Object(map) => map.get(segment)?,
Value::Array(items) => items.get(segment.parse::<usize>().ok()?)?,
_ => return None,
};
}
Some(cursor)
}
pub fn scalar_to_string(value: &Value) -> Option<String> {
match value {
Value::String(s) => Some(s.clone()),
Value::Number(n) => Some(n.to_string()),
Value::Bool(b) => Some(b.to_string()),
Value::Null | Value::Array(_) | Value::Object(_) => None,
}
}
pub fn merge(base: Value, overlay: Value) -> Value {
match (base, overlay) {
(Value::Object(mut base), Value::Object(overlay)) => {
for (key, value) in overlay {
let merged = match base.remove(&key) {
Some(existing) => merge(existing, value),
None => value,
};
base.insert(key, merged);
}
Value::Object(base)
}
(_, overlay) => overlay,
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
fn parse(query: &str) -> Value {
parse_urlencoded(query.as_bytes())
}
#[test]
fn parses_flat_pairs() {
assert_eq!(parse("a=1&b=2"), json!({ "a": "1", "b": "2" }));
}
#[test]
fn values_stay_strings() {
assert_eq!(parse("n=1&t=true"), json!({ "n": "1", "t": "true" }));
}
#[test]
fn percent_and_plus_decoding() {
assert_eq!(parse("q=hello+world"), json!({ "q": "hello world" }));
assert_eq!(parse("q=a%26b"), json!({ "q": "a&b" }));
}
#[test]
fn empty_input_is_an_empty_object() {
assert_eq!(parse(""), json!({}));
}
#[test]
fn bracket_append_builds_an_array() {
assert_eq!(parse("tags[]=x&tags[]=y"), json!({ "tags": ["x", "y"] }));
}
#[test]
fn a_single_bracket_append_is_still_an_array() {
assert_eq!(parse("tags[]=x"), json!({ "tags": ["x"] }));
}
#[test]
fn a_repeated_plain_key_collapses_to_an_array() {
assert_eq!(parse("a=1&a=2&a=3"), json!({ "a": ["1", "2", "3"] }));
}
#[test]
fn bracket_keys_build_nested_objects() {
assert_eq!(parse("user[name]=ada"), json!({ "user": { "name": "ada" } }));
assert_eq!(
parse("user[address][city]=paris"),
json!({ "user": { "address": { "city": "paris" } } })
);
}
#[test]
fn nested_objects_and_scalars_coexist() {
assert_eq!(
parse("user[name]=ada&user[age]=36&active=1"),
json!({ "user": { "name": "ada", "age": "36" }, "active": "1" })
);
}
#[test]
fn an_unbalanced_bracket_keeps_the_value() {
let parsed = parse("a[b=1");
assert!(parsed.get("a").is_some(), "{parsed}");
}
#[test]
fn repeated_appends_accumulate_into_one_array() {
assert_eq!(parse("t[]=a&t[]=b&t[]=c"), json!({ "t": ["a", "b", "c"] }));
assert_eq!(parse("u[tags][]=x&u[tags][]=y"), json!({ "u": { "tags": ["x", "y"] } }));
}
#[test]
fn an_append_of_objects_starts_a_new_element_each_time() {
assert_eq!(
parse("rows[][name]=a&rows[][name]=b"),
json!({ "rows": [{ "name": "a" }, { "name": "b" }] })
);
}
#[test]
fn a_more_specific_later_key_wins_a_shape_conflict() {
assert_eq!(parse("a=1&a[b]=2"), json!({ "a": { "b": "2" } }));
}
#[test]
fn dotted_lookup_walks_objects_and_arrays() {
let value = json!({ "user": { "roles": ["admin", "editor"] } });
assert_eq!(lookup(&value, "user.roles.0"), Some(&json!("admin")));
assert_eq!(lookup(&value, "user.roles.9"), None);
assert_eq!(lookup(&value, "user.missing"), None);
}
#[test]
fn only_scalars_stringify() {
assert_eq!(scalar_to_string(&json!("x")), Some("x".to_string()));
assert_eq!(scalar_to_string(&json!(3)), Some("3".to_string()));
assert_eq!(scalar_to_string(&json!(true)), Some("true".to_string()));
assert_eq!(scalar_to_string(&json!(null)), None);
assert_eq!(scalar_to_string(&json!({ "a": 1 })), None);
assert_eq!(scalar_to_string(&json!([1])), None);
}
#[test]
fn merge_is_recursive_over_objects() {
let merged =
merge(json!({ "a": { "x": 1, "y": 2 }, "b": 1 }), json!({ "a": { "y": 3 }, "c": 4 }));
assert_eq!(merged, json!({ "a": { "x": 1, "y": 3 }, "b": 1, "c": 4 }));
}
}