1use percent_encoding::{utf8_percent_encode, AsciiSet, NON_ALPHANUMERIC};
4use serde_json::{Map, Number, Value};
5
6use crate::error::Error;
7
8pub(crate) const STRICT: &AsciiSet = &NON_ALPHANUMERIC
10 .remove(b'-')
11 .remove(b'_')
12 .remove(b'.')
13 .remove(b'~');
14
15#[derive(Debug, Clone, Default, PartialEq)]
28pub struct Query(Map<String, Value>);
29
30impl Query {
31 pub fn new() -> Self {
33 Self::default()
34 }
35
36 pub fn set(mut self, key: impl Into<String>, value: impl Into<Value>) -> Self {
38 self.0.insert(key.into(), value.into());
39 self
40 }
41
42 pub fn insert(&mut self, key: impl Into<String>, value: impl Into<Value>) -> &mut Self {
44 self.0.insert(key.into(), value.into());
45 self
46 }
47
48 pub fn get(&self, key: &str) -> Option<&Value> {
50 self.0.get(key)
51 }
52
53 pub fn remove(&mut self, key: &str) -> Option<Value> {
55 self.0.remove(key)
56 }
57
58 pub fn is_empty(&self) -> bool {
60 self.0.is_empty()
61 }
62
63 pub fn as_map(&self) -> &Map<String, Value> {
65 &self.0
66 }
67
68 pub fn into_map(self) -> Map<String, Value> {
70 self.0
71 }
72
73 pub fn encode(&self) -> String {
75 encode(&self.0)
76 }
77
78 pub(crate) fn merge(&mut self, other: Map<String, Value>) {
79 for (k, v) in other {
80 self.0.insert(k, v);
81 }
82 }
83}
84
85impl From<Map<String, Value>> for Query {
86 fn from(map: Map<String, Value>) -> Self {
87 Self(map)
88 }
89}
90
91pub trait IntoQuery {
97 fn into_query(self) -> Result<Map<String, Value>, Error>;
99}
100
101impl IntoQuery for Query {
102 fn into_query(self) -> Result<Map<String, Value>, Error> {
103 Ok(self.0)
104 }
105}
106
107impl IntoQuery for &Query {
108 fn into_query(self) -> Result<Map<String, Value>, Error> {
109 Ok(self.0.clone())
110 }
111}
112
113impl IntoQuery for Map<String, Value> {
114 fn into_query(self) -> Result<Map<String, Value>, Error> {
115 Ok(self)
116 }
117}
118
119impl IntoQuery for &Map<String, Value> {
120 fn into_query(self) -> Result<Map<String, Value>, Error> {
121 Ok(self.clone())
122 }
123}
124
125impl IntoQuery for Value {
126 fn into_query(self) -> Result<Map<String, Value>, Error> {
127 match self {
128 Value::Object(map) => Ok(map),
129 Value::Null => Ok(Map::new()),
130 other => Err(Error::InvalidRequest(format!(
131 "query must be a JSON object, got {other}"
132 ))),
133 }
134 }
135}
136
137impl IntoQuery for &Value {
138 fn into_query(self) -> Result<Map<String, Value>, Error> {
139 self.clone().into_query()
140 }
141}
142
143pub fn encode(map: &Map<String, Value>) -> String {
145 let mut parts = Vec::new();
146 let mut keys: Vec<&String> = map.keys().collect();
147 keys.sort();
148 for key in keys {
149 encode_value(key, &map[key.as_str()], &mut parts);
150 }
151 parts.join("&")
152}
153
154fn encode_value(prefix: &str, value: &Value, parts: &mut Vec<String>) {
155 match value {
156 Value::Object(obj) => {
157 let mut keys: Vec<&String> = obj.keys().collect();
158 keys.sort();
159 for key in keys {
160 encode_value(&format!("{prefix}[{key}]"), &obj[key.as_str()], parts);
161 }
162 }
163 Value::Array(items) => {
164 for (i, item) in items.iter().enumerate() {
165 encode_value(&format!("{prefix}[{i}]"), item, parts);
166 }
167 }
168 Value::Null => parts.push(format!("{}=", escape(prefix))),
169 Value::Bool(b) => parts.push(format!("{}={b}", escape(prefix))),
170 Value::Number(n) => parts.push(format!("{}={}", escape(prefix), number(n))),
171 Value::String(s) => parts.push(format!("{}={}", escape(prefix), escape(s))),
172 }
173}
174
175fn number(n: &Number) -> String {
177 if n.is_i64() || n.is_u64() {
178 return n.to_string();
179 }
180 match n.as_f64() {
181 Some(f) if f.is_finite() && f.fract() == 0.0 && f.abs() < 1e21 => format!("{f:.0}"),
182 _ => n.to_string(),
183 }
184}
185
186fn escape(s: &str) -> String {
187 utf8_percent_encode(s, STRICT).to_string()
188}
189
190#[cfg(test)]
191mod tests {
192 use super::*;
193 use serde_json::json;
194
195 #[test]
196 fn whole_floats_drop_the_fraction() {
197 let q = json!({"a": 50.0, "b": 1.25});
198 assert_eq!(encode(q.as_object().unwrap()), "a=50&b=1.25");
199 }
200
201 #[test]
202 fn builder_merges() {
203 let mut q = Query::new().set("a", 1);
204 q.merge(json!({"b": "x"}).into_query().unwrap());
205 assert_eq!(q.encode(), "a=1&b=x");
206 }
207}