Skip to main content

doido_controller/
params.rs

1//! Strong parameters: a Rails-style allowlist over untyped request params.
2//!
3//! Deserializing straight into a typed struct is the common path in doido, but
4//! when you hold an untyped params object (e.g. a nested form) [`Params`] lets
5//! you `require` a key and `permit` an explicit set of fields before use, so
6//! unexpected keys can never be mass-assigned.
7
8use doido_core::Result;
9use serde::de::DeserializeOwned;
10use serde_json::{Map, Value};
11
12/// A wrapper around a JSON params object supporting `require`/`permit`.
13#[derive(Debug, Clone)]
14pub struct Params {
15    value: Value,
16}
17
18impl Params {
19    /// Wrap a params value (typically a JSON object).
20    pub fn new(value: Value) -> Self {
21        Self { value }
22    }
23
24    /// Return the nested params under `key`, erroring if it is absent (Rails
25    /// `params.require(:key)`).
26    pub fn require(&self, key: &str) -> Result<Params> {
27        match self.value.get(key) {
28            Some(v) => Ok(Params::new(v.clone())),
29            None => Err(doido_core::anyhow::anyhow!(
30                "param `{key}` is required but missing"
31            )),
32        }
33    }
34
35    /// Keep only the listed top-level keys, dropping everything else (Rails
36    /// `params.permit(:a, :b)`). Non-object params become an empty object.
37    pub fn permit(&self, allowed: &[&str]) -> Params {
38        let mut out = Map::new();
39        if let Value::Object(map) = &self.value {
40            for &key in allowed {
41                if let Some(v) = map.get(key) {
42                    out.insert(key.to_string(), v.clone());
43                }
44            }
45        }
46        Params::new(Value::Object(out))
47    }
48
49    /// Borrow a raw value by key.
50    pub fn get(&self, key: &str) -> Option<&Value> {
51        self.value.get(key)
52    }
53
54    /// Deserialize the (typically permitted) params into a typed value.
55    pub fn deserialize<T: DeserializeOwned>(&self) -> Result<T> {
56        serde_json::from_value(self.value.clone())
57            .map_err(|e| doido_core::anyhow::anyhow!("params deserialization failed: {e}"))
58    }
59
60    /// Consume into the underlying JSON value.
61    pub fn into_value(self) -> Value {
62        self.value
63    }
64}