use core::marker::PhantomData;
use std::borrow::Borrow;
use yo_common::{Code, Error, Result};
use yo_index::RawMap;
use yo_shape::Tag;
use crate::db::Handle;
use crate::store::{Decode, Encode};
pub struct Map<K, V> {
db: Handle,
at: usize,
tag: Tag,
marker: PhantomData<fn() -> (K, V)>,
}
impl<K, V> Clone for Map<K, V> {
fn clone(&self) -> Map<K, V> {
Map {
db: self.db.clone(),
at: self.at,
tag: self.tag,
marker: PhantomData,
}
}
}
impl<K, V> core::fmt::Debug for Map<K, V> {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
let name = self
.db
.read(|inner| Ok(inner.collections[self.at].name.clone()))
.unwrap_or_else(|_| "?".to_owned());
f.debug_struct("Map").field("name", &name).finish()
}
}
impl<K: Decode, V: Decode> Map<K, V> {
pub(crate) fn new(db: Handle, at: usize, tag: Tag) -> Map<K, V> {
Map {
db,
at,
tag,
marker: PhantomData,
}
}
pub fn name(&self) -> Result<String> {
self.read(|c| Ok(c.name.clone()))
}
#[must_use]
pub fn tag(&self) -> Tag {
self.tag
}
pub fn get<Q>(&self, key: &Q) -> Result<Option<V>>
where
K: Borrow<Q>,
Q: Encode + ?Sized,
{
self.read(|c| match key.encode(|k| c.data.get(k)) {
Some(bytes) => V::decode(bytes).map(Some),
None => Ok(None),
})
}
pub fn with<Q, R>(&self, key: &Q, f: impl FnOnce(V::Ref<'_>) -> R) -> Result<Option<R>>
where
K: Borrow<Q>,
Q: Encode + ?Sized,
{
self.read(|c| match key.encode(|k| c.data.get(k)) {
Some(bytes) => V::view(bytes).map(|view| Some(f(view))),
None => Ok(None),
})
}
pub fn set<Q, W>(&self, key: &Q, value: &W) -> Result<()>
where
K: Borrow<Q>,
Q: Encode + ?Sized,
V: Borrow<W>,
W: Encode + ?Sized,
{
self.write(|c| {
key.encode(|k| {
value.encode(|v| {
let total = RawMap::header_len() + k.len() + v.len();
if total > RawMap::max_record() {
return Err(too_big(total));
}
c.data.set(k, v);
Ok(())
})
})
})
}
pub fn del<Q>(&self, key: &Q) -> Result<bool>
where
K: Borrow<Q>,
Q: Encode + ?Sized,
{
self.write(|c| Ok(key.encode(|k| c.data.del(k))))
}
pub fn contains<Q>(&self, key: &Q) -> Result<bool>
where
K: Borrow<Q>,
Q: Encode + ?Sized,
{
self.read(|c| Ok(key.encode(|k| c.data.contains(k))))
}
pub fn len(&self) -> Result<usize> {
self.read(|c| Ok(c.data.len()))
}
pub fn is_empty(&self) -> Result<bool> {
self.read(|c| Ok(c.data.is_empty()))
}
#[must_use]
pub const fn max_entry() -> usize {
RawMap::max_record() - RawMap::header_len()
}
fn read<R>(&self, f: impl FnOnce(&crate::db::Collection) -> Result<R>) -> Result<R> {
self.db.read(|inner| f(&inner.collections[self.at]))
}
fn write<R>(&self, f: impl FnOnce(&mut crate::db::Collection) -> Result<R>) -> Result<R> {
self.db.write(|inner| f(&mut inner.collections[self.at]))
}
}
fn too_big(total: usize) -> Error {
Error::fmt(
Code::Full,
format_args!(
"a key and value of {} bytes is larger than the {} a record holds. A value that size belongs in the log region, which arrives with the .yo format in M5",
total - RawMap::header_len(),
Map::<Vec<u8>, Vec<u8>>::max_entry()
),
)
}
#[cfg(test)]
mod tests {
use crate::{MEMORY, open};
#[test]
fn a_map_of_strings_to_numbers_reads_back_what_it_wrote() {
let db = open(MEMORY).unwrap();
let hits = db.map::<String, u64>("hits").unwrap();
assert!(hits.is_empty().unwrap());
hits.set("home", &1).unwrap();
hits.set("about", &2).unwrap();
assert_eq!(hits.get("home").unwrap(), Some(1));
assert_eq!(hits.get("about").unwrap(), Some(2));
assert_eq!(hits.get("nowhere").unwrap(), None);
assert_eq!(hits.len().unwrap(), 2);
assert!(hits.contains("home").unwrap());
assert_eq!(hits.name().unwrap(), "hits");
}
#[test]
fn a_write_replaces_and_a_delete_removes() {
let db = open(MEMORY).unwrap();
let hits = db.map::<String, u64>("hits").unwrap();
hits.set("home", &1).unwrap();
hits.set("home", &9).unwrap();
assert_eq!(hits.get("home").unwrap(), Some(9));
assert_eq!(hits.len().unwrap(), 1);
assert!(hits.del("home").unwrap());
assert!(!hits.del("home").unwrap());
assert_eq!(hits.get("home").unwrap(), None);
assert!(hits.is_empty().unwrap());
}
#[test]
fn neither_the_key_nor_the_value_has_to_be_owned() {
let db = open(MEMORY).unwrap();
let names = db.map::<String, String>("names").unwrap();
names.set("7", "ada").unwrap();
assert_eq!(names.get("7").unwrap().as_deref(), Some("ada"));
assert_eq!(
names.with("7", str::to_owned).unwrap().as_deref(),
Some("ada")
);
}
#[test]
fn keys_can_be_numbers_and_values_can_be_bytes() {
let db = open(MEMORY).unwrap();
let blobs = db.map::<u64, Vec<u8>>("blobs").unwrap();
blobs.set(&7, b"\x00\xff".as_slice()).unwrap();
assert_eq!(blobs.get(&7).unwrap().as_deref(), Some(&b"\x00\xff"[..]));
assert_eq!(blobs.with(&7, <[u8]>::len).unwrap(), Some(2));
assert_eq!(blobs.with(&8, <[u8]>::len).unwrap(), None);
}
#[test]
fn a_clone_of_a_handle_is_the_same_collection() {
let db = open(MEMORY).unwrap();
let hits = db.map::<String, u64>("hits").unwrap();
let same = hits.clone();
hits.set("home", &4).unwrap();
assert_eq!(same.get("home").unwrap(), Some(4));
assert_eq!(same.tag(), hits.tag());
assert!(format!("{same:?}").contains("hits"));
}
#[test]
fn calling_back_into_the_database_from_a_closure_is_an_error() {
let db = open(MEMORY).unwrap();
let hits = db.map::<String, u64>("hits").unwrap();
hits.set("home", &1).unwrap();
let inner = hits.clone();
let e = hits
.with("home", |_| inner.set("other", &2))
.unwrap()
.unwrap()
.expect_err("a write inside a read is re-entrant");
assert_eq!(e.code(), yo_common::Code::Invalid);
assert!(e.message().contains("cannot call back"), "{e}");
assert_eq!(
hits.with("home", |_| inner.get("home").unwrap()).unwrap(),
Some(Some(1))
);
}
#[test]
fn a_record_larger_than_the_arena_takes_is_full_rather_than_a_panic() {
let db = open(MEMORY).unwrap();
let blobs = db.map::<String, Vec<u8>>("blobs").unwrap();
let huge = vec![0u8; super::Map::<String, Vec<u8>>::max_entry()];
let e = blobs
.set("k", huge.as_slice())
.expect_err("one byte too far");
assert_eq!(e.code(), yo_common::Code::Full);
assert!(e.message().contains("belongs in the log region"), "{e}");
let fits = vec![0u8; super::Map::<String, Vec<u8>>::max_entry() - 1];
blobs.set("k", fits.as_slice()).unwrap();
assert_eq!(blobs.with("k", <[u8]>::len).unwrap(), Some(fits.len()));
}
}