canic/memory/state/
app.rs1use 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
13eager_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#[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#[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#[derive(CandidType, Clone, Copy, Debug, Deserialize, Display, Eq, PartialEq)]
62pub enum AppCommand {
63 Start,
64 Readonly,
65 Stop,
66}
67
68#[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
79pub 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#[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 assert!(AppState::command(AppCommand::Start).is_ok());
168 assert_eq!(AppState::get_mode(), AppMode::Enabled);
169
170 assert!(AppState::command(AppCommand::Readonly).is_ok());
172 assert_eq!(AppState::get_mode(), AppMode::Readonly);
173
174 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 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 let exported = AppState::export();
207 assert_eq!(exported, data);
208 }
209}