use std::sync::Mutex;
use async_trait::async_trait;
use lino_objects_codec::LinoValue;
use crate::collection::apply_collection_query;
use crate::problem::LinoHttpError;
use crate::query::CollectionQuery;
use crate::value::{as_object, get, merge, set};
#[async_trait]
pub trait Store: Send + Sync {
fn id_field(&self) -> &str {
"id"
}
async fn list(&self, query: &CollectionQuery) -> Result<LinoValue, LinoHttpError>;
async fn get(&self, identifier: &str) -> Result<Option<LinoValue>, LinoHttpError>;
async fn create(&self, body: &LinoValue) -> Result<LinoValue, LinoHttpError>;
async fn update(
&self,
identifier: &str,
body: &LinoValue,
) -> Result<Option<LinoValue>, LinoHttpError>;
async fn patch(
&self,
identifier: &str,
body: &LinoValue,
) -> Result<Option<LinoValue>, LinoHttpError>;
async fn remove(&self, identifier: &str) -> Result<bool, LinoHttpError>;
}
pub fn normalize_id(identifier: &str) -> LinoValue {
match identifier.parse::<i64>() {
Ok(number) if !identifier.is_empty() => LinoValue::Int(number),
_ => LinoValue::String(identifier.to_string()),
}
}
fn normalize_value_id(identifier: &LinoValue) -> LinoValue {
match identifier {
LinoValue::String(text) => normalize_id(text),
other => other.clone(),
}
}
pub fn id_to_string(identifier: &LinoValue) -> String {
crate::value::to_query_text(identifier)
}
#[derive(Debug, Default)]
pub struct MemoryStore {
id_field: String,
state: Mutex<MemoryState>,
}
#[derive(Debug, Default)]
struct MemoryState {
items: Vec<(LinoValue, LinoValue)>,
next_id: i64,
}
impl MemoryStore {
pub fn new() -> Self {
Self {
id_field: "id".to_string(),
state: Mutex::new(MemoryState {
items: Vec::new(),
next_id: 1,
}),
}
}
pub fn with_id_field<S: Into<String>>(id_field: S) -> Self {
Self {
id_field: id_field.into(),
..Self::new()
}
}
pub fn seeded<I: IntoIterator<Item = LinoValue>>(items: I) -> Self {
let store = Self::new();
for item in items {
store.insert(&item);
}
store
}
pub fn insert(&self, body: &LinoValue) -> LinoValue {
let mut state = self.state.lock().expect("store is not poisoned");
let identifier = match get(body, &self.id_field) {
Some(LinoValue::Null) | None => {
let identifier = LinoValue::Int(state.next_id);
state.next_id += 1;
identifier
}
Some(provided) => normalize_value_id(provided),
};
if let LinoValue::Int(number) = identifier {
if number >= state.next_id {
state.next_id = number + 1;
}
}
let mut item = if matches!(body, LinoValue::Object(_)) {
body.clone()
} else {
LinoValue::Object(Vec::new())
};
set(&mut item, self.id_field.clone(), identifier.clone());
match state.items.iter_mut().find(|(key, _)| *key == identifier) {
Some(entry) => entry.1 = item.clone(),
None => state.items.push((identifier, item.clone())),
}
item
}
pub fn read(&self, identifier: &str) -> Option<LinoValue> {
let key = normalize_id(identifier);
let state = self.state.lock().expect("store is not poisoned");
state
.items
.iter()
.find(|(existing, _)| *existing == key)
.map(|(_, item)| item.clone())
}
pub fn items(&self) -> Vec<LinoValue> {
let state = self.state.lock().expect("store is not poisoned");
state.items.iter().map(|(_, item)| item.clone()).collect()
}
pub fn len(&self) -> usize {
self.state
.lock()
.expect("store is not poisoned")
.items
.len()
}
pub fn is_empty(&self) -> bool {
self.len() == 0
}
pub fn clear(&self) {
let mut state = self.state.lock().expect("store is not poisoned");
state.items.clear();
state.next_id = 1;
}
fn replace(&self, identifier: &str, item: LinoValue) -> Option<LinoValue> {
let key = normalize_id(identifier);
let mut state = self.state.lock().expect("store is not poisoned");
let entry = state
.items
.iter_mut()
.find(|(existing, _)| *existing == key)?;
entry.1 = item.clone();
Some(item)
}
}
#[async_trait]
impl Store for MemoryStore {
fn id_field(&self) -> &str {
&self.id_field
}
async fn list(&self, query: &CollectionQuery) -> Result<LinoValue, LinoHttpError> {
Ok(apply_collection_query(&self.items(), query))
}
async fn get(&self, identifier: &str) -> Result<Option<LinoValue>, LinoHttpError> {
Ok(self.read(identifier))
}
async fn create(&self, body: &LinoValue) -> Result<LinoValue, LinoHttpError> {
Ok(self.insert(body))
}
async fn update(
&self,
identifier: &str,
body: &LinoValue,
) -> Result<Option<LinoValue>, LinoHttpError> {
if self.read(identifier).is_none() {
return Ok(None);
}
let mut item = if as_object(body).is_some() {
body.clone()
} else {
LinoValue::Object(Vec::new())
};
set(&mut item, self.id_field.clone(), normalize_id(identifier));
Ok(self.replace(identifier, item))
}
async fn patch(
&self,
identifier: &str,
body: &LinoValue,
) -> Result<Option<LinoValue>, LinoHttpError> {
let Some(existing) = self.read(identifier) else {
return Ok(None);
};
let mut item = merge(&existing, body);
set(&mut item, self.id_field.clone(), normalize_id(identifier));
Ok(self.replace(identifier, item))
}
async fn remove(&self, identifier: &str) -> Result<bool, LinoHttpError> {
let key = normalize_id(identifier);
let mut state = self.state.lock().expect("store is not poisoned");
match state
.items
.iter()
.position(|(existing, _)| *existing == key)
{
Some(index) => {
state.items.remove(index);
Ok(true)
}
None => Ok(false),
}
}
}