canic/memory/state/
app.rs

1use crate::{
2    Error,
3    cdk::structures::{DefaultMemoryImpl, cell::Cell, memory::VirtualMemory},
4    eager_static, ic_memory, impl_storable_bounded, log,
5    memory::{MemoryError, id::state::APP_STATE_ID, state::StateError},
6};
7use candid::CandidType;
8use derive_more::Display;
9use serde::{Deserialize, Serialize};
10use std::cell::RefCell;
11use thiserror::Error as ThisError;
12
13//
14// APP_STATE
15//
16
17eager_static! {
18    static APP_STATE: RefCell<Cell<AppStateData, VirtualMemory<DefaultMemoryImpl>>> =
19        RefCell::new(Cell::init(
20            ic_memory!(AppState, APP_STATE_ID),
21            AppStateData::default(),
22        ));
23}
24
25///
26/// AppStateError
27///
28
29#[derive(Debug, ThisError)]
30pub enum AppStateError {
31    #[error("app is already in {0} mode")]
32    AlreadyInMode(AppMode),
33}
34
35impl From<AppStateError> for Error {
36    fn from(err: AppStateError) -> Self {
37        MemoryError::from(StateError::from(err)).into()
38    }
39}
40
41///
42/// AppMode
43/// used for the query/update guards
44/// Eventually we'll have more granularity overall
45///
46
47#[derive(
48    CandidType, Clone, Copy, Debug, Default, Display, Eq, PartialEq, Serialize, Deserialize,
49)]
50pub enum AppMode {
51    Enabled,
52    Readonly,
53    #[default]
54    Disabled,
55}
56
57///
58/// AppCommand
59///
60
61#[derive(CandidType, Clone, Copy, Debug, Deserialize, Display, Eq, PartialEq)]
62pub enum AppCommand {
63    Start,
64    Readonly,
65    Stop,
66}
67
68///
69/// AppStateData
70///
71
72#[derive(CandidType, Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
73pub struct AppStateData {
74    pub mode: AppMode,
75}
76
77impl_storable_bounded!(AppStateData, 32, true);
78
79///
80/// AppState
81///
82
83pub struct AppState;
84
85impl AppState {
86    #[must_use]
87    pub fn get_mode() -> AppMode {
88        APP_STATE.with_borrow(|cell| cell.get().mode)
89    }
90
91    pub fn set_mode(mode: AppMode) {
92        APP_STATE.with_borrow_mut(|cell| {
93            let mut data = *cell.get();
94            data.mode = mode;
95            cell.set(data);
96        });
97    }
98
99    pub fn command(cmd: AppCommand) -> Result<(), Error> {
100        APP_STATE.with_borrow_mut(|cell| {
101            let old_mode = cell.get().mode;
102
103            let new_mode = match cmd {
104                AppCommand::Start => AppMode::Enabled,
105                AppCommand::Readonly => AppMode::Readonly,
106                AppCommand::Stop => AppMode::Disabled,
107            };
108
109            if old_mode == new_mode {
110                return Err(AppStateError::AlreadyInMode(old_mode))?;
111            }
112
113            let mut data = *cell.get();
114            data.mode = new_mode;
115            cell.set(data);
116
117            log!(Ok, "app: mode changed {old_mode} -> {new_mode}");
118
119            Ok(())
120        })
121    }
122
123    pub fn import(data: AppStateData) {
124        APP_STATE.with_borrow_mut(|cell| cell.set(data));
125    }
126
127    #[must_use]
128    pub fn export() -> AppStateData {
129        APP_STATE.with_borrow(|cell| *cell.get())
130    }
131}
132
133///
134/// TESTS
135///
136
137#[cfg(test)]
138mod tests {
139    use super::*;
140
141    fn reset_state(mode: AppMode) {
142        AppState::import(AppStateData { mode });
143    }
144
145    #[test]
146    fn default_mode_is_disabled() {
147        reset_state(AppMode::Disabled);
148        assert_eq!(AppState::get_mode(), AppMode::Disabled);
149    }
150
151    #[test]
152    fn can_set_mode() {
153        reset_state(AppMode::Disabled);
154
155        AppState::set_mode(AppMode::Enabled);
156        assert_eq!(AppState::get_mode(), AppMode::Enabled);
157
158        AppState::set_mode(AppMode::Readonly);
159        assert_eq!(AppState::get_mode(), AppMode::Readonly);
160    }
161
162    #[test]
163    fn command_changes_modes() {
164        reset_state(AppMode::Disabled);
165
166        // Start command sets to Enabled
167        assert!(AppState::command(AppCommand::Start).is_ok());
168        assert_eq!(AppState::get_mode(), AppMode::Enabled);
169
170        // Readonly command sets to Readonly
171        assert!(AppState::command(AppCommand::Readonly).is_ok());
172        assert_eq!(AppState::get_mode(), AppMode::Readonly);
173
174        // Stop command sets to Disabled
175        assert!(AppState::command(AppCommand::Stop).is_ok());
176        assert_eq!(AppState::get_mode(), AppMode::Disabled);
177    }
178
179    #[test]
180    fn duplicate_command_fails() {
181        reset_state(AppMode::Enabled);
182
183        // Sending Start again when already Enabled should error
184        let err = AppState::command(AppCommand::Start)
185            .unwrap_err()
186            .to_string();
187
188        assert!(
189            err.contains("app is already in Enabled mode"),
190            "unexpected error: {err}"
191        );
192    }
193
194    #[test]
195    fn import_and_export_state() {
196        reset_state(AppMode::Disabled);
197
198        let data = AppStateData {
199            mode: AppMode::Readonly,
200        };
201        AppState::import(data);
202
203        assert_eq!(AppState::export().mode, AppMode::Readonly);
204
205        // After export we can reuse
206        let exported = AppState::export();
207        assert_eq!(exported, data);
208    }
209}