oxide_core 0.4.0

Rust engine primitives for Oxide (store, snapshot streams, error model, optional persistence).
Documentation
#![cfg(feature = "state-persistence")]

use oxide_core::{CoreResult, InitContext, Reducer};

#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
struct StructState {
    value: u64,
}

#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
enum EnumState {
    Idle,
    Active { count: u64 },
}

#[derive(Debug, Clone)]
enum Action {
    Inc,
}

enum SideEffect {}

#[derive(Default)]
struct TestReducer;

impl Reducer for TestReducer {
    type State = StructState;
    type Action = Action;
    type SideEffect = SideEffect;

    async fn init(&mut self, _ctx: InitContext<Self::SideEffect>) {}

    fn reduce(
        &mut self,
        state: &mut Self::State,
        ctx: oxide_core::Context<'_, Self::Action, Self::State, ()>,
    ) -> CoreResult<oxide_core::StateChange> {
        match ctx.input {
            Action::Inc => {
                state.value = state.value.saturating_add(1);
            }
        }
        Ok(oxide_core::StateChange::Full)
    }

    fn effect(
        &mut self,
        _state: &mut Self::State,
        _ctx: oxide_core::Context<'_, Self::SideEffect, Self::State, ()>,
    ) -> CoreResult<oxide_core::StateChange> {
        Ok(oxide_core::StateChange::None)
    }
}

#[test]
fn bincode_round_trip_struct_state() {
    let state = StructState { value: 123 };
    let bytes = oxide_core::persistence::encode(&state).expect("encode");
    let decoded: StructState = oxide_core::persistence::decode(&bytes).expect("decode");
    assert_eq!(decoded, state);
}

#[test]
fn bincode_round_trip_enum_state() {
    let state = EnumState::Active { count: 7 };
    let bytes = oxide_core::persistence::encode(&state).expect("encode");
    let decoded: EnumState = oxide_core::persistence::decode(&bytes).expect("decode");
    assert_eq!(decoded, state);
}

#[tokio::test]
async fn dispatch_then_encode_decode_snapshot() {
    fn thread_pool() -> &'static flutter_rust_bridge::SimpleThreadPool {
        static POOL: std::sync::OnceLock<flutter_rust_bridge::SimpleThreadPool> =
            std::sync::OnceLock::new();
        POOL.get_or_init(flutter_rust_bridge::SimpleThreadPool::default)
    }
    let _ = oxide_core::runtime::init(thread_pool);
    #[cfg(feature = "navigation-binding")]
    let _ = oxide_core::init_navigation();

    let engine = oxide_core::ReducerEngine::<TestReducer>::new(
        TestReducer::default(),
        StructState { value: 0 },
    )
    .await
    .unwrap();
    let snapshot = engine.dispatch(Action::Inc).await.expect("dispatch");
    assert_eq!(snapshot.revision, 1);
    assert_eq!(snapshot.state.value, 1);

    let bytes = oxide_core::persistence::encode(&snapshot).expect("encode snapshot");
    let decoded: oxide_core::StateSnapshot<StructState> =
        oxide_core::persistence::decode(&bytes).expect("decode snapshot");

    assert_eq!(decoded, snapshot);
}