use percent_encoding::{utf8_percent_encode, AsciiSet, NON_ALPHANUMERIC};
use serde_json::{Map, Number, Value};
use crate::error::Error;
pub(crate) const STRICT: &AsciiSet = &NON_ALPHANUMERIC
.remove(b'-')
.remove(b'_')
.remove(b'.')
.remove(b'~');
#[derive(Debug, Clone, Default, PartialEq)]
pub struct Query(Map<String, Value>);
impl Query {
pub fn new() -> Self {
Self::default()
}
pub fn set(mut self, key: impl Into<String>, value: impl Into<Value>) -> Self {
self.0.insert(key.into(), value.into());
self
}
pub fn insert(&mut self, key: impl Into<String>, value: impl Into<Value>) -> &mut Self {
self.0.insert(key.into(), value.into());
self
}
pub fn get(&self, key: &str) -> Option<&Value> {
self.0.get(key)
}
pub fn remove(&mut self, key: &str) -> Option<Value> {
self.0.remove(key)
}
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
pub fn as_map(&self) -> &Map<String, Value> {
&self.0
}
pub fn into_map(self) -> Map<String, Value> {
self.0
}
pub fn encode(&self) -> String {
encode(&self.0)
}
pub(crate) fn merge(&mut self, other: Map<String, Value>) {
for (k, v) in other {
self.0.insert(k, v);
}
}
}
impl From<Map<String, Value>> for Query {
fn from(map: Map<String, Value>) -> Self {
Self(map)
}
}
pub trait IntoQuery {
fn into_query(self) -> Result<Map<String, Value>, Error>;
}
impl IntoQuery for Query {
fn into_query(self) -> Result<Map<String, Value>, Error> {
Ok(self.0)
}
}
impl IntoQuery for &Query {
fn into_query(self) -> Result<Map<String, Value>, Error> {
Ok(self.0.clone())
}
}
impl IntoQuery for Map<String, Value> {
fn into_query(self) -> Result<Map<String, Value>, Error> {
Ok(self)
}
}
impl IntoQuery for &Map<String, Value> {
fn into_query(self) -> Result<Map<String, Value>, Error> {
Ok(self.clone())
}
}
impl IntoQuery for Value {
fn into_query(self) -> Result<Map<String, Value>, Error> {
match self {
Value::Object(map) => Ok(map),
Value::Null => Ok(Map::new()),
other => Err(Error::InvalidRequest(format!(
"query must be a JSON object, got {other}"
))),
}
}
}
impl IntoQuery for &Value {
fn into_query(self) -> Result<Map<String, Value>, Error> {
self.clone().into_query()
}
}
pub fn encode(map: &Map<String, Value>) -> String {
let mut parts = Vec::new();
let mut keys: Vec<&String> = map.keys().collect();
keys.sort();
for key in keys {
encode_value(key, &map[key.as_str()], &mut parts);
}
parts.join("&")
}
fn encode_value(prefix: &str, value: &Value, parts: &mut Vec<String>) {
match value {
Value::Object(obj) => {
let mut keys: Vec<&String> = obj.keys().collect();
keys.sort();
for key in keys {
encode_value(&format!("{prefix}[{key}]"), &obj[key.as_str()], parts);
}
}
Value::Array(items) => {
for (i, item) in items.iter().enumerate() {
encode_value(&format!("{prefix}[{i}]"), item, parts);
}
}
Value::Null => parts.push(format!("{}=", escape(prefix))),
Value::Bool(b) => parts.push(format!("{}={b}", escape(prefix))),
Value::Number(n) => parts.push(format!("{}={}", escape(prefix), number(n))),
Value::String(s) => parts.push(format!("{}={}", escape(prefix), escape(s))),
}
}
fn number(n: &Number) -> String {
if n.is_i64() || n.is_u64() {
return n.to_string();
}
match n.as_f64() {
Some(f) if f.is_finite() && f.fract() == 0.0 && f.abs() < 1e21 => format!("{f:.0}"),
_ => n.to_string(),
}
}
fn escape(s: &str) -> String {
utf8_percent_encode(s, STRICT).to_string()
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn whole_floats_drop_the_fraction() {
let q = json!({"a": 50.0, "b": 1.25});
assert_eq!(encode(q.as_object().unwrap()), "a=50&b=1.25");
}
#[test]
fn builder_merges() {
let mut q = Query::new().set("a", 1);
q.merge(json!({"b": "x"}).into_query().unwrap());
assert_eq!(q.encode(), "a=1&b=x");
}
}