Skip to main content

pamoja_profile/
params.rs

1//! The parameters a manifest carries beside a control kind pamoja never shipped.
2
3use std::collections::BTreeMap;
4
5use serde::{Deserialize, Serialize};
6
7/// One parameter of a custom control kind: a number, a flag, or a piece of text.
8///
9/// A manifest writes it as the plain JSON value, so `"warn_below": 2.0`,
10/// `"latching": true`, and `"zone": "north"` are all parameters.
11#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
12#[serde(untagged)]
13pub enum Param {
14    /// A number; an integer in the manifest arrives as a whole number here.
15    Number(f64),
16    /// A flag.
17    Flag(bool),
18    /// A piece of text.
19    Text(String),
20}
21
22/// The parameters of a custom control kind, by name.
23///
24/// These are every field a manifest carried beside `kind` for a kind the library does
25/// not know, kept as data so the factory registered for the kind can read them. The
26/// accessors answer `None` when a parameter is absent or of another type, so a factory
27/// reports what it needed rather than guessing.
28///
29/// # Examples
30///
31/// ```
32/// use pamoja_profile::{Param, Params};
33///
34/// let params = Params::new()
35///     .with("warn_below", 2.0)
36///     .with("latching", true)
37///     .with("zone", "north");
38/// assert_eq!(params.number("warn_below"), Some(2.0));
39/// assert_eq!(params.flag("latching"), Some(true));
40/// assert_eq!(params.text("zone"), Some("north"));
41/// assert_eq!(params.number("zone"), None);
42/// assert_eq!(params.get("missing"), None);
43/// assert!(matches!(params.get("zone"), Some(Param::Text(_))));
44/// ```
45#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
46#[serde(transparent)]
47pub struct Params(BTreeMap<String, Param>);
48
49impl Params {
50    /// Starts with no parameters.
51    ///
52    /// # Returns
53    ///
54    /// An empty set.
55    pub fn new() -> Self {
56        Self::default()
57    }
58
59    /// Adds a parameter, replacing one of the same name.
60    ///
61    /// # Arguments
62    ///
63    /// * `name` - the parameter's name, as the manifest writes it.
64    /// * `value` - a number, a flag, or text.
65    ///
66    /// # Returns
67    ///
68    /// The parameters, for chaining.
69    pub fn with(mut self, name: impl Into<String>, value: impl Into<Param>) -> Self {
70        self.0.insert(name.into(), value.into());
71        self
72    }
73
74    /// Looks a parameter up by name.
75    ///
76    /// # Arguments
77    ///
78    /// * `name` - the parameter's name.
79    ///
80    /// # Returns
81    ///
82    /// The parameter, or `None` when the manifest carried none of that name.
83    pub fn get(&self, name: &str) -> Option<&Param> {
84        self.0.get(name)
85    }
86
87    /// Reads a numeric parameter.
88    ///
89    /// # Arguments
90    ///
91    /// * `name` - the parameter's name.
92    ///
93    /// # Returns
94    ///
95    /// The number, or `None` when the parameter is absent or not a number.
96    pub fn number(&self, name: &str) -> Option<f64> {
97        match self.0.get(name) {
98            Some(Param::Number(value)) => Some(*value),
99            _ => None,
100        }
101    }
102
103    /// Reads a flag parameter.
104    ///
105    /// # Arguments
106    ///
107    /// * `name` - the parameter's name.
108    ///
109    /// # Returns
110    ///
111    /// The flag, or `None` when the parameter is absent or not a flag.
112    pub fn flag(&self, name: &str) -> Option<bool> {
113        match self.0.get(name) {
114            Some(Param::Flag(value)) => Some(*value),
115            _ => None,
116        }
117    }
118
119    /// Reads a text parameter.
120    ///
121    /// # Arguments
122    ///
123    /// * `name` - the parameter's name.
124    ///
125    /// # Returns
126    ///
127    /// The text, or `None` when the parameter is absent or not text.
128    pub fn text(&self, name: &str) -> Option<&str> {
129        match self.0.get(name) {
130            Some(Param::Text(value)) => Some(value),
131            _ => None,
132        }
133    }
134
135    /// Loads parameters from the JSON object a manifest carries beside a custom kind.
136    ///
137    /// # Arguments
138    ///
139    /// * `text` - the JSON object.
140    ///
141    /// # Returns
142    ///
143    /// The parameters.
144    ///
145    /// # Errors
146    ///
147    /// Returns [`Error::Codec`](pamoja_core::Error::Codec) if the text is not an object
148    /// of numbers, flags, and text.
149    #[cfg(feature = "json")]
150    pub fn from_json(text: &str) -> pamoja_core::Result<Self> {
151        serde_json::from_str(text).map_err(|error| pamoja_core::Error::Codec(error.to_string()))
152    }
153
154    /// Writes the parameters as the JSON object a manifest carries beside a custom kind.
155    ///
156    /// # Returns
157    ///
158    /// The JSON text, on one line.
159    ///
160    /// # Errors
161    ///
162    /// Returns [`Error::Codec`](pamoja_core::Error::Codec) if the parameters cannot be
163    /// serialized.
164    #[cfg(feature = "json")]
165    pub fn to_json(&self) -> pamoja_core::Result<String> {
166        serde_json::to_string(self).map_err(|error| pamoja_core::Error::Codec(error.to_string()))
167    }
168
169    /// Walks the parameters in name order.
170    ///
171    /// # Returns
172    ///
173    /// Each name with its parameter.
174    pub fn iter(&self) -> impl Iterator<Item = (&str, &Param)> {
175        self.0.iter().map(|(name, value)| (name.as_str(), value))
176    }
177
178    /// Counts the parameters.
179    ///
180    /// # Returns
181    ///
182    /// How many the manifest carried.
183    pub fn len(&self) -> usize {
184        self.0.len()
185    }
186
187    /// Whether there are no parameters.
188    ///
189    /// # Returns
190    ///
191    /// `true` when the manifest carried only the kind.
192    pub fn is_empty(&self) -> bool {
193        self.0.is_empty()
194    }
195}
196
197impl From<BTreeMap<String, Param>> for Params {
198    fn from(map: BTreeMap<String, Param>) -> Self {
199        Self(map)
200    }
201}
202
203impl From<Params> for BTreeMap<String, Param> {
204    fn from(params: Params) -> Self {
205        params.0
206    }
207}
208
209impl From<f64> for Param {
210    fn from(value: f64) -> Self {
211        Param::Number(value)
212    }
213}
214
215impl From<f32> for Param {
216    fn from(value: f32) -> Self {
217        Param::Number(f64::from(value))
218    }
219}
220
221impl From<i32> for Param {
222    fn from(value: i32) -> Self {
223        Param::Number(f64::from(value))
224    }
225}
226
227impl From<u32> for Param {
228    fn from(value: u32) -> Self {
229        Param::Number(f64::from(value))
230    }
231}
232
233impl From<bool> for Param {
234    fn from(value: bool) -> Self {
235        Param::Flag(value)
236    }
237}
238
239impl From<&str> for Param {
240    fn from(value: &str) -> Self {
241        Param::Text(value.to_owned())
242    }
243}
244
245impl From<String> for Param {
246    fn from(value: String) -> Self {
247        Param::Text(value)
248    }
249}
250
251#[cfg(test)]
252mod tests {
253    use super::*;
254
255    #[test]
256    fn parameters_keep_their_type_and_round_trip_as_json() {
257        let params = Params::new()
258            .with("warn_below", 2.5f32)
259            .with("samples", 4u32)
260            .with("latching", false)
261            .with("zone", "north");
262        let json = serde_json::to_string(&params).unwrap();
263        assert_eq!(
264            json,
265            r#"{"latching":false,"samples":4.0,"warn_below":2.5,"zone":"north"}"#
266        );
267        let restored: Params = serde_json::from_str(&json).unwrap();
268        assert_eq!(restored, params);
269        assert_eq!(restored.number("samples"), Some(4.0));
270        assert_eq!(restored.flag("latching"), Some(false));
271        assert_eq!(restored.text("zone"), Some("north"));
272        assert_eq!(restored.len(), 4);
273        assert!(!restored.is_empty());
274        assert_eq!(
275            restored.iter().map(|(name, _)| name).collect::<Vec<_>>(),
276            ["latching", "samples", "warn_below", "zone"]
277        );
278    }
279
280    #[test]
281    fn an_integer_in_a_manifest_is_a_number() {
282        let params: Params = serde_json::from_str(r#"{ "count": 3, "on": true }"#).unwrap();
283        assert_eq!(params.number("count"), Some(3.0));
284        assert_eq!(params.flag("on"), Some(true));
285        assert_eq!(params.flag("count"), None);
286    }
287}