Skip to main content

cratestack_axum/query/
computed_params.rs

1//! `?computedParams=` query-value parsing — split out of
2//! [`super`] to stay under the repo's ~200-LoC file convention.
3
4use std::collections::BTreeMap;
5
6use cratestack_core::CratestackError;
7
8/// Decodes the raw `?computedParams=` query value (already percent-decoded
9/// by [`super::parse_query_pairs`]) into a `{ fieldName: paramsJson }` map.
10/// Shared, schema-independent JSON-shape validation — "is this even a JSON
11/// object" — lives here once rather than being re-emitted per model by the
12/// macro; per-model concerns (which keys are legal for *this* model, does
13/// the referenced field have a params type, is it excluded by `?fields=`)
14/// stay in generated code, which is the only place that field list is
15/// known. See `docs/design/computed-fields.md`'s "Parameterized resolvers
16/// on the wire" section.
17pub fn parse_computed_params_object(
18    raw: &str,
19) -> Result<BTreeMap<String, serde_json::Value>, CratestackError> {
20    let value: serde_json::Value = serde_json::from_str(raw).map_err(|error| {
21        CratestackError::Validation(format!("computedParams must be valid JSON: {error}"))
22    })?;
23    match value {
24        serde_json::Value::Object(map) => Ok(map.into_iter().collect()),
25        _ => Err(CratestackError::Validation(
26            "computedParams must be a JSON object".to_owned(),
27        )),
28    }
29}
30
31#[cfg(test)]
32mod tests {
33    use super::*;
34
35    #[test]
36    fn parses_a_valid_computed_params_object() {
37        let map = parse_computed_params_object(r#"{"proxyUrl":{"width":800}}"#)
38            .expect("object should parse");
39        assert_eq!(
40            map.get("proxyUrl"),
41            Some(&serde_json::json!({ "width": 800 }))
42        );
43    }
44
45    #[test]
46    fn rejects_malformed_json() {
47        let error = parse_computed_params_object("{not json").unwrap_err();
48        assert!(matches!(error, CratestackError::Validation(_)));
49    }
50
51    #[test]
52    fn rejects_non_object_json() {
53        let error = parse_computed_params_object("[1,2,3]").unwrap_err();
54        assert!(matches!(error, CratestackError::Validation(_)));
55    }
56
57    #[test]
58    fn accepts_an_empty_object() {
59        let map = parse_computed_params_object("{}").expect("empty object should parse");
60        assert!(map.is_empty());
61    }
62}