use std::sync::RwLock;
use crate::transport::{Page, PageMeta, PageQuery};
use crate::types::{DateTime, Url};
pub trait Stored: Clone + Send + Sync + 'static {
fn key(&self) -> String;
fn last_updated(&self) -> DateTime;
}
#[derive(Debug, Default)]
pub struct InMemoryStore<T> {
items: RwLock<Vec<T>>,
max_page: usize,
}
impl<T: Stored> InMemoryStore<T> {
#[must_use]
pub fn new() -> Self {
Self { items: RwLock::new(Vec::new()), max_page: 100 }
}
#[must_use]
pub fn with_page_size(max_page: usize) -> Self {
Self { items: RwLock::new(Vec::new()), max_page: max_page.max(1) }
}
pub fn put(&self, item: T) -> bool {
let mut items = self.items.write().expect("store lock poisoned");
let key = item.key().to_ascii_lowercase();
if let Some(index) = items.iter().position(|existing| existing.key().to_ascii_lowercase() == key) {
items[index] = item;
false
} else {
items.push(item);
true
}
}
#[must_use]
pub fn get(&self, key: &str) -> Option<T> {
let items = self.items.read().expect("store lock poisoned");
items.iter().find(|item| item.key().eq_ignore_ascii_case(key)).cloned()
}
pub fn remove(&self, key: &str) -> bool {
let mut items = self.items.write().expect("store lock poisoned");
let before = items.len();
items.retain(|item| !item.key().eq_ignore_ascii_case(key));
items.len() != before
}
#[must_use]
pub fn len(&self) -> usize {
self.items.read().expect("store lock poisoned").len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.len() == 0
}
#[must_use]
pub fn all(&self) -> Vec<T> {
let mut items = self.items.read().expect("store lock poisoned").clone();
items.sort_by_key(Stored::last_updated);
items
}
#[must_use]
pub fn page(&self, query: &PageQuery, base: &Url) -> Page<T> {
let matching: Vec<T> = self
.all()
.into_iter()
.filter(|item| {
let updated = item.last_updated();
query.date_from.is_none_or(|from| updated >= from)
&& query.date_to.is_none_or(|to| updated < to)
})
.collect();
let total = matching.len() as u64;
let offset = query.offset_or_default();
let limit = query.limit.map_or(self.max_page as u64, |l| l.min(self.max_page as u64)).max(1);
let start = usize::try_from(offset).unwrap_or(usize::MAX).min(matching.len());
let end = start.saturating_add(usize::try_from(limit).unwrap_or(usize::MAX)).min(matching.len());
let items = matching[start..end].to_vec();
let next = if (end as u64) < total {
let next_query = query.clone().with_offset(end as u64).with_limit(limit);
Some(next_query.apply_to(base))
} else {
None
};
Page { items, meta: PageMeta { next, total_count: Some(total), limit: Some(self.max_page as u64) } }
}
}
macro_rules! stored_for {
($($ty:ty),* $(,)?) => {$(
impl Stored for $ty {
fn key(&self) -> String {
self.id.as_str().to_owned()
}
fn last_updated(&self) -> DateTime {
self.last_updated
}
}
)*};
}
stored_for!(
crate::v2_3_0::locations::Location,
crate::v2_3_0::sessions::Session,
crate::v2_3_0::cdrs::Cdr,
crate::v2_3_0::tariffs::Tariff,
);
impl Stored for crate::v2_3_0::tokens::Token {
fn key(&self) -> String {
self.uid.as_str().to_owned()
}
fn last_updated(&self) -> DateTime {
self.last_updated
}
}
pub type InMemoryLocations = InMemoryStore<crate::v2_3_0::locations::Location>;
pub type InMemorySessions = InMemoryStore<crate::v2_3_0::sessions::Session>;
pub type InMemoryCdrs = InMemoryStore<crate::v2_3_0::cdrs::Cdr>;
pub type InMemoryTariffs = InMemoryStore<crate::v2_3_0::tariffs::Tariff>;
pub type InMemoryTokens = InMemoryStore<crate::v2_3_0::tokens::Token>;
#[cfg(test)]
mod tests {
use super::*;
use crate::testkit::sample;
fn store_with(count: usize, page_size: usize) -> InMemoryLocations {
let store = InMemoryLocations::with_page_size(page_size);
for i in 0..count {
let mut location = sample::location(&format!("LOC{i}")).unwrap();
location.last_updated =
DateTime::from_unix_timestamp(1_705_312_800 + i64::try_from(i).unwrap() * 60).unwrap();
store.put(location);
}
store
}
fn base() -> Url {
Url::new("https://cpo.example.com/ocpi/cpo/2.3.0/locations").unwrap()
}
#[test]
fn put_reports_whether_the_object_was_created() {
let store = InMemoryLocations::new();
let location = sample::location("LOC1").unwrap();
assert!(store.put(location.clone()), "the first PUT creates");
assert!(!store.put(location), "the second replaces");
assert_eq!(store.len(), 1);
}
#[test]
fn ids_are_matched_case_insensitively_as_cistring_requires() {
let store = InMemoryLocations::new();
store.put(sample::location("LOC1").unwrap());
assert!(store.get("loc1").is_some());
assert!(store.remove("Loc1"));
assert!(store.is_empty());
}
#[test]
fn a_page_carries_a_next_link_until_the_last_one() {
let store = store_with(25, 10);
let first = store.page(&PageQuery::new(), &base());
assert_eq!(first.items.len(), 10);
assert_eq!(first.meta.total_count, Some(25));
assert_eq!(first.meta.limit, Some(10), "X-Limit is the server maximum");
let next = first.meta.next.expect("not the last page");
assert!(next.as_str().contains("offset=10"), "{next}");
let last = store.page(&PageQuery::new().with_offset(20), &base());
assert_eq!(last.items.len(), 5);
assert!(last.meta.next.is_none(), "the last page has no Link");
}
#[test]
fn the_date_window_is_half_open() {
let store = store_with(5, 100);
let all = store.all();
let second = all[1].last_updated;
let fourth = all[3].last_updated;
let page = store.page(&PageQuery::between(second, fourth), &base());
assert_eq!(page.items.len(), 2);
assert_eq!(page.items[0].last_updated, second);
assert_eq!(page.meta.total_count, Some(2), "the total reflects the filter");
}
#[test]
fn sequential_intervals_do_not_overlap() {
let store = store_with(6, 100);
let all = store.all();
let boundary = all[3].last_updated;
let first = store.page(&PageQuery::between(all[0].last_updated, boundary), &base());
let second = store.page(&PageQuery::since(boundary), &base());
assert_eq!(first.items.len() + second.items.len(), 6, "every object appears exactly once");
}
#[test]
fn the_total_count_excludes_limit_and_offset() {
let store = store_with(25, 10);
let page = store.page(&PageQuery::new().with_offset(10).with_limit(5), &base());
assert_eq!(page.items.len(), 5);
assert_eq!(page.meta.total_count, Some(25));
}
#[test]
fn objects_come_back_oldest_first() {
let store = store_with(3, 100);
let page = store.page(&PageQuery::new(), &base());
assert!(page.items[0].last_updated < page.items[1].last_updated);
assert!(page.items[1].last_updated < page.items[2].last_updated);
}
}