luminarys-sdk 0.1.0

Rust SDK for building Luminarys WASM skills
Documentation
//! Versioned skill state — safe serialisation across schema migrations.

use crate::types::SkillError;
use serde::{Deserialize, Serialize};

/// Wraps any skill state with a schema version.
///
/// This allows safe migration when the state structure changes.
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(default)]
pub struct StateEnvelope {
    #[serde(rename = "schema_version")]
    pub schema_version: u32,
    #[serde(rename = "data", with = "crate::bytes_or_null")]
    pub data: Vec<u8>,
}

/// Encode `state` into a versioned envelope.
///
/// Increment `schema_version` whenever the `State` struct changes.
///
/// ```rust,no_run
/// # use serde::{Serialize, Deserialize};
/// # use luminarys_sdk::state::marshal_state;
/// #[derive(Serialize)]
/// struct State { counter: u64 }
///
/// let bytes = marshal_state(1, &State { counter: 42 }).unwrap();
/// ```
pub fn marshal_state<S: Serialize>(schema_version: u32, state: &S) -> Result<Vec<u8>, SkillError> {
    let data = rmp_serde::to_vec_named(state)?;
    let envelope = StateEnvelope { schema_version, data };
    Ok(rmp_serde::to_vec_named(&envelope)?)
}

/// Decode the versioned envelope and populate `dst`.
///
/// Returns the `schema_version` so callers can run migrations when needed.
/// Returns `(0, ())` when `raw` is empty (fresh state).
///
/// ```rust,no_run
/// # use serde::{Serialize, Deserialize};
/// # use luminarys_sdk::state::unmarshal_state;
/// #[derive(Default, Deserialize)]
/// struct State { counter: u64 }
///
/// # fn example(raw: Vec<u8>) {
/// let (version, state): (u32, State) = unmarshal_state(&raw).unwrap();
/// match version {
///     0 => { /* fresh state, use defaults */ }
///     1 => { /* current schema */ }
///     v => panic!("unknown schema version {v}"),
/// }
/// # }
/// ```
pub fn unmarshal_state<D: serde::de::DeserializeOwned>(
    raw: &[u8],
) -> Result<(u32, D), SkillError> {
    if raw.is_empty() {
        // Fresh state — caller should use D::default()
        return Err(SkillError("empty state".into()));
    }
    let envelope: StateEnvelope = rmp_serde::from_slice(raw)?;
    let value: D = rmp_serde::from_slice(&envelope.data)?;
    Ok((envelope.schema_version, value))
}