replace static with in-mem store and test the hell out of it
This commit is contained in:
parent
9249ac7717
commit
b8abd92adb
6 changed files with 263 additions and 84 deletions
|
@ -1,30 +1,39 @@
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
pub struct SiteMetadata {
|
pub struct SiteMetadata {
|
||||||
pub domain: String,
|
pub domain: String,
|
||||||
pub title: String,
|
pub title: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
pub struct PageInfo {
|
pub struct PageInfo {
|
||||||
pub title: String,
|
pub title: String,
|
||||||
pub name: String,
|
pub name: String,
|
||||||
|
pub order: i32,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
pub struct SiteInfo {
|
pub struct SiteInfo {
|
||||||
pub info: SiteMetadata,
|
pub info: SiteMetadata,
|
||||||
pub pages: Vec<PageInfo>,
|
pub pages: Vec<PageInfo>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
pub struct Page {
|
pub struct Page {
|
||||||
pub info: PageInfo,
|
pub info: PageInfo,
|
||||||
pub content: PageContent,
|
pub content: PageContent,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
pub enum PageContent {
|
pub enum PageContent {
|
||||||
Single { content: Post },
|
Single { content: Post },
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
pub struct Post {
|
pub struct Post {
|
||||||
pub blocks: Vec<Block>,
|
pub blocks: Vec<Block>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
pub enum Block {
|
pub enum Block {
|
||||||
Text { text: String },
|
Text { text: String },
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,13 +15,13 @@ fn main() {
|
||||||
|
|
||||||
let mut builder = dioxus::LaunchBuilder::new();
|
let mut builder = dioxus::LaunchBuilder::new();
|
||||||
server_only! {
|
server_only! {
|
||||||
use outbound::repository::adapters::static_data::StaticData;
|
use outbound::repository::adapters::memory::InMemoryStore;
|
||||||
use outbound::services::site::SiteService;
|
use outbound::services::site::SiteService;
|
||||||
|
|
||||||
builder = builder
|
builder = builder
|
||||||
.with_context_provider(
|
.with_context_provider(
|
||||||
|| {
|
|| {
|
||||||
Box::new(SiteService::new(StaticData{}))
|
Box::new(SiteService::new(InMemoryStore::new()))
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
227
src/outbound/repository/adapters/memory.rs
Normal file
227
src/outbound/repository/adapters/memory.rs
Normal file
|
@ -0,0 +1,227 @@
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
domain::entities::site::{Page, PageInfo, SiteMetadata},
|
||||||
|
outbound::repository::{
|
||||||
|
self,
|
||||||
|
site::{Result, SiteRepository},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
pub struct InMemoryStore {
|
||||||
|
sites: HashMap<String, SiteMetadata>,
|
||||||
|
pages: HashMap<(String, String), Page>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl InMemoryStore {
|
||||||
|
pub fn new() -> InMemoryStore {
|
||||||
|
InMemoryStore {
|
||||||
|
sites: HashMap::new(),
|
||||||
|
pages: HashMap::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_site(&mut self, site: SiteMetadata) {
|
||||||
|
self.sites.insert(site.domain.clone(), site);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_page(&mut self, site: &str, page: Page) {
|
||||||
|
self.pages
|
||||||
|
.insert((site.to_string(), page.info.name.clone()), page);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
pub fn with_test_data() -> InMemoryStore {
|
||||||
|
use crate::domain::entities::site::{Block, PageContent, Post};
|
||||||
|
|
||||||
|
let mut store = InMemoryStore::new();
|
||||||
|
store.add_site(SiteMetadata {
|
||||||
|
domain: "example.com".to_string(),
|
||||||
|
title: "Test site".to_string(),
|
||||||
|
});
|
||||||
|
store.add_page(
|
||||||
|
"example.com",
|
||||||
|
Page {
|
||||||
|
info: PageInfo {
|
||||||
|
title: "Home".to_string(),
|
||||||
|
name: "/".to_string(),
|
||||||
|
order: 0,
|
||||||
|
},
|
||||||
|
content: PageContent::Single {
|
||||||
|
content: Post {
|
||||||
|
blocks: vec![Block::Text {
|
||||||
|
text: "Hello, world!".to_string(),
|
||||||
|
}],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
store.add_page(
|
||||||
|
"example.com",
|
||||||
|
Page {
|
||||||
|
info: PageInfo {
|
||||||
|
title: "About".to_string(),
|
||||||
|
name: "/about".to_string(),
|
||||||
|
order: 10,
|
||||||
|
},
|
||||||
|
content: PageContent::Single {
|
||||||
|
content: Post {
|
||||||
|
blocks: vec![Block::Text {
|
||||||
|
text: "This is the about page.".to_string(),
|
||||||
|
}],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
store
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SiteRepository for InMemoryStore {
|
||||||
|
async fn get_site_by_domain(&self, domain: &str) -> Result<SiteMetadata> {
|
||||||
|
self.sites
|
||||||
|
.get(domain)
|
||||||
|
.ok_or(repository::site::Error::NotFound)
|
||||||
|
.cloned()
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_pages_for_site(&self, domain: &str) -> Result<Vec<PageInfo>> {
|
||||||
|
let mut pages = self
|
||||||
|
.pages
|
||||||
|
.iter()
|
||||||
|
.filter(|((site, _), _)| site == domain)
|
||||||
|
.map(|(_, page)| page.info.clone())
|
||||||
|
.collect::<Vec<PageInfo>>();
|
||||||
|
|
||||||
|
pages.sort_by(|a, b| a.order.cmp(&b.order));
|
||||||
|
|
||||||
|
Ok(pages)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_page(&self, domain: &str, page: &str) -> Result<Page> {
|
||||||
|
self.pages
|
||||||
|
.get(&(domain.to_string(), page.to_string()))
|
||||||
|
.ok_or(repository::site::Error::NotFound)
|
||||||
|
.cloned()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use crate::{
|
||||||
|
domain::entities::site::{Block, Page, PageContent, PageInfo, Post, SiteMetadata},
|
||||||
|
outbound::repository::{self, site::SiteRepository},
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::InMemoryStore;
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn get_site_by_domain_works() {
|
||||||
|
let mut store = InMemoryStore::new();
|
||||||
|
store.add_site(SiteMetadata {
|
||||||
|
domain: "test.com".to_string(),
|
||||||
|
title: "Some test site".to_string(),
|
||||||
|
});
|
||||||
|
let info = store.get_site_by_domain("test.com").await.unwrap();
|
||||||
|
assert_eq!(info.domain, "test.com");
|
||||||
|
assert_eq!(info.title, "Some test site");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn get_site_by_domain_for_nonexistent_fails() {
|
||||||
|
let store = InMemoryStore::new();
|
||||||
|
let result = store.get_site_by_domain("test.com").await;
|
||||||
|
assert!(result.is_err());
|
||||||
|
assert!(matches!(
|
||||||
|
result.err().unwrap(),
|
||||||
|
repository::site::Error::NotFound
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn get_pages_for_site_works() {
|
||||||
|
let mut store = InMemoryStore::new();
|
||||||
|
store.add_site(SiteMetadata {
|
||||||
|
domain: "example.com".to_string(),
|
||||||
|
title: "Test site".to_string(),
|
||||||
|
});
|
||||||
|
store.add_page(
|
||||||
|
"example.com",
|
||||||
|
Page {
|
||||||
|
info: PageInfo {
|
||||||
|
title: "Home".to_string(),
|
||||||
|
name: "/".to_string(),
|
||||||
|
order: 0,
|
||||||
|
},
|
||||||
|
content: PageContent::Single {
|
||||||
|
content: Post { blocks: vec![] },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
store.add_page(
|
||||||
|
"example.com",
|
||||||
|
Page {
|
||||||
|
info: PageInfo {
|
||||||
|
title: "Cool page".to_string(),
|
||||||
|
name: "cool".to_string(),
|
||||||
|
order: 10,
|
||||||
|
},
|
||||||
|
content: PageContent::Single {
|
||||||
|
content: Post { blocks: vec![] },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
let pages = store.get_pages_for_site("example.com").await.unwrap();
|
||||||
|
assert_eq!(pages.len(), 2);
|
||||||
|
assert_eq!(pages[0].title, "Home");
|
||||||
|
assert_eq!(pages[0].name, "/");
|
||||||
|
assert_eq!(pages[1].title, "Cool page");
|
||||||
|
assert_eq!(pages[1].name, "cool");
|
||||||
|
assert!(pages[0].order < pages[1].order);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn get_page_works() {
|
||||||
|
let mut store = InMemoryStore::new();
|
||||||
|
store.add_site(SiteMetadata {
|
||||||
|
domain: "example.com".to_string(),
|
||||||
|
title: "Test site".to_string(),
|
||||||
|
});
|
||||||
|
store.add_page(
|
||||||
|
"example.com",
|
||||||
|
Page {
|
||||||
|
info: PageInfo {
|
||||||
|
title: "Home".to_string(),
|
||||||
|
name: "/".to_string(),
|
||||||
|
order: 0,
|
||||||
|
},
|
||||||
|
content: PageContent::Single {
|
||||||
|
content: Post {
|
||||||
|
blocks: vec![Block::Text {
|
||||||
|
text: "Hello, world!".to_string(),
|
||||||
|
}],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
let page = store.get_page("example.com", "/").await.unwrap();
|
||||||
|
let PageContent::Single { content } = page.content;
|
||||||
|
let Post { blocks } = content;
|
||||||
|
assert_eq!(blocks.len(), 1);
|
||||||
|
let Block::Text { text } = &blocks[0];
|
||||||
|
assert_eq!(text, "Hello, world!");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn get_page_for_nonexistent_fails() {
|
||||||
|
let store = InMemoryStore::new();
|
||||||
|
let result = store.get_page("example.com", "/").await;
|
||||||
|
assert!(result.is_err());
|
||||||
|
assert!(matches!(
|
||||||
|
result.err().unwrap(),
|
||||||
|
repository::site::Error::NotFound
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
|
@ -1 +1 @@
|
||||||
pub mod static_data;
|
pub mod memory;
|
||||||
|
|
|
@ -1,60 +0,0 @@
|
||||||
use crate::{
|
|
||||||
domain::entities::site::{Block, Page, PageContent, PageInfo, Post, SiteMetadata},
|
|
||||||
outbound::repository::site::{Error, Result, SiteRepository},
|
|
||||||
};
|
|
||||||
|
|
||||||
pub struct StaticData {}
|
|
||||||
|
|
||||||
impl SiteRepository for StaticData {
|
|
||||||
async fn get_site_by_domain(&self, domain: &str) -> Result<SiteMetadata> {
|
|
||||||
Ok(SiteMetadata {
|
|
||||||
domain: domain.to_string(),
|
|
||||||
title: "Test site".to_string(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn get_pages_for_site(&self, _: &str) -> Result<Vec<PageInfo>> {
|
|
||||||
Ok(vec![
|
|
||||||
PageInfo {
|
|
||||||
title: "Home".to_string(),
|
|
||||||
name: "/".to_string(),
|
|
||||||
},
|
|
||||||
PageInfo {
|
|
||||||
title: "Cool page".to_string(),
|
|
||||||
name: "cool".to_string(),
|
|
||||||
},
|
|
||||||
])
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn get_page(&self, _: &str, name: &str) -> Result<Page> {
|
|
||||||
match name {
|
|
||||||
"/" => Ok(Page {
|
|
||||||
info: PageInfo {
|
|
||||||
title: "Home".to_string(),
|
|
||||||
name: name.to_string(),
|
|
||||||
},
|
|
||||||
content: PageContent::Single {
|
|
||||||
content: Post {
|
|
||||||
blocks: vec![Block::Text {
|
|
||||||
text: "Hello, world!".to_string(),
|
|
||||||
}],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
"cool" => Ok(Page {
|
|
||||||
info: PageInfo {
|
|
||||||
title: "Cool page".to_string(),
|
|
||||||
name: name.to_string(),
|
|
||||||
},
|
|
||||||
content: PageContent::Single {
|
|
||||||
content: Post {
|
|
||||||
blocks: vec![Block::Text {
|
|
||||||
text: "Hello, world!".to_string(),
|
|
||||||
}],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
_ => Err(Error::NotFound),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -15,14 +15,7 @@ impl<SiteRepo: SiteRepository> SiteService<SiteRepo> {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_site(&self, domain: &str) -> Result<SiteInfo> {
|
pub async fn get_site(&self, domain: &str) -> Result<SiteInfo> {
|
||||||
let info = self
|
let info = self.site_repository.get_site_by_domain(domain).await?;
|
||||||
.site_repository
|
|
||||||
.get_site_by_domain(domain)
|
|
||||||
.await
|
|
||||||
.map_err(|e| match e {
|
|
||||||
repository::site::Error::NotFound => Error::NotFound,
|
|
||||||
_ => Error::RepositoryError(e),
|
|
||||||
})?;
|
|
||||||
let pages = self
|
let pages = self
|
||||||
.site_repository
|
.site_repository
|
||||||
.get_pages_for_site(&info.domain)
|
.get_pages_for_site(&info.domain)
|
||||||
|
@ -32,14 +25,7 @@ impl<SiteRepo: SiteRepository> SiteService<SiteRepo> {
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
let info = self.site_repository.get_site_by_domain(domain).await?;
|
||||||
.site_repository
|
|
||||||
.get_site_by_domain(domain)
|
|
||||||
.await
|
|
||||||
.map_err(|e| match e {
|
|
||||||
repository::site::Error::NotFound => Error::NotFound,
|
|
||||||
_ => Error::RepositoryError(e),
|
|
||||||
})?;
|
|
||||||
let page = self.site_repository.get_page(&info.domain, name).await?;
|
let page = self.site_repository.get_page(&info.domain, name).await?;
|
||||||
|
|
||||||
Ok(page)
|
Ok(page)
|
||||||
|
@ -54,20 +40,29 @@ pub enum Error {
|
||||||
NotFound,
|
NotFound,
|
||||||
|
|
||||||
#[error("the server encountered an error: {0}")]
|
#[error("the server encountered an error: {0}")]
|
||||||
RepositoryError(#[from] repository::site::Error),
|
RepositoryError(repository::site::Error),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<repository::site::Error> for Error {
|
||||||
|
fn from(e: repository::site::Error) -> Self {
|
||||||
|
match e {
|
||||||
|
repository::site::Error::NotFound => Error::NotFound,
|
||||||
|
_ => Error::RepositoryError(e),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::SiteService;
|
use super::{Error, SiteService};
|
||||||
use crate::{
|
use crate::{
|
||||||
domain::entities::site::{Block, PageContent},
|
domain::entities::site::{Block, PageContent},
|
||||||
outbound::repository::adapters::static_data::StaticData,
|
outbound::repository::adapters::memory::InMemoryStore,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn gets_site_info() {
|
async fn gets_site_info() {
|
||||||
let service = SiteService::new(StaticData {});
|
let service = SiteService::new(InMemoryStore::with_test_data());
|
||||||
let info = service.get_site("example.com").await.unwrap();
|
let info = service.get_site("example.com").await.unwrap();
|
||||||
assert_eq!(info.info.domain, "example.com");
|
assert_eq!(info.info.domain, "example.com");
|
||||||
assert_eq!(info.info.title, "Test site");
|
assert_eq!(info.info.title, "Test site");
|
||||||
|
@ -76,7 +71,7 @@ mod tests {
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn gets_page_data() {
|
async fn gets_page_data() {
|
||||||
let service = SiteService::new(StaticData {});
|
let service = SiteService::new(InMemoryStore::with_test_data());
|
||||||
let page = service.get_page("example.com", "/").await.unwrap();
|
let page = service.get_page("example.com", "/").await.unwrap();
|
||||||
|
|
||||||
assert_eq!(page.info.title, "Home");
|
assert_eq!(page.info.title, "Home");
|
||||||
|
@ -90,4 +85,12 @@ mod tests {
|
||||||
let Block::Text { text } = &page_content.blocks[0];
|
let Block::Text { text } = &page_content.blocks[0];
|
||||||
assert_eq!(text, "Hello, world!");
|
assert_eq!(text, "Hello, world!");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn gets_nonexistent_page() {
|
||||||
|
let service = SiteService::new(InMemoryStore::with_test_data());
|
||||||
|
let result = service.get_page("example.com", "nonexistent").await;
|
||||||
|
assert!(result.is_err());
|
||||||
|
assert!(matches!(result.err().unwrap(), Error::NotFound));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue