Skip to main content

sim_lib_plugin_core/
state.rs

1use std::collections::BTreeMap;
2
3use sim_kernel::{Error, Expr, NumberLiteral, Result, Symbol};
4use sim_value::kind::expr_kind;
5
6const LIB_NS: &str = "plugin-core";
7
8/// A plugin's persistable state: parameter values plus opaque keyed data.
9///
10/// Parameters are keyed by their numeric id and opaque data by string key, both
11/// in sorted [`BTreeMap`]s so serialization is deterministic. The state
12/// round-trips through an [`Expr`] map via [`PluginState::to_expr`] and
13/// [`PluginState::from_expr`].
14#[derive(Clone, Debug, Default, PartialEq)]
15pub struct PluginState {
16    params: BTreeMap<u32, f64>,
17    data: BTreeMap<String, Expr>,
18}
19
20impl PluginState {
21    /// Creates an empty state with no parameters and no data.
22    pub fn new() -> Self {
23        Self::default()
24    }
25
26    /// Returns the parameter id-to-value map.
27    pub fn params(&self) -> &BTreeMap<u32, f64> {
28        &self.params
29    }
30
31    /// Returns the opaque key-to-[`Expr`] data map.
32    pub fn data(&self) -> &BTreeMap<String, Expr> {
33        &self.data
34    }
35
36    /// Sets the value of the parameter with the given id, inserting it if
37    /// absent.
38    pub fn set_param(&mut self, id: u32, value: f64) {
39        self.params.insert(id, value);
40    }
41
42    /// Returns the stored value for the parameter id, if present.
43    pub fn param(&self, id: u32) -> Option<f64> {
44        self.params.get(&id).copied()
45    }
46
47    /// Inserts or replaces an opaque data entry under `key`.
48    pub fn insert_data(&mut self, key: impl Into<String>, value: Expr) {
49        self.data.insert(key.into(), value);
50    }
51
52    /// Encodes the state as a tagged [`Expr`] map.
53    ///
54    /// The result is a `plugin-core/state`-tagged map carrying `params` and
55    /// `data` vectors; [`PluginState::from_expr`] reverses it.
56    pub fn to_expr(&self) -> Expr {
57        Expr::Map(vec![
58            (
59                field("tag"),
60                Expr::Symbol(Symbol::qualified(LIB_NS, "state")),
61            ),
62            (
63                field("params"),
64                Expr::Vector(
65                    self.params
66                        .iter()
67                        .map(|(id, value)| {
68                            Expr::Map(vec![
69                                (field("id"), number_u32(*id)),
70                                (field("value"), number_f64(*value)),
71                            ])
72                        })
73                        .collect(),
74                ),
75            ),
76            (
77                field("data"),
78                Expr::Vector(
79                    self.data
80                        .iter()
81                        .map(|(key, value)| {
82                            Expr::Map(vec![
83                                (field("key"), Expr::String(key.clone())),
84                                (field("value"), value.clone()),
85                            ])
86                        })
87                        .collect(),
88                ),
89            ),
90        ])
91    }
92
93    /// Decodes a state previously produced by [`PluginState::to_expr`].
94    ///
95    /// # Errors
96    ///
97    /// Returns an error when `expr` is not a `plugin-core/state`-tagged map or
98    /// any required field has the wrong shape or numeric type.
99    pub fn from_expr(expr: &Expr) -> Result<Self> {
100        let map = expr_map(expr, "plugin state")?;
101        match lookup(map, "tag") {
102            Some(Expr::Symbol(symbol)) if is_symbol(symbol, LIB_NS, "state") => {}
103            Some(_) => return Err(Error::Eval("plugin state tag is invalid".to_owned())),
104            None => return Err(missing("tag")),
105        }
106        let mut state = Self::new();
107        for entry in expr_vector(lookup_required(map, "params")?, "params")? {
108            let entry = expr_map(entry, "param entry")?;
109            state.set_param(
110                expr_u32(lookup_required(entry, "id")?, "param id")?,
111                expr_f64(lookup_required(entry, "value")?, "param value")?,
112            );
113        }
114        for entry in expr_vector(lookup_required(map, "data")?, "data")? {
115            let entry = expr_map(entry, "data entry")?;
116            state.insert_data(
117                expr_string(lookup_required(entry, "key")?, "data key")?.to_owned(),
118                lookup_required(entry, "value")?.clone(),
119            );
120        }
121        Ok(state)
122    }
123}
124
125fn field(name: &'static str) -> Expr {
126    sim_value::build::qsym(LIB_NS, name)
127}
128
129fn number_u32(value: u32) -> Expr {
130    Expr::Number(NumberLiteral {
131        domain: Symbol::qualified("numbers", "i64"),
132        canonical: value.to_string(),
133    })
134}
135
136fn number_f64(value: f64) -> Expr {
137    Expr::Number(NumberLiteral {
138        domain: Symbol::qualified("numbers", "f64"),
139        canonical: value.to_string(),
140    })
141}
142
143fn expr_map<'a>(expr: &'a Expr, context: &str) -> Result<&'a [(Expr, Expr)]> {
144    match expr {
145        Expr::Map(entries) => Ok(entries),
146        other => Err(Error::Eval(format!(
147            "expected {context} map, found {}",
148            expr_kind(other)
149        ))),
150    }
151}
152
153fn expr_vector<'a>(expr: &'a Expr, context: &str) -> Result<&'a [Expr]> {
154    match expr {
155        Expr::Vector(items) => Ok(items),
156        other => Err(Error::Eval(format!(
157            "expected {context} vector, found {}",
158            expr_kind(other)
159        ))),
160    }
161}
162
163fn expr_string<'a>(expr: &'a Expr, context: &str) -> Result<&'a str> {
164    match expr {
165        Expr::String(text) => Ok(text),
166        other => Err(Error::Eval(format!(
167            "expected {context} string, found {}",
168            expr_kind(other)
169        ))),
170    }
171}
172
173fn expr_u32(expr: &Expr, context: &str) -> Result<u32> {
174    let text = number_text(expr, context)?;
175    text.parse::<u32>()
176        .map_err(|_| Error::Eval(format!("expected {context} u32 number, found {text}")))
177}
178
179fn expr_f64(expr: &Expr, context: &str) -> Result<f64> {
180    let text = number_text(expr, context)?;
181    text.parse::<f64>()
182        .map_err(|_| Error::Eval(format!("expected {context} f64 number, found {text}")))
183}
184
185fn number_text<'a>(expr: &'a Expr, context: &str) -> Result<&'a str> {
186    match expr {
187        Expr::Number(number) => Ok(number.canonical.as_str()),
188        Expr::String(text) => Ok(text),
189        other => Err(Error::Eval(format!(
190            "expected {context} number, found {}",
191            expr_kind(other)
192        ))),
193    }
194}
195
196fn lookup_required<'a>(map: &'a [(Expr, Expr)], name: &str) -> Result<&'a Expr> {
197    lookup(map, name).ok_or_else(|| missing(name))
198}
199
200fn lookup<'a>(map: &'a [(Expr, Expr)], name: &str) -> Option<&'a Expr> {
201    map.iter().find_map(|(key, value)| match key {
202        Expr::Symbol(symbol) if is_symbol(symbol, LIB_NS, name) => Some(value),
203        _ => None,
204    })
205}
206
207fn is_symbol(symbol: &Symbol, namespace: &str, name: &str) -> bool {
208    symbol.namespace.as_deref() == Some(namespace) && symbol.name.as_ref() == name
209}
210
211fn missing(field: &str) -> Error {
212    Error::Eval(format!("plugin state field is missing: {field}"))
213}