1use yo_common::{Code, Error, Result};
4
5use crate::db::Handle;
6
7#[derive(Clone)]
27pub struct Counter {
28 pub(crate) db: Handle,
29 pub(crate) key: Vec<u8>,
30}
31
32impl core::fmt::Debug for Counter {
33 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
34 f.debug_struct("Counter")
35 .field("key", &String::from_utf8_lossy(&self.key))
36 .field("value", &self.get().ok())
37 .finish()
38 }
39}
40
41impl Counter {
42 #[must_use]
44 pub fn key(&self) -> &[u8] {
45 &self.key
46 }
47
48 pub fn get(&self) -> Result<i64> {
54 self.db.run(|inner| match inner.strings.get(&self.key)? {
55 Some(v) => v.as_int().ok_or_else(not_a_number),
56 None => Ok(0),
57 })
58 }
59
60 pub fn incr(&self) -> Result<i64> {
67 self.db.run(|inner| inner.strings.incr(&self.key))
68 }
69
70 pub fn decr(&self) -> Result<i64> {
76 self.db.run(|inner| inner.strings.decr(&self.key))
77 }
78
79 pub fn add(&self, by: i64) -> Result<i64> {
86 self.db.run(|inner| inner.strings.incrby(&self.key, by))
87 }
88
89 pub fn set(&self, value: i64) -> Result<()> {
96 let mut buf = itoa_buf();
97 let text = write_i64(&mut buf, value);
98 self.db
99 .run(|inner| inner.strings.set_plain(&self.key, text))
100 }
101
102 pub fn reset(&self) -> Result<()> {
110 self.db.run(|inner| {
111 inner.strings.del(&self.key);
112 Ok(())
113 })
114 }
115}
116
117fn not_a_number() -> Error {
118 Error::new(Code::Invalid, "value is not an integer or out of range")
119}
120
121const fn itoa_buf() -> [u8; 20] {
123 [0; 20]
124}
125
126fn write_i64(buf: &mut [u8; 20], n: i64) -> &[u8] {
130 use core::fmt::Write;
131
132 struct Cursor<'a> {
133 buf: &'a mut [u8; 20],
134 at: usize,
135 }
136 impl Write for Cursor<'_> {
137 fn write_str(&mut self, s: &str) -> core::fmt::Result {
138 let end = self.at + s.len();
139 self.buf[self.at..end].copy_from_slice(s.as_bytes());
140 self.at = end;
141 Ok(())
142 }
143 }
144
145 let mut cursor = Cursor { buf, at: 0 };
146 let _ = write!(cursor, "{n}");
149 let at = cursor.at;
150 &buf[..at]
151}
152
153#[cfg(test)]
154mod tests {
155 use super::*;
156 use crate::{MEMORY, open};
157
158 #[test]
159 fn a_counter_counts_without_being_created_first() {
160 let db = open(MEMORY).unwrap();
161 let hits = db.counter("hits");
162
163 assert_eq!(hits.get().unwrap(), 0);
164 assert_eq!(hits.incr().unwrap(), 1);
165 assert_eq!(hits.add(9).unwrap(), 10);
166 assert_eq!(hits.decr().unwrap(), 9);
167 assert_eq!(hits.add(-9).unwrap(), 0);
168 assert_eq!(hits.key(), b"hits");
169 }
170
171 #[test]
172 fn setting_and_resetting_go_through_the_same_key_a_client_would_see() {
173 let db = open(MEMORY).unwrap();
174 let hits = db.counter("hits");
175
176 hits.set(i64::MIN).unwrap();
177 assert_eq!(hits.get().unwrap(), i64::MIN);
178 assert_eq!(
179 db.strings().get("hits").unwrap().as_deref(),
180 Some(i64::MIN.to_string().as_bytes())
181 );
182
183 hits.set(41).unwrap();
184 assert_eq!(hits.incr().unwrap(), 42);
185
186 hits.reset().unwrap();
187 assert_eq!(hits.get().unwrap(), 0);
188 assert!(!db.strings().exists("hits").unwrap());
189 }
190
191 #[test]
194 fn two_handles_on_a_key_are_one_counter() {
195 let db = open(MEMORY).unwrap();
196 let a = db.counter("hits");
197 let b = db.counter(b"hits".to_vec());
198
199 a.incr().unwrap();
200 b.incr().unwrap();
201 assert_eq!(a.get().unwrap(), 2);
202 assert_eq!(db.strings().incr("hits").unwrap(), 3);
203 assert_eq!(a.clone().get().unwrap(), 3);
204 assert!(format!("{a:?}").contains("hits"));
205 }
206
207 #[test]
208 fn a_key_holding_words_is_not_a_counter() {
209 let db = open(MEMORY).unwrap();
210 db.strings().set("hits", "lots").unwrap();
211 let hits = db.counter("hits");
212
213 let e = hits.get().expect_err("that is not a number");
214 assert_eq!(e.code(), Code::Invalid);
215 assert_eq!(e.message(), "value is not an integer or out of range");
216 assert_eq!(hits.incr().unwrap_err().code(), Code::Invalid);
217 }
218}