diff --git a/src/main.rs b/src/main.rs index c1f7aa3..57e951b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -31,11 +31,13 @@ async fn setup_inmem_store() -> outbound::repository::adapters::memory::InMemory let store = outbound::repository::adapters::memory::InMemoryStore::new(); store - .add_site(outbound::repository::site::SiteMetadata { + .create_site(outbound::repository::site::SiteMetadata { domain: "localhost".to_string(), title: "Test site".to_string(), }) - .await; + .await + .unwrap(); + store .set_page( "localhost", @@ -56,6 +58,7 @@ async fn setup_inmem_store() -> outbound::repository::adapters::memory::InMemory ) .await .unwrap(); + store .set_page( "localhost", @@ -73,6 +76,7 @@ async fn setup_inmem_store() -> outbound::repository::adapters::memory::InMemory ) .await .unwrap(); + store .add_post( "localhost", @@ -85,6 +89,7 @@ async fn setup_inmem_store() -> outbound::repository::adapters::memory::InMemory }, ) .await; + store .add_post( "localhost", diff --git a/src/outbound/repository/adapters/memory.rs b/src/outbound/repository/adapters/memory.rs index 95822bb..81974a0 100644 --- a/src/outbound/repository/adapters/memory.rs +++ b/src/outbound/repository/adapters/memory.rs @@ -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) { self.data .lock() @@ -61,11 +53,12 @@ impl InMemoryStore { let store = InMemoryStore::new(); store - .add_site(SiteMetadata { + .create_site(SiteMetadata { domain: "example.com".to_string(), title: "Test site".to_string(), }) - .await; + .await + .unwrap(); store .set_page( @@ -167,6 +160,21 @@ impl SiteRepository for InMemoryStore { 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> { self.data .lock() @@ -187,6 +195,21 @@ impl SiteRepository for InMemoryStore { 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> { self.data .lock() @@ -259,7 +282,10 @@ mod tests { cursor::CursorOptions, site::{Block, Page, PageContent, PageInfo, Post}, }, - outbound::repository::{self, site::SiteRepository}, + outbound::repository::{ + self, + site::{SiteMetadata, SiteRepository}, + }, }; use super::InMemoryStore; @@ -445,4 +471,57 @@ mod tests { assert_eq!(page.info.name, "new-page"); 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 + )); + } } diff --git a/src/outbound/repository/site.rs b/src/outbound/repository/site.rs index b420ff4..735ff5d 100644 --- a/src/outbound/repository/site.rs +++ b/src/outbound/repository/site.rs @@ -17,8 +17,11 @@ pub trait SiteRepository: Send + Sync + 'static { 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 create_site(&self, site: SiteMetadata) -> Result<()>; + async fn get_page(&self, domain: &str, name: &str) -> Result<Page>; 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_posts_for_collection( @@ -36,6 +39,9 @@ pub enum Error { #[error("resource not found")] NotFound, + #[error("a resource with the same identifier already exists")] + Conflict, + #[error("the server encountered an error: {0}")] ServerError(String), } diff --git a/src/outbound/services/site.rs b/src/outbound/services/site.rs index 1a33178..3300755 100644 --- a/src/outbound/services/site.rs +++ b/src/outbound/services/site.rs @@ -7,7 +7,10 @@ use crate::{ cursor::{CursorOptions, Paginated}, site::{Page, Post, SiteInfo}, }, - outbound::repository::{self, site::SiteRepository}, + outbound::repository::{ + self, + site::{SiteMetadata, SiteRepository}, + }, }; #[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> { - let info = self.site_repository.get_site_by_domain(domain).await?; - let page = self.site_repository.get_page(&info.domain, name).await?; + let page = self.site_repository.get_page(domain, name).await?; 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> { - let info = self.site_repository.get_site_by_domain(domain).await?; let post = self .site_repository - .get_post(&info.domain, collection_id, id) + .get_post(domain, collection_id, id) .await?; Ok(post) @@ -59,11 +78,10 @@ impl SiteService { collection_id: &str, cursor: Option<String>, ) -> Result<Paginated<Post, String>> { - let info = self.site_repository.get_site_by_domain(domain).await?; let posts = self .site_repository .get_posts_for_collection( - &info.domain, + domain, collection_id, CursorOptions { after: cursor, @@ -74,13 +92,6 @@ impl SiteService { 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>; @@ -108,7 +119,7 @@ mod tests { use super::{Error, SiteService}; use crate::{ domain::entities::site::{Block, Page, PageContent, PageInfo, Post}, - outbound::repository::adapters::memory::InMemoryStore, + outbound::repository::{adapters::memory::InMemoryStore, site::SiteMetadata}, }; #[tokio::test] @@ -244,4 +255,49 @@ mod tests { assert_eq!(page.info.name, "new_page"); 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) + )); + } }