use yo_common::{Code, Error, Result};
use crate::db::Handle;
#[derive(Clone)]
pub struct Counter {
pub(crate) db: Handle,
pub(crate) key: Vec<u8>,
}
impl core::fmt::Debug for Counter {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("Counter")
.field("key", &String::from_utf8_lossy(&self.key))
.field("value", &self.get().ok())
.finish()
}
}
impl Counter {
#[must_use]
pub fn key(&self) -> &[u8] {
&self.key
}
pub fn get(&self) -> Result<i64> {
self.db.run(|inner| match inner.strings.get(&self.key)? {
Some(v) => v.as_int().ok_or_else(not_a_number),
None => Ok(0),
})
}
pub fn incr(&self) -> Result<i64> {
self.db.run(|inner| inner.strings.incr(&self.key))
}
pub fn decr(&self) -> Result<i64> {
self.db.run(|inner| inner.strings.decr(&self.key))
}
pub fn add(&self, by: i64) -> Result<i64> {
self.db.run(|inner| inner.strings.incrby(&self.key, by))
}
pub fn set(&self, value: i64) -> Result<()> {
let mut buf = itoa_buf();
let text = write_i64(&mut buf, value);
self.db
.run(|inner| inner.strings.set_plain(&self.key, text))
}
pub fn reset(&self) -> Result<()> {
self.db.run(|inner| {
inner.strings.del(&self.key);
Ok(())
})
}
}
fn not_a_number() -> Error {
Error::new(Code::Invalid, "value is not an integer or out of range")
}
const fn itoa_buf() -> [u8; 20] {
[0; 20]
}
fn write_i64(buf: &mut [u8; 20], n: i64) -> &[u8] {
use core::fmt::Write;
struct Cursor<'a> {
buf: &'a mut [u8; 20],
at: usize,
}
impl Write for Cursor<'_> {
fn write_str(&mut self, s: &str) -> core::fmt::Result {
let end = self.at + s.len();
self.buf[self.at..end].copy_from_slice(s.as_bytes());
self.at = end;
Ok(())
}
}
let mut cursor = Cursor { buf, at: 0 };
let _ = write!(cursor, "{n}");
let at = cursor.at;
&buf[..at]
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{MEMORY, open};
#[test]
fn a_counter_counts_without_being_created_first() {
let db = open(MEMORY).unwrap();
let hits = db.counter("hits");
assert_eq!(hits.get().unwrap(), 0);
assert_eq!(hits.incr().unwrap(), 1);
assert_eq!(hits.add(9).unwrap(), 10);
assert_eq!(hits.decr().unwrap(), 9);
assert_eq!(hits.add(-9).unwrap(), 0);
assert_eq!(hits.key(), b"hits");
}
#[test]
fn setting_and_resetting_go_through_the_same_key_a_client_would_see() {
let db = open(MEMORY).unwrap();
let hits = db.counter("hits");
hits.set(i64::MIN).unwrap();
assert_eq!(hits.get().unwrap(), i64::MIN);
assert_eq!(
db.strings().get("hits").unwrap().as_deref(),
Some(i64::MIN.to_string().as_bytes())
);
hits.set(41).unwrap();
assert_eq!(hits.incr().unwrap(), 42);
hits.reset().unwrap();
assert_eq!(hits.get().unwrap(), 0);
assert!(!db.strings().exists("hits").unwrap());
}
#[test]
fn two_handles_on_a_key_are_one_counter() {
let db = open(MEMORY).unwrap();
let a = db.counter("hits");
let b = db.counter(b"hits".to_vec());
a.incr().unwrap();
b.incr().unwrap();
assert_eq!(a.get().unwrap(), 2);
assert_eq!(db.strings().incr("hits").unwrap(), 3);
assert_eq!(a.clone().get().unwrap(), 3);
assert!(format!("{a:?}").contains("hits"));
}
#[test]
fn a_key_holding_words_is_not_a_counter() {
let db = open(MEMORY).unwrap();
db.strings().set("hits", "lots").unwrap();
let hits = db.counter("hits");
let e = hits.get().expect_err("that is not a number");
assert_eq!(e.code(), Code::Invalid);
assert_eq!(e.message(), "value is not an integer or out of range");
assert_eq!(hits.incr().unwrap_err().code(), Code::Invalid);
}
}