more CRUDdy bits

This commit is contained in:
Hamcha 2025-01-26 02:35:29 +01:00
parent 0f79975ad6
commit 7a4f3b15b7
Signed by: hamcha
GPG key ID: 1669C533B8CF6D89
4 changed files with 174 additions and 28 deletions
src
main.rs
outbound
repository
services

View file

@ -31,11 +31,13 @@ async fn setup_inmem_store() -> outbound::repository::adapters::memory::InMemory
let store = outbound::repository::adapters::memory::InMemoryStore::new(); let store = outbound::repository::adapters::memory::InMemoryStore::new();
store store
.add_site(outbound::repository::site::SiteMetadata { .create_site(outbound::repository::site::SiteMetadata {
domain: "localhost".to_string(), domain: "localhost".to_string(),
title: "Test site".to_string(), title: "Test site".to_string(),
}) })
.await; .await
.unwrap();
store store
.set_page( .set_page(
"localhost", "localhost",
@ -56,6 +58,7 @@ async fn setup_inmem_store() -> outbound::repository::adapters::memory::InMemory
) )
.await .await
.unwrap(); .unwrap();
store store
.set_page( .set_page(
"localhost", "localhost",
@ -73,6 +76,7 @@ async fn setup_inmem_store() -> outbound::repository::adapters::memory::InMemory
) )
.await .await
.unwrap(); .unwrap();
store store
.add_post( .add_post(
"localhost", "localhost",
@ -85,6 +89,7 @@ async fn setup_inmem_store() -> outbound::repository::adapters::memory::InMemory
}, },
) )
.await; .await;
store store
.add_post( .add_post(
"localhost", "localhost",

View file

@ -36,14 +36,6 @@ impl InMemoryStore {
} }
} }
pub async fn add_site(&self, site: SiteMetadata) {
self.data
.lock()
.await
.sites
.insert(site.domain.clone(), site);
}
pub async fn add_post(&self, site: &str, collection_id: &str, post_id: &str, post: Post) { pub async fn add_post(&self, site: &str, collection_id: &str, post_id: &str, post: Post) {
self.data self.data
.lock() .lock()
@ -61,11 +53,12 @@ impl InMemoryStore {
let store = InMemoryStore::new(); let store = InMemoryStore::new();
store store
.add_site(SiteMetadata { .create_site(SiteMetadata {
domain: "example.com".to_string(), domain: "example.com".to_string(),
title: "Test site".to_string(), title: "Test site".to_string(),
}) })
.await; .await
.unwrap();
store store
.set_page( .set_page(
@ -167,6 +160,21 @@ impl SiteRepository for InMemoryStore {
Ok(pages) Ok(pages)
} }
async fn create_site(&self, site: SiteMetadata) -> Result<()> {
// Check for existing site
if self.data.lock().await.sites.contains_key(&site.domain) {
return Err(repository::site::Error::Conflict);
}
self.data
.lock()
.await
.sites
.insert(site.domain.clone(), site);
Ok(())
}
async fn get_page(&self, domain: &str, page: &str) -> Result<Page> { async fn get_page(&self, domain: &str, page: &str) -> Result<Page> {
self.data self.data
.lock() .lock()
@ -187,6 +195,21 @@ impl SiteRepository for InMemoryStore {
Ok(()) Ok(())
} }
async fn delete_page(&self, domain: &str, page: &str) -> Result<()> {
let deleted = self
.data
.lock()
.await
.pages
.remove(&(domain.to_string(), page.to_string()));
if deleted.is_none() {
return Err(repository::site::Error::NotFound);
}
Ok(())
}
async fn get_post(&self, domain: &str, collection_id: &str, post_id: &str) -> Result<Post> { async fn get_post(&self, domain: &str, collection_id: &str, post_id: &str) -> Result<Post> {
self.data self.data
.lock() .lock()
@ -259,7 +282,10 @@ mod tests {
cursor::CursorOptions, cursor::CursorOptions,
site::{Block, Page, PageContent, PageInfo, Post}, site::{Block, Page, PageContent, PageInfo, Post},
}, },
outbound::repository::{self, site::SiteRepository}, outbound::repository::{
self,
site::{SiteMetadata, SiteRepository},
},
}; };
use super::InMemoryStore; use super::InMemoryStore;
@ -445,4 +471,57 @@ mod tests {
assert_eq!(page.info.name, "new-page"); assert_eq!(page.info.name, "new-page");
assert_eq!(page.content, content); assert_eq!(page.content, content);
} }
#[tokio::test]
async fn delete_page_works() {
let store = InMemoryStore::with_test_data().await;
let _ = store.delete_page("example.com", "/").await.unwrap();
let result = store.get_page("example.com", "/").await;
assert!(result.is_err());
assert!(matches!(
result.err().unwrap(),
repository::site::Error::NotFound
));
}
#[tokio::test]
async fn delete_page_for_nonexistent_fails() {
let store = InMemoryStore::new();
let result = store.delete_page("example.com", "/").await;
assert!(result.is_err());
assert!(matches!(
result.err().unwrap(),
repository::site::Error::NotFound
));
}
#[tokio::test]
async fn creates_a_site() {
let store = InMemoryStore::new();
let _ = store
.create_site(SiteMetadata {
domain: "example.com".to_string(),
title: "Test site".to_string(),
})
.await;
let info = store.get_site_by_domain("example.com").await.unwrap();
assert_eq!(info.domain, "example.com");
assert_eq!(info.title, "Test site");
}
#[tokio::test]
async fn doesnt_overwrite_existing_site() {
let store = InMemoryStore::with_test_data().await;
let result = store
.create_site(SiteMetadata {
domain: "example.com".to_string(),
title: "Test site".to_string(),
})
.await;
assert!(result.is_err());
assert!(matches!(
result.err().unwrap(),
repository::site::Error::Conflict
));
}
} }

View file

@ -17,8 +17,11 @@ pub trait SiteRepository: Send + Sync + 'static {
async fn get_site_by_domain(&self, domain: &str) -> Result<SiteMetadata>; async fn get_site_by_domain(&self, domain: &str) -> Result<SiteMetadata>;
async fn get_pages_for_site(&self, domain: &str) -> Result<Vec<PageInfo>>; async fn get_pages_for_site(&self, domain: &str) -> Result<Vec<PageInfo>>;
async fn create_site(&self, site: SiteMetadata) -> Result<()>;
async fn get_page(&self, domain: &str, name: &str) -> Result<Page>; async fn get_page(&self, domain: &str, name: &str) -> Result<Page>;
async fn set_page(&self, domain: &str, page: Page) -> Result<()>; async fn set_page(&self, domain: &str, page: Page) -> Result<()>;
async fn delete_page(&self, domain: &str, page: &str) -> Result<()>;
async fn get_post(&self, domain: &str, collection_id: &str, id: &str) -> Result<Post>; async fn get_post(&self, domain: &str, collection_id: &str, id: &str) -> Result<Post>;
async fn get_posts_for_collection( async fn get_posts_for_collection(
@ -36,6 +39,9 @@ pub enum Error {
#[error("resource not found")] #[error("resource not found")]
NotFound, NotFound,
#[error("a resource with the same identifier already exists")]
Conflict,
#[error("the server encountered an error: {0}")] #[error("the server encountered an error: {0}")]
ServerError(String), ServerError(String),
} }

View file

@ -7,7 +7,10 @@ use crate::{
cursor::{CursorOptions, Paginated}, cursor::{CursorOptions, Paginated},
site::{Page, Post, SiteInfo}, site::{Page, Post, SiteInfo},
}, },
outbound::repository::{self, site::SiteRepository}, outbound::repository::{
self,
site::{SiteMetadata, SiteRepository},
},
}; };
#[derive(Clone)] #[derive(Clone)]
@ -36,18 +39,34 @@ impl SiteService {
}) })
} }
pub async fn create_site(&self, site: SiteMetadata) -> Result<()> {
self.site_repository.create_site(site).await?;
Ok(())
}
pub async fn get_page(&self, domain: &str, name: &str) -> Result<Page> { pub async fn get_page(&self, domain: &str, name: &str) -> Result<Page> {
let info = self.site_repository.get_site_by_domain(domain).await?; let page = self.site_repository.get_page(domain, name).await?;
let page = self.site_repository.get_page(&info.domain, name).await?;
Ok(page) Ok(page)
} }
pub async fn set_page(&self, domain: &str, page: Page) -> Result<()> {
self.site_repository.set_page(domain, page).await?;
Ok(())
}
pub async fn delete_page(&self, domain: &str, page: &str) -> Result<()> {
self.site_repository.delete_page(domain, page).await?;
Ok(())
}
pub async fn get_post(&self, domain: &str, collection_id: &str, id: &str) -> Result<Post> { pub async fn get_post(&self, domain: &str, collection_id: &str, id: &str) -> Result<Post> {
let info = self.site_repository.get_site_by_domain(domain).await?;
let post = self let post = self
.site_repository .site_repository
.get_post(&info.domain, collection_id, id) .get_post(domain, collection_id, id)
.await?; .await?;
Ok(post) Ok(post)
@ -59,11 +78,10 @@ impl SiteService {
collection_id: &str, collection_id: &str,
cursor: Option<String>, cursor: Option<String>,
) -> Result<Paginated<Post, String>> { ) -> Result<Paginated<Post, String>> {
let info = self.site_repository.get_site_by_domain(domain).await?;
let posts = self let posts = self
.site_repository .site_repository
.get_posts_for_collection( .get_posts_for_collection(
&info.domain, domain,
collection_id, collection_id,
CursorOptions { CursorOptions {
after: cursor, after: cursor,
@ -74,13 +92,6 @@ impl SiteService {
Ok(posts) Ok(posts)
} }
pub async fn set_page(&self, domain: &str, page: Page) -> Result<()> {
let info = self.site_repository.get_site_by_domain(domain).await?;
self.site_repository.set_page(&info.domain, page).await?;
Ok(())
}
} }
pub type Result<T> = std::result::Result<T, Error>; pub type Result<T> = std::result::Result<T, Error>;
@ -108,7 +119,7 @@ mod tests {
use super::{Error, SiteService}; use super::{Error, SiteService};
use crate::{ use crate::{
domain::entities::site::{Block, Page, PageContent, PageInfo, Post}, domain::entities::site::{Block, Page, PageContent, PageInfo, Post},
outbound::repository::adapters::memory::InMemoryStore, outbound::repository::{adapters::memory::InMemoryStore, site::SiteMetadata},
}; };
#[tokio::test] #[tokio::test]
@ -244,4 +255,49 @@ mod tests {
assert_eq!(page.info.name, "new_page"); assert_eq!(page.info.name, "new_page");
assert_eq!(page.content, content); assert_eq!(page.content, content);
} }
#[tokio::test]
async fn deletes_a_page() {
let service = SiteService::new(InMemoryStore::with_test_data().await);
service.delete_page("example.com", "about").await.unwrap();
let result = service.get_page("example.com", "about").await;
assert!(result.is_err());
assert!(matches!(result.err().unwrap(), Error::NotFound));
}
#[tokio::test]
async fn deletes_a_nonexistent_page() {
let service = SiteService::new(InMemoryStore::with_test_data().await);
let result = service.delete_page("example.com", "nonexistent").await;
assert!(result.is_err());
assert!(matches!(result.err().unwrap(), Error::NotFound));
}
#[tokio::test]
async fn creates_a_site() {
let service = SiteService::new(InMemoryStore::with_test_data().await);
service
.create_site(SiteMetadata {
domain: "new.com".to_string(),
title: "New site".to_string(),
})
.await
.unwrap();
}
#[tokio::test]
async fn doesnt_create_a_site_that_already_exists() {
let service = SiteService::new(InMemoryStore::with_test_data().await);
let result = service
.create_site(SiteMetadata {
domain: "example.com".to_string(),
title: "Example site".to_string(),
})
.await;
assert!(result.is_err());
assert!(matches!(
result.err().unwrap(),
Error::RepositoryError(crate::outbound::repository::site::Error::Conflict)
));
}
} }