ic_canister_kit/functions/
record.rs1use candid::CandidType;
2use serde::{Deserialize, Serialize};
3
4use crate::{
5 identity::CallerId,
6 types::{PageData, QueryPage, QueryPageError},
7};
8
9#[derive(CandidType, Serialize, Deserialize, Debug, Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord)]
11pub struct RecordId(u64);
12
13impl From<u64> for RecordId {
14 fn from(value: u64) -> Self {
15 Self(value)
16 }
17}
18
19impl RecordId {
20 pub fn into_inner(&self) -> u64 {
22 self.0
23 }
24 pub fn next(self) -> Self {
26 Self(self.0 + 1)
27 }
28}
29
30#[derive(CandidType, Serialize, Deserialize, Debug, Clone)]
32pub struct MigratedRecords<Record> {
33 pub removed: u64,
35 pub next_id: u64,
37 pub records: Vec<Record>,
39}
40
41pub trait Searchable<Record> {
43 fn test(&self, record: &Record) -> bool;
45}
46
47pub trait Recordable<Record, RecordTopic, Search: Searchable<Record>> {
49 fn record_find_all(&self) -> &[Record];
52
53 fn record_push(&mut self, caller: CallerId, topic: RecordTopic, content: String) -> RecordId;
56 fn record_update(&mut self, record_id: RecordId, done: String);
58 fn record_migrate(&mut self, max: u32) -> MigratedRecords<Record>;
60
61 fn record_find_by_page(
63 &self,
64 page: &QueryPage,
65 max: u32,
66 search: &Option<Search>,
67 ) -> Result<PageData<&Record>, QueryPageError> {
68 let list = self.record_find_all();
69 if let Some(search) = search {
70 return page.query_desc_by_list_and_filter(list, max, |item| search.test(item));
71 }
72 page.query_desc_by_list(list, max)
73 }
74}
75
76pub mod basic {
80 use std::collections::HashSet;
81
82 use candid::CandidType;
83 use serde::{Deserialize, Serialize};
84
85 use crate::{
86 functions::{
87 record::MigratedRecords,
88 types::{RecordId, Recordable, Searchable},
89 },
90 identity::CallerId,
91 types::TimestampNanos,
92 };
93
94 pub type RecordTopic = u8;
96
97 #[derive(CandidType, Serialize, Deserialize, Debug, Clone)]
99 pub struct Record {
100 pub id: RecordId,
102 pub created: TimestampNanos,
104 pub caller: CallerId,
106 pub topic: RecordTopic,
108 pub content: String,
110 pub done: Option<(TimestampNanos, String)>,
112 }
113
114 impl Record {
115 #[inline]
116 fn same(&self, id: &RecordId) -> bool {
117 self.id == *id
118 }
119
120 #[inline]
121 fn update(&mut self, done: String) {
122 self.done = Some((crate::times::now(), done));
123 }
124 }
125
126 #[derive(CandidType, Serialize, Deserialize, Debug, Clone)]
128 pub struct RecordSearch {
129 pub id: Option<(Option<RecordId>, Option<RecordId>)>,
131 pub created: Option<(Option<TimestampNanos>, Option<TimestampNanos>)>,
133 pub caller: Option<HashSet<CallerId>>,
135 pub topic: Option<HashSet<RecordTopic>>,
137 pub content: Option<String>,
139 }
140
141 impl Searchable<Record> for RecordSearch {
142 #[allow(unused)]
143 #[inline]
144 fn test(&self, record: &Record) -> bool {
145 if let Some((id_min, id_max)) = &self.id {
146 if let Some(id_min) = &id_min
147 && record.id < *id_min
148 {
149 return false;
150 }
151 if let Some(id_max) = &id_max
152 && *id_max < record.id
153 {
154 return false;
155 }
156 }
157 if let Some(created) = self.created {
158 let (created_min, created_max) = created;
159 if let Some(created_min) = created_min
160 && record.created < created_min
161 {
162 return false;
163 }
164 if let Some(created_max) = created_max
165 && created_max < record.created
166 {
167 return false;
168 }
169 }
170 if let Some(caller) = &self.caller
171 && !caller.contains(&record.caller)
172 {
173 return false;
174 }
175 if let Some(topic) = &self.topic
176 && !topic.contains(&record.topic)
177 {
178 return false;
179 }
180 if let Some(content) = &self.content
181 && !record.content.contains(content)
182 {
183 return false;
184 }
185 true
186 }
187 }
188
189 #[derive(CandidType, Serialize, Deserialize, Debug, Clone)]
191 pub struct Records {
192 pub max: u64,
194 pub removed: u64,
196 pub next_id: RecordId,
198 pub records: Vec<Record>,
200 }
201
202 impl Default for Records {
203 fn default() -> Self {
204 Self {
205 max: 1024 * 64, removed: Default::default(),
207 next_id: Default::default(),
208 records: Default::default(),
209 }
210 }
211 }
212
213 impl Recordable<Record, RecordTopic, RecordSearch> for Records {
214 fn record_find_all(&self) -> &[Record] {
218 &self.records
219 }
220
221 fn record_push(&mut self, caller: CallerId, topic: RecordTopic, content: String) -> RecordId {
223 if self.max <= self.records.len() as u64 {
225 let (_migrated, left) = self.records.split_at(1);
226 self.records = left.to_owned();
227 self.removed += 1;
228 }
229
230 let id = self.next_id;
231
232 self.next_id = self.next_id.next();
233
234 self.records.push(Record {
235 id,
236 created: crate::times::now(),
237 caller,
238 topic,
239 content,
240 done: None,
241 });
242
243 id
244 }
245
246 fn record_update(&mut self, record_id: RecordId, done: String) {
248 let list = &mut self.records;
249 let mut index = list.len();
250 loop {
251 index -= 1;
252 if let Some(item) = list.get_mut(index) {
253 if item.same(&record_id) {
254 item.update(done);
255 break;
256 }
257 } else {
258 break;
259 }
260 if index == 0 {
261 break;
262 }
263 }
264 }
265
266 fn record_migrate(&mut self, max: u32) -> MigratedRecords<Record> {
268 let removed = self.removed;
269 let next_id = self.next_id.into_inner();
270 let records = if self.records.len() < max as usize {
271 std::mem::take(&mut self.records) } else {
273 let (migrated, left) = self.records.split_at(max as usize);
274 let migrated = migrated.to_owned();
275 self.records = left.to_owned();
276 migrated
277 };
278 MigratedRecords {
279 removed,
280 next_id,
281 records,
282 }
283 }
284 }
285
286 #[derive(CandidType, Serialize, Deserialize, Debug, Clone)]
288 pub struct RecordSearchArg {
289 pub id: Option<(Option<u64>, Option<u64>)>,
291 pub created: Option<(Option<u64>, Option<u64>)>,
293 pub caller: Option<HashSet<CallerId>>,
295 pub topic: Option<HashSet<String>>,
297 pub content: Option<String>,
299 }
300
301 impl RecordSearchArg {
302 pub fn into<E, F: Fn(&str) -> Result<RecordTopic, E>>(self, f: F) -> Result<RecordSearch, E> {
304 Ok(RecordSearch {
305 id: self.id.map(|(a, b)| (a.map(|a| a.into()), b.map(|b| b.into()))),
306 created: self
307 .created
308 .map(|(a, b)| (a.map(|a| (a as i128).into()), b.map(|b| (b as i128).into()))),
309 caller: self.caller,
310 topic: self
311 .topic
312 .map(|topic| topic.iter().map(|t| f(t)).collect::<Result<HashSet<_>, _>>())
313 .transpose()?,
314 content: self.content,
315 })
316 }
317 }
318}