Skip to main content

parse_rust_server/
params.rs

1//! Query parameters, from wherever they arrived.
2//!
3//! `ClassesRouter` merges `req.body` with the decoded query string before reading either
4//! (`ClassesRouter.js:23`, `:49`), so a `where` sent in a POST body reaches the same code as one
5//! sent in the URL. A `/batch` sub-request has no URL to carry them at all, so its parameters are
6//! its body. One type for both, built at each entry point, so the readers below cannot know or
7//! care which happened.
8//!
9//! Values are held as strings, which is the query-string form. A caller that starts from JSON
10//! re-encodes objects and arrays as JSON text, which is what a real query string carries.
11
12use std::collections::HashMap;
13
14use parse_rust_core::ParseError;
15use parse_rust_rest::{parse_include, parse_where, FindOptions, ParsedWhere};
16use parse_rust_storage::{QueryOptions, DEFAULT_LIMIT};
17use serde_json::Value as Json;
18
19/// Parameters for one request.
20#[derive(Debug, Clone, Default)]
21pub struct Params(HashMap<String, String>);
22
23/// `allowConstraints` (`ClassesRouter.js:177-194`). Anything else is `INVALID_QUERY`.
24const FIND_KEYS: [&str; 16] = [
25    "skip",
26    "limit",
27    "order",
28    "count",
29    "keys",
30    "excludeKeys",
31    "include",
32    "includeAll",
33    "redirectClassNameForKey",
34    "where",
35    "readPreference",
36    "includeReadPreference",
37    "subqueryReadPreference",
38    "hint",
39    "explain",
40    "comment",
41];
42
43/// `ALLOWED_GET_QUERY_KEYS` (`ClassesRouter.js:8-15`).
44const GET_KEYS: [&str; 6] = [
45    "keys",
46    "include",
47    "excludeKeys",
48    "readPreference",
49    "includeReadPreference",
50    "subqueryReadPreference",
51];
52
53/// Parameters upstream accepts and this server does not implement.
54///
55/// Refused rather than ignored, on the rule that an unsupported query constraint is an error.
56/// Each one changes what comes back, so accepting it silently would answer a different question
57/// than the client asked. `readPreference` and its two siblings are deliberately absent from this
58/// list: they select a replica and do not change the result.
59const UNIMPLEMENTED_KEYS: [&str; 5] = [
60    "includeAll",
61    "redirectClassNameForKey",
62    "hint",
63    "explain",
64    "comment",
65];
66
67impl Params {
68    pub fn from_map(map: HashMap<String, String>) -> Self {
69        Self(map)
70    }
71
72    /// Build from a JSON object, which is how a `/batch` sub-request carries its parameters.
73    ///
74    /// Non-string values are re-encoded as JSON text, matching the query-string form. This is the
75    /// inverse of upstream's `JSONFromQuery` (`ClassesRouter.js:142-152`), which parses each query
76    /// value as JSON and falls back to the raw string.
77    pub fn from_json(value: Option<&Json>) -> Self {
78        let mut map = HashMap::new();
79        if let Some(Json::Object(object)) = value {
80            for (key, value) in object {
81                let text = match value {
82                    Json::String(s) => s.clone(),
83                    other => other.to_string(),
84                };
85                map.insert(key.clone(), text);
86            }
87        }
88        Self(map)
89    }
90
91    pub fn get(&self, key: &str) -> Option<&str> {
92        self.0.get(key).map(String::as_str)
93    }
94
95    /// `optionsFromBody`'s key check (`ClassesRouter.js:196-200`).
96    pub fn reject_unknown_find_keys(&self) -> Result<(), ParseError> {
97        for key in self.0.keys() {
98            if !FIND_KEYS.contains(&key.as_str()) {
99                return Err(ParseError::invalid_query(format!(
100                    "Invalid parameter for query: {key}"
101                )));
102            }
103        }
104        self.reject_unimplemented()
105    }
106
107    /// `handleGet`'s key check (`ClassesRouter.js:52-56`). Note the message says nothing about
108    /// which key, which is upstream's.
109    pub fn reject_unknown_get_keys(&self) -> Result<(), ParseError> {
110        for key in self.0.keys() {
111            if !GET_KEYS.contains(&key.as_str()) {
112                return Err(ParseError::invalid_query("Improper encode of parameter"));
113            }
114        }
115        self.reject_unimplemented()
116    }
117
118    fn reject_unimplemented(&self) -> Result<(), ParseError> {
119        for key in UNIMPLEMENTED_KEYS {
120            if self.0.contains_key(key) {
121                return Err(ParseError::new(
122                    parse_rust_core::ErrorCode::CommandUnavailable,
123                    format!("The {key} query parameter is not supported yet."),
124                ));
125            }
126        }
127        Ok(())
128    }
129
130    /// The `where` document, decoded.
131    ///
132    /// `decodeWhere` (`ClassesRouter.js:165-174`) reports `where parameter is not valid JSON` for
133    /// a string that does not parse, and that is the message a client sees.
134    pub fn parse_where(&self) -> Result<ParsedWhere, ParseError> {
135        let Some(raw) = self.get("where") else {
136            return Ok(ParsedWhere::default());
137        };
138        let value: Json = serde_json::from_str(raw)
139            .map_err(|_| ParseError::invalid_json("where parameter is not valid JSON"))?;
140        parse_where(&value)
141    }
142
143    pub fn wants_count(&self) -> bool {
144        // `if (body.count)` is a truthiness test, so `count=0` and `count=false` are both off.
145        !matches!(
146            self.get("count"),
147            None | Some("0") | Some("false") | Some("")
148        )
149    }
150
151    /// Everything a find carries besides the constraints.
152    pub fn find_options(&self) -> Result<FindOptions, ParseError> {
153        Ok(FindOptions {
154            // An absent or unparsable `limit` falls back to Parse's default of 100 rather than to
155            // "no limit". `limit=0` is a legitimate request for zero rows, usually paired with
156            // `count=1`, and must not be read as unlimited either.
157            limit: Some(
158                self.get("limit")
159                    .and_then(|v| v.parse::<u32>().ok())
160                    .unwrap_or(DEFAULT_LIMIT),
161            ),
162            skip: self.get("skip").and_then(|v| v.parse().ok()),
163            order: self
164                .get("order")
165                .map(QueryOptions::parse_order)
166                .unwrap_or_default(),
167            keys: self.csv("keys"),
168            exclude_keys: self.csv("excludeKeys"),
169            include: match self.get("include") {
170                Some(raw) => parse_include(raw)?,
171                None => Vec::new(),
172            },
173        })
174    }
175
176    /// The subset of the above a `get` may carry.
177    pub fn get_options(&self) -> Result<FindOptions, ParseError> {
178        Ok(FindOptions {
179            limit: Some(1),
180            skip: None,
181            order: Vec::new(),
182            keys: self.csv("keys"),
183            exclude_keys: self.csv("excludeKeys"),
184            include: match self.get("include") {
185                Some(raw) => parse_include(raw)?,
186                None => Vec::new(),
187            },
188        })
189    }
190
191    fn csv(&self, key: &str) -> Option<Vec<String>> {
192        self.get(key).map(|raw| {
193            raw.split(',')
194                .map(str::trim)
195                .filter(|s| !s.is_empty())
196                .map(str::to_string)
197                .collect()
198        })
199    }
200}
201
202#[cfg(test)]
203mod tests {
204    use super::*;
205
206    fn params(pairs: &[(&str, &str)]) -> Params {
207        Params::from_map(
208            pairs
209                .iter()
210                .map(|(k, v)| ((*k).to_string(), (*v).to_string()))
211                .collect(),
212        )
213    }
214
215    #[test]
216    fn an_unknown_find_parameter_is_named_in_the_error() {
217        let e = params(&[("nonsense", "1")])
218            .reject_unknown_find_keys()
219            .unwrap_err();
220        assert_eq!(e.code, parse_rust_core::ErrorCode::InvalidQuery);
221        assert_eq!(e.message, "Invalid parameter for query: nonsense");
222    }
223
224    #[test]
225    fn an_unknown_get_parameter_is_not_named() {
226        // Upstream's message carries no key. Two similar checks, two different strings.
227        let e = params(&[("limit", "1")])
228            .reject_unknown_get_keys()
229            .unwrap_err();
230        assert_eq!(e.message, "Improper encode of parameter");
231    }
232
233    /// An accepted-but-unimplemented parameter is an error, not a silently different answer.
234    #[test]
235    fn unimplemented_parameters_are_refused_rather_than_ignored() {
236        for key in UNIMPLEMENTED_KEYS {
237            let e = params(&[(key, "1")])
238                .reject_unknown_find_keys()
239                .unwrap_err();
240            assert_eq!(
241                e.code,
242                parse_rust_core::ErrorCode::CommandUnavailable,
243                "{key}"
244            );
245        }
246        // A read preference only picks a replica, so it is accepted and ignored.
247        assert!(params(&[("readPreference", "SECONDARY")])
248            .reject_unknown_find_keys()
249            .is_ok());
250    }
251
252    #[test]
253    fn a_batch_sub_request_carries_its_parameters_as_json() {
254        let body: Json = serde_json::from_str(r#"{"where":{"a":1},"limit":5}"#).expect("literal");
255        let p = Params::from_json(Some(&body));
256        assert_eq!(p.get("where"), Some(r#"{"a":1}"#));
257        assert_eq!(p.get("limit"), Some("5"));
258        assert_eq!(p.find_options().expect("options").limit, Some(5));
259    }
260
261    #[test]
262    fn a_malformed_where_reports_upstreams_message() {
263        let e = params(&[("where", "{oops")]).parse_where().unwrap_err();
264        assert_eq!(e.code, parse_rust_core::ErrorCode::InvalidJson);
265        assert_eq!(e.message, "where parameter is not valid JSON");
266    }
267
268    #[test]
269    fn count_is_a_truthiness_test() {
270        assert!(params(&[("count", "1")]).wants_count());
271        assert!(params(&[("count", "true")]).wants_count());
272        assert!(!params(&[("count", "0")]).wants_count());
273        assert!(!params(&[]).wants_count());
274    }
275
276    #[test]
277    fn limit_falls_back_to_the_parse_default_and_zero_is_honoured() {
278        assert_eq!(params(&[]).find_options().expect("o").limit, Some(100));
279        assert_eq!(
280            params(&[("limit", "0")]).find_options().expect("o").limit,
281            Some(0)
282        );
283    }
284}