Skip to main content

yo/
counter.rs

1//! `Counter`, a handle at one key (`15` section 2).
2
3use yo_common::{Code, Error, Result};
4
5use crate::db::Handle;
6
7/// A counter at one key.
8///
9/// Sugar over [`crate::keyspace::Strings`], and the kind of sugar that is worth
10/// having: counting is the commonest thing a string key is used for, and a
11/// handle that holds the key means the key is spelled once instead of at every
12/// call site, which is one fewer place for a typo to live.
13///
14/// A counter that has never been written reads as zero, the way Redis's does,
15/// so there is no create step and nothing to check before the first `incr`.
16///
17/// ```
18/// let db = yo::open(yo::MEMORY)?;
19/// let hits = db.counter("hits");
20///
21/// assert_eq!(hits.get()?, 0);
22/// assert_eq!(hits.incr()?, 1);
23/// assert_eq!(hits.add(41)?, 42);
24/// # Ok::<(), yo::Error>(())
25/// ```
26#[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    /// The key this counter lives at.
43    #[must_use]
44    pub fn key(&self) -> &[u8] {
45        &self.key
46    }
47
48    /// What the counter reads, which is zero if nothing has written it yet.
49    ///
50    /// # Errors
51    ///
52    /// [`Code::Invalid`] if the key holds something that is not an integer.
53    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    /// Add one and hand back the result. `INCR`.
61    ///
62    /// # Errors
63    ///
64    /// [`Code::Invalid`] if the key holds something that is not an integer, or
65    /// if the result would leave the range of an `i64`.
66    pub fn incr(&self) -> Result<i64> {
67        self.db.run(|inner| inner.strings.incr(&self.key))
68    }
69
70    /// Subtract one and hand back the result. `DECR`.
71    ///
72    /// # Errors
73    ///
74    /// As [`Counter::incr`].
75    pub fn decr(&self) -> Result<i64> {
76        self.db.run(|inner| inner.strings.decr(&self.key))
77    }
78
79    /// Add `by` and hand back the result, which is `DECRBY` for a negative
80    /// `by`. `INCRBY`.
81    ///
82    /// # Errors
83    ///
84    /// As [`Counter::incr`].
85    pub fn add(&self, by: i64) -> Result<i64> {
86        self.db.run(|inner| inner.strings.incrby(&self.key, by))
87    }
88
89    /// Put the counter at `value`, whatever it read before. `SET`.
90    ///
91    /// # Errors
92    ///
93    /// [`Code::Invalid`] if called from inside a callback that is already
94    /// holding this database.
95    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    /// Remove the key, so the counter reads zero again and stops taking up
103    /// room. `DEL`.
104    ///
105    /// # Errors
106    ///
107    /// [`Code::Invalid`] if called from inside a callback that is already
108    /// holding this database.
109    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
121/// Room for the digits of any `i64`, sign included.
122const fn itoa_buf() -> [u8; 20] {
123    [0; 20]
124}
125
126/// The digits of `n`, written into `buf` rather than into a `String`, because a
127/// counter that allocates to set itself is a counter nobody would put on a hot
128/// path.
129fn 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    // Writing an i64 into twenty bytes cannot fail, and the formatter's error
147    // type carries nothing to report anyway.
148    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    /// Two handles on one key are one counter, and the keyspace sees the same
192    /// number, because there is one store underneath and not two.
193    #[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}