#[derive(Debug, Clone)]
pub struct FindOptions<K> {
pub sort: Option<Vec<Sort<K>>>,
pub limit: Option<u32>,
pub offset: Option<u64>,
}
impl<K> Default for FindOptions<K> {
fn default() -> Self {
Self {
sort: None,
limit: None,
offset: None,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Direction {
Asc,
Desc,
}
#[derive(Debug, Clone)]
pub struct Sort<K> {
pub key: K,
pub direction: Direction,
}
pub trait Repository<T>
where
T: crate::domain::entity::HexEntity,
{
fn save(&mut self, entity: T) -> crate::result::hex_result::HexResult<()>;
}
pub trait QueryRepository<T>
where
T: crate::domain::entity::HexEntity,
{
type Filter;
type SortKey;
fn find_one(&self, filter: &Self::Filter) -> crate::result::hex_result::HexResult<Option<T>>;
fn find(
&self,
filter: &Self::Filter,
options: FindOptions<Self::SortKey>,
) -> crate::result::hex_result::HexResult<Vec<T>>;
fn exists(&self, filter: &Self::Filter) -> crate::result::hex_result::HexResult<bool> {
Ok(self.find_one(filter)?.is_some())
}
fn count(&self, filter: &Self::Filter) -> crate::result::hex_result::HexResult<u64> {
Ok(self.find(filter, FindOptions::default())?.len() as u64)
}
fn delete_where(&mut self, _filter: &Self::Filter) -> crate::result::hex_result::HexResult<u64> {
Ok(0)
}
}
#[cfg(test)]
mod tests {
#[derive(Clone, Debug)]
struct TestEntity {
id: u64,
name: String,
}
impl crate::domain::entity::HexEntity for TestEntity {
type Id = u64;
}
#[derive(Clone, Debug)]
enum TestFilter {
ById(u64),
NameEquals(String),
All,
And(Vec<TestFilter>),
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum TestSortKey {
Id,
Name,
}
#[derive(Default)]
struct TestRepository {
entities: Vec<TestEntity>,
}
impl crate::ports::repository::Repository<TestEntity> for TestRepository {
fn save(&mut self, entity: TestEntity) -> crate::result::hex_result::HexResult<()> {
if let Some(i) = self.entities.iter().position(|e| e.id == entity.id) {
self.entities[i] = entity;
} else {
self.entities.push(entity);
}
Ok(())
}
}
impl crate::ports::repository::QueryRepository<TestEntity> for TestRepository {
type Filter = TestFilter;
type SortKey = TestSortKey;
fn find_one(
&self,
filter: &Self::Filter,
) -> crate::result::hex_result::HexResult<Option<TestEntity>> {
Ok(
self
.entities
.iter()
.find(|e| matches_filter(e, filter))
.cloned(),
)
}
fn find(
&self,
filter: &Self::Filter,
options: crate::ports::repository::FindOptions<Self::SortKey>,
) -> crate::result::hex_result::HexResult<Vec<TestEntity>> {
let mut items: Vec<_> = self
.entities
.iter()
.filter(|e| matches_filter(e, filter))
.cloned()
.collect();
if let Some(sorts) = options.sort {
for s in sorts.into_iter().rev() {
match (s.key, s.direction) {
(TestSortKey::Id, crate::ports::repository::Direction::Asc) => {
items.sort_by_key(|e| e.id)
}
(TestSortKey::Id, crate::ports::repository::Direction::Desc) => {
items.sort_by_key(|e| std::cmp::Reverse(e.id))
}
(TestSortKey::Name, crate::ports::repository::Direction::Asc) => {
items.sort_by(|a, b| a.name.cmp(&b.name))
}
(TestSortKey::Name, crate::ports::repository::Direction::Desc) => {
items.sort_by(|a, b| b.name.cmp(&a.name))
}
}
}
}
let offset = options.offset.unwrap_or(0) as usize;
let limit = options
.limit
.map(|l| l as usize)
.unwrap_or_else(|| items.len().saturating_sub(offset));
let end = offset.saturating_add(limit).min(items.len());
Ok(
items
.into_iter()
.skip(offset)
.take(end.saturating_sub(offset))
.collect(),
)
}
fn delete_where(&mut self, filter: &Self::Filter) -> crate::result::hex_result::HexResult<u64> {
let before = self.entities.len();
self.entities.retain(|e| !matches_filter(e, filter));
Ok((before - self.entities.len()) as u64)
}
}
fn matches_filter(e: &TestEntity, f: &TestFilter) -> bool {
match f {
TestFilter::ById(id) => e.id == *id,
TestFilter::NameEquals(n) => &e.name == n,
TestFilter::All => true,
TestFilter::And(fs) => fs.iter().all(|x| matches_filter(e, x)),
}
}
#[test]
fn test_repository_save_and_find_new_api() {
let mut repo = TestRepository {
entities: Vec::new(),
};
<TestRepository as crate::ports::repository::Repository<TestEntity>>::save(
&mut repo,
TestEntity {
id: 2,
name: String::from("B"),
},
)
.unwrap();
<TestRepository as crate::ports::repository::Repository<TestEntity>>::save(
&mut repo,
TestEntity {
id: 1,
name: String::from("A"),
},
)
.unwrap();
let found =
<TestRepository as crate::ports::repository::QueryRepository<TestEntity>>::find_one(
&repo,
&TestFilter::ById(1),
)
.unwrap();
assert!(found.is_some());
let opts = crate::ports::repository::FindOptions {
sort: Some(vec![crate::ports::repository::Sort {
key: TestSortKey::Name,
direction: crate::ports::repository::Direction::Asc,
}]),
limit: Some(1),
offset: Some(0),
};
let page = <TestRepository as crate::ports::repository::QueryRepository<TestEntity>>::find(
&repo,
&TestFilter::All,
opts,
)
.unwrap();
assert_eq!(page.len(), 1);
assert_eq!(page[0].name, "A");
let removed =
<TestRepository as crate::ports::repository::QueryRepository<TestEntity>>::delete_where(
&mut repo,
&TestFilter::ById(2),
)
.unwrap();
assert_eq!(removed, 1);
let none = <TestRepository as crate::ports::repository::QueryRepository<TestEntity>>::find_one(
&repo,
&TestFilter::ById(2),
)
.unwrap();
assert!(none.is_none());
}
}