Skip to main content

sim_lib_plugin_lv2/
state.rs

1use sim_kernel::{Error, Expr, Result, Symbol};
2use sim_lib_plugin_core::PluginState;
3use sim_value::kind::expr_kind;
4
5const LIB_NS: &str = "plugin-lv2";
6
7/// A portable snapshot of one LV2 plugin's state, keyed by its URI.
8///
9/// Pairs a plugin URI with a [`PluginState`] and round-trips through the shared
10/// [`Expr`] graph via [`Lv2StatePatch::to_expr`] and
11/// [`Lv2StatePatch::from_expr`].
12#[derive(Clone, Debug, PartialEq)]
13pub struct Lv2StatePatch {
14    plugin_uri: String,
15    state: PluginState,
16}
17
18impl Lv2StatePatch {
19    /// Builds a state patch for `plugin_uri` carrying `state`.
20    ///
21    /// # Errors
22    ///
23    /// Returns [`Error::Eval`] when `plugin_uri` is blank.
24    pub fn new(plugin_uri: impl Into<String>, state: PluginState) -> Result<Self> {
25        let plugin_uri = plugin_uri.into();
26        if plugin_uri.trim().is_empty() {
27            return Err(Error::Eval(
28                "LV2 state patch plugin URI cannot be empty".to_owned(),
29            ));
30        }
31        Ok(Self { plugin_uri, state })
32    }
33
34    /// Returns the plugin URI this patch applies to.
35    pub fn plugin_uri(&self) -> &str {
36        &self.plugin_uri
37    }
38
39    /// Returns the captured plugin state.
40    pub fn state(&self) -> &PluginState {
41        &self.state
42    }
43
44    /// Encodes this patch as a tagged [`Expr::Map`] in the `plugin-lv2` namespace.
45    pub fn to_expr(&self) -> Expr {
46        Expr::Map(vec![
47            (
48                field("tag"),
49                Expr::Symbol(Symbol::qualified(LIB_NS, "state-patch")),
50            ),
51            (field("uri"), Expr::String(self.plugin_uri.clone())),
52            (field("state"), self.state.to_expr()),
53        ])
54    }
55
56    /// Decodes a patch from an [`Expr`] emitted by [`Lv2StatePatch::to_expr`].
57    ///
58    /// # Errors
59    ///
60    /// Returns [`Error::Eval`] when `expr` is not a map, its tag is missing or
61    /// wrong, or the `uri`/`state` fields are absent or ill-typed.
62    pub fn from_expr(expr: &Expr) -> Result<Self> {
63        let map = expr_map(expr, "LV2 state patch")?;
64        match lookup(map, "tag") {
65            Some(Expr::Symbol(symbol)) if is_symbol(symbol, LIB_NS, "state-patch") => {}
66            Some(_) => return Err(Error::Eval("LV2 state patch tag is invalid".to_owned())),
67            None => return Err(missing("tag")),
68        }
69        Self::new(
70            expr_string(lookup_required(map, "uri")?, "LV2 plugin URI")?.to_owned(),
71            PluginState::from_expr(lookup_required(map, "state")?)?,
72        )
73    }
74}
75
76fn field(name: &'static str) -> Expr {
77    sim_value::build::qsym(LIB_NS, name)
78}
79
80fn expr_map<'a>(expr: &'a Expr, context: &str) -> Result<&'a [(Expr, Expr)]> {
81    match expr {
82        Expr::Map(entries) => Ok(entries),
83        other => Err(Error::Eval(format!(
84            "expected {context} map, found {}",
85            expr_kind(other)
86        ))),
87    }
88}
89
90fn expr_string<'a>(expr: &'a Expr, context: &str) -> Result<&'a str> {
91    match expr {
92        Expr::String(text) => Ok(text),
93        other => Err(Error::Eval(format!(
94            "expected {context} string, found {}",
95            expr_kind(other)
96        ))),
97    }
98}
99
100fn lookup_required<'a>(map: &'a [(Expr, Expr)], name: &str) -> Result<&'a Expr> {
101    lookup(map, name).ok_or_else(|| missing(name))
102}
103
104fn lookup<'a>(map: &'a [(Expr, Expr)], name: &str) -> Option<&'a Expr> {
105    map.iter().find_map(|(key, value)| match key {
106        Expr::Symbol(symbol) if is_symbol(symbol, LIB_NS, name) => Some(value),
107        _ => None,
108    })
109}
110
111fn is_symbol(symbol: &Symbol, namespace: &str, name: &str) -> bool {
112    symbol.namespace.as_deref() == Some(namespace) && symbol.name.as_ref() == name
113}
114
115fn missing(field: &str) -> Error {
116    Error::Eval(format!("LV2 state patch field is missing: {field}"))
117}