canic-core 0.85.1

Canic — a canister orchestration and management toolkit for the Internet Computer
Documentation
use crate::{
    cdk::structures::{DefaultMemoryImpl, cell::Cell, memory::VirtualMemory},
    role_contract::allocation::memory::env::APP_STATE_ID,
    storage::prelude::*,
};
use std::cell::RefCell;

pub use crate::domain::state::AppMode;

//
// APP_STATE
//

eager_static! {
    static APP_STATE: RefCell<Cell<AppStateRecord, VirtualMemory<DefaultMemoryImpl>>> =
        RefCell::new(Cell::init(
            crate::ic_memory_key!(authority = CANIC_CORE_MEMORY_AUTHORITY, key = "canic.core.app_state.v1", ty = AppState, id = APP_STATE_ID),
            AppStateRecord::default(),
        ));
}

///
/// AppStateRecord
///

#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct AppStateRecord {
    pub mode: AppMode,
    #[serde(default = "default_cycles_funding_enabled")]
    pub cycles_funding_enabled: bool,
}

impl_storable_bounded!(AppStateRecord, 32, true);

impl AppStateRecord {
    pub const STATE_CONTRACT_NAME: &'static str = "AppStateRecord";
}

const fn default_cycles_funding_enabled() -> bool {
    true
}

impl Default for AppStateRecord {
    fn default() -> Self {
        Self {
            mode: AppMode::default(),
            cycles_funding_enabled: default_cycles_funding_enabled(),
        }
    }
}

///
/// AppStateData
///
/// Canonical application-state import/export snapshot.
///

#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub struct AppStateData {
    pub record: AppStateRecord,
}

impl AppStateData {
    pub const STATE_CONTRACT_NAME: &'static str = "AppStateData";
}

///
/// AppState
///

pub struct AppState;

impl AppState {
    #[must_use]
    pub(crate) fn get_mode() -> AppMode {
        APP_STATE.with_borrow(|cell| cell.get().mode)
    }

    pub(crate) fn set_mode(mode: AppMode) {
        APP_STATE.with_borrow_mut(|cell| {
            let mut data = *cell.get();
            data.mode = mode;
            cell.set(data);
        });
    }

    #[must_use]
    pub(crate) fn cycles_funding_enabled() -> bool {
        APP_STATE.with_borrow(|cell| cell.get().cycles_funding_enabled)
    }

    pub(crate) fn set_cycles_funding_enabled(enabled: bool) {
        APP_STATE.with_borrow_mut(|cell| {
            let mut data = *cell.get();
            data.cycles_funding_enabled = enabled;
            cell.set(data);
        });
    }

    pub(crate) fn import(data: AppStateData) {
        APP_STATE.with_borrow_mut(|cell| cell.set(data.record));
    }

    #[must_use]
    pub(crate) fn export() -> AppStateData {
        AppStateData {
            record: APP_STATE.with_borrow(|cell| *cell.get()),
        }
    }
}

// -----------------------------------------------------------------------------
// Tests
// -----------------------------------------------------------------------------

#[cfg(test)]
mod tests {
    use super::*;
    use crate::cdk::structures::storable::Storable;

    fn reset_state(mode: AppMode) {
        AppState::import(AppStateData {
            record: AppStateRecord {
                mode,
                cycles_funding_enabled: true,
            },
        });
    }

    #[test]
    fn default_mode_is_enabled() {
        AppState::import(AppStateData::default());
        assert_eq!(AppState::get_mode(), AppMode::Enabled);
    }

    #[test]
    fn can_set_mode() {
        reset_state(AppMode::Disabled);

        AppState::set_mode(AppMode::Enabled);
        assert_eq!(AppState::get_mode(), AppMode::Enabled);

        AppState::set_mode(AppMode::Readonly);
        assert_eq!(AppState::get_mode(), AppMode::Readonly);
    }

    #[test]
    fn import_and_export_state() {
        reset_state(AppMode::Disabled);

        let data = AppStateRecord {
            mode: AppMode::Readonly,
            cycles_funding_enabled: false,
        };
        AppState::import(AppStateData { record: data });

        assert_eq!(AppState::export().record.mode, AppMode::Readonly);
        assert!(!AppState::export().record.cycles_funding_enabled);

        let exported = AppState::export();
        assert_eq!(exported, AppStateData { record: data });
    }

    #[test]
    fn app_state_record_storable_roundtrips_shared_app_mode() {
        let data = AppStateRecord {
            mode: AppMode::Readonly,
            cycles_funding_enabled: false,
        };

        let bytes = data.to_bytes();
        let decoded = AppStateRecord::from_bytes(bytes);

        assert_eq!(decoded, data);
    }

    #[test]
    fn cycles_funding_switch_round_trip() {
        AppState::import(AppStateData::default());
        assert!(AppState::cycles_funding_enabled());

        AppState::set_cycles_funding_enabled(false);
        assert!(!AppState::cycles_funding_enabled());

        AppState::set_cycles_funding_enabled(true);
        assert!(AppState::cycles_funding_enabled());
    }
}