readd most of the frontend
This commit is contained in:
parent
b8abd92adb
commit
192a952ae8
15 changed files with 660 additions and 62 deletions
37
Cargo.lock
generated
37
Cargo.lock
generated
|
@ -427,6 +427,7 @@ dependencies = [
|
|||
"dioxus-html",
|
||||
"dioxus-liveview",
|
||||
"dioxus-logger",
|
||||
"dioxus-router",
|
||||
"dioxus-signals",
|
||||
"dioxus-ssr",
|
||||
"dioxus-web",
|
||||
|
@ -738,6 +739,34 @@ dependencies = [
|
|||
"tracing-wasm",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dioxus-router"
|
||||
version = "0.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0299dea6f8759eb766ecf79247d50139f4b640056c3e60e8af254ea6921fb60d"
|
||||
dependencies = [
|
||||
"dioxus-cli-config",
|
||||
"dioxus-history",
|
||||
"dioxus-lib",
|
||||
"dioxus-router-macro",
|
||||
"rustversion",
|
||||
"tracing",
|
||||
"url",
|
||||
"urlencoding",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dioxus-router-macro"
|
||||
version = "0.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "149652f16bb41cdcb4477829947bdefd7847660986e7eabaa26fd7bf43acc985"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"slab",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dioxus-rsx"
|
||||
version = "0.6.1"
|
||||
|
@ -1506,10 +1535,12 @@ dependencies = [
|
|||
name = "mabel-hex"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"dioxus",
|
||||
"dioxus-cli-config",
|
||||
"dioxus-logger",
|
||||
"dotenvy",
|
||||
"serde",
|
||||
"thiserror 2.0.11",
|
||||
"tokio",
|
||||
]
|
||||
|
@ -2641,6 +2672,12 @@ dependencies = [
|
|||
"percent-encoding",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "urlencoding"
|
||||
version = "2.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da"
|
||||
|
||||
[[package]]
|
||||
name = "utf-8"
|
||||
version = "0.7.6"
|
||||
|
|
|
@ -5,11 +5,13 @@ authors = ["Ash Keel <ash@nebula.cafe>"]
|
|||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
dioxus = { version = "0.6", features = ["fullstack"] }
|
||||
async-trait = "0.1"
|
||||
dioxus = { version = "0.6", features = ["fullstack", "router"] }
|
||||
dioxus-cli-config = "0.6"
|
||||
dioxus-logger = "0.6"
|
||||
dotenvy = { version = "0.15", optional = true }
|
||||
thiserror = "2.0.11"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
thiserror = "2"
|
||||
tokio = { version = "1", features = ["full"], optional = true }
|
||||
|
||||
[features]
|
||||
|
|
|
@ -1,22 +0,0 @@
|
|||
@import url(https://rsms.me/inter/inter.css);
|
||||
|
||||
:root {
|
||||
background-color: #F8F7FF;
|
||||
color: rgb(23, 3, 21);
|
||||
|
||||
font-family: Inter, sans-serif;
|
||||
/* fix for Chrome */
|
||||
font-feature-settings: 'liga' 1, 'calt' 1;
|
||||
}
|
||||
|
||||
@supports (font-variation-settings: normal) {
|
||||
:root {
|
||||
font-family: InterVariable, sans-serif;
|
||||
}
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
62
assets/style/main.css
Normal file
62
assets/style/main.css
Normal file
|
@ -0,0 +1,62 @@
|
|||
@import url(https://rsms.me/inter/inter.css);
|
||||
|
||||
:root {
|
||||
background-color: #F8F7FF;
|
||||
color: rgb(23, 3, 21);
|
||||
|
||||
font-family: Inter, sans-serif;
|
||||
/* fix for Chrome */
|
||||
font-feature-settings: 'liga' 1, 'calt' 1;
|
||||
}
|
||||
|
||||
@supports (font-variation-settings: normal) {
|
||||
:root {
|
||||
font-family: InterVariable, sans-serif;
|
||||
}
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
header.site-header {
|
||||
background-color: #5720a5;
|
||||
color: #F8F7FF;
|
||||
padding: 0.5rem 1rem;
|
||||
|
||||
a[href],
|
||||
a[href]:visited {
|
||||
color: #fab19a;
|
||||
|
||||
&.active {
|
||||
pointer-events: none;
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
|
||||
nav.page-list {
|
||||
font-size: 16pt;
|
||||
|
||||
&>ul {
|
||||
display: flex;
|
||||
list-style-type: none;
|
||||
gap: 1rem;
|
||||
padding: 0;
|
||||
|
||||
&>li a {
|
||||
font-weight: 300;
|
||||
text-decoration: none;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
main {
|
||||
padding: 0.5rem 1rem;
|
||||
}
|
7
src/domain/entities/cursor.rs
Normal file
7
src/domain/entities/cursor.rs
Normal file
|
@ -0,0 +1,7 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
pub struct Paginated<DataType, CursorType> {
|
||||
pub data: Vec<DataType>,
|
||||
pub next: Option<CursorType>,
|
||||
}
|
|
@ -1 +1,2 @@
|
|||
pub mod cursor;
|
||||
pub mod site;
|
||||
|
|
|
@ -1,39 +1,55 @@
|
|||
#[derive(Debug, Clone)]
|
||||
pub struct SiteMetadata {
|
||||
pub domain: String,
|
||||
pub title: String,
|
||||
}
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct PageInfo {
|
||||
pub title: String,
|
||||
pub name: String,
|
||||
pub order: i32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct SiteInfo {
|
||||
pub info: SiteMetadata,
|
||||
pub domain: String,
|
||||
pub title: String,
|
||||
pub pages: Vec<PageInfo>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Page {
|
||||
pub info: PageInfo,
|
||||
pub content: PageContent,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub enum PageContent {
|
||||
Single { content: Post },
|
||||
Single {
|
||||
content: Post,
|
||||
},
|
||||
Collection {
|
||||
kind: CollectionKind,
|
||||
collection_id: String,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub enum CollectionKind {
|
||||
Blog,
|
||||
Gallery,
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Post {
|
||||
pub blocks: Vec<Block>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
#[derive(PartialEq, Debug, Clone, Serialize, Deserialize)]
|
||||
pub enum Block {
|
||||
Text { text: String },
|
||||
Gallery { images: Vec<Image> },
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Image {
|
||||
pub src: String,
|
||||
pub caption: String,
|
||||
}
|
||||
|
|
29
src/inbound/renderer/meta.rs
Normal file
29
src/inbound/renderer/meta.rs
Normal file
|
@ -0,0 +1,29 @@
|
|||
use dioxus::prelude::*;
|
||||
|
||||
use crate::domain::entities::site::{Page, SiteInfo};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct SiteContext {
|
||||
pub info: SiteInfo,
|
||||
}
|
||||
|
||||
pub fn set_site(info: &SiteInfo) {
|
||||
let _state = use_context_provider(|| SiteContext { info: info.clone() });
|
||||
}
|
||||
|
||||
pub fn site() -> SiteContext {
|
||||
use_context::<SiteContext>()
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct PageContext {
|
||||
pub data: Page,
|
||||
}
|
||||
|
||||
pub fn set_page(data: Page) {
|
||||
let _state = use_context_provider(|| PageContext { data });
|
||||
}
|
||||
|
||||
pub fn page() -> PageContext {
|
||||
use_context::<PageContext>()
|
||||
}
|
|
@ -2,10 +2,185 @@
|
|||
|
||||
use dioxus::prelude::*;
|
||||
|
||||
use page::{Page, PostElement};
|
||||
use server::get_site_info;
|
||||
|
||||
use crate::domain::entities::site::PageContent;
|
||||
|
||||
mod meta;
|
||||
mod page;
|
||||
mod server;
|
||||
|
||||
#[derive(Clone, Routable, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
|
||||
#[rustfmt::skip]
|
||||
enum Route {
|
||||
#[layout(SiteLayout)]
|
||||
#[route("/")]
|
||||
Home {},
|
||||
#[nest("/p")]
|
||||
#[route("/:page")]
|
||||
Single { page: String },
|
||||
#[route("/:page/:id")]
|
||||
Post { page: String, id: String },
|
||||
#[end_nest]
|
||||
#[route("/:..route")]
|
||||
PageNotFound { route: Vec<String> },
|
||||
}
|
||||
|
||||
pub fn App() -> Element {
|
||||
// Retrieve site info
|
||||
let site_info = use_server_future(get_site_info)?.suspend()?;
|
||||
|
||||
// Inject site info in context
|
||||
match &*site_info.read() {
|
||||
Ok(info) => meta::set_site(info),
|
||||
Err(err) => {
|
||||
return rsx! {
|
||||
h1 { "FATAL ERROR: {err}" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
rsx! {
|
||||
h1 { "Hello, world!" }
|
||||
Router::<Route> {}
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn PageNotFound(route: Vec<String>) -> Element {
|
||||
rsx! {
|
||||
h1 { "404 - {route:?}" }
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn PageLink(page: String, title: String, current: Route) -> Element {
|
||||
let to = if page == "/" {
|
||||
Route::Home {}
|
||||
} else {
|
||||
Route::Single { page }
|
||||
};
|
||||
|
||||
let class = if current == to { "active" } else { "" };
|
||||
|
||||
rsx! {
|
||||
Link { to, class, "{title}" }
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn SiteLayout() -> Element {
|
||||
let site = meta::site();
|
||||
let route = use_route::<Route>();
|
||||
|
||||
rsx! {
|
||||
header { class: "site-header",
|
||||
nav { class: "page-list",
|
||||
ul {
|
||||
for page in site.info.pages {
|
||||
li {
|
||||
PageLink {
|
||||
page: page.name,
|
||||
title: page.title,
|
||||
current: route.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
h1 { "{site.info.title}" }
|
||||
}
|
||||
SuspenseBoundary { fallback: |_context: SuspenseContext| rsx! { "..." },
|
||||
main { Outlet::<Route> {} }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn Home() -> Element {
|
||||
let site = meta::site();
|
||||
let page_ref =
|
||||
use_server_future(move || server::get_page(site.info.domain.clone(), "/".to_string()))?
|
||||
.suspend()?;
|
||||
match &*page_ref.read() {
|
||||
Ok(page) => meta::set_page(page.clone()),
|
||||
Err(err) => {
|
||||
return rsx! {
|
||||
h1 { "FATAL ERROR: {err}" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
rsx! {
|
||||
Page {}
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn Single(page: String) -> Element {
|
||||
let site = meta::site();
|
||||
let page_ref =
|
||||
use_server_future(move || server::get_page(site.info.domain.clone(), page.clone()))?
|
||||
.suspend()?;
|
||||
match &*page_ref.read() {
|
||||
Ok(page) => meta::set_page(page.clone()),
|
||||
Err(err) => {
|
||||
return rsx! {
|
||||
h1 { "FATAL ERROR: {err}" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
rsx! {
|
||||
Page {}
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn Post(page: String, id: String) -> Element {
|
||||
let site = meta::site();
|
||||
let domain = site.info.domain.clone();
|
||||
let page_ref =
|
||||
use_server_future(move || server::get_page(domain.clone(), page.clone()))?.suspend()?;
|
||||
match &*page_ref.read() {
|
||||
Ok(page) => {
|
||||
meta::set_page(page.clone());
|
||||
}
|
||||
Err(err) => {
|
||||
return rsx! {
|
||||
h1 { "FATAL ERROR: {err}" }
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let page = meta::page();
|
||||
|
||||
let collection_id = match &page.data.content {
|
||||
PageContent::Collection {
|
||||
kind: _,
|
||||
collection_id,
|
||||
} => collection_id.clone(),
|
||||
_ => {
|
||||
return rsx! {
|
||||
h1 { "NOT A COLLECTION" }
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let post_ref = use_server_future(move || {
|
||||
server::get_post(site.info.domain.clone(), collection_id.clone(), id.clone())
|
||||
})?
|
||||
.suspend()?;
|
||||
let post = match &*post_ref.read() {
|
||||
Ok(post) => post.clone(),
|
||||
Err(err) => {
|
||||
return rsx! {
|
||||
h1 { "FATAL ERROR: {err}" }
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
rsx! {
|
||||
PostElement { post }
|
||||
}
|
||||
}
|
||||
|
|
74
src/inbound/renderer/page.rs
Normal file
74
src/inbound/renderer/page.rs
Normal file
|
@ -0,0 +1,74 @@
|
|||
use dioxus::prelude::*;
|
||||
|
||||
use crate::domain::entities::site::{Block, PageContent, Post};
|
||||
|
||||
use super::meta;
|
||||
|
||||
#[component]
|
||||
pub fn Page() -> Element {
|
||||
let page = meta::page();
|
||||
let info = page.data.info;
|
||||
|
||||
rsx! {
|
||||
h2 { "Page {info.title}" }
|
||||
match page.data.content {
|
||||
PageContent::Single { content } => rsx! {
|
||||
PostElement { post: content }
|
||||
},
|
||||
PageContent::Collection { kind: _, collection_id } => {
|
||||
rsx! {
|
||||
SuspenseBoundary { fallback: |_context: SuspenseContext| rsx! { "..." },
|
||||
//Collection { collection_id }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
#[component]
|
||||
pub fn Collection(collection_id: String) -> Element {
|
||||
let site = meta::site();
|
||||
let posts =
|
||||
use_server_future(move || get_posts(site.info.domain.clone(), collection_id.clone()))?
|
||||
.suspend()?;
|
||||
|
||||
let result = match &*posts.read() {
|
||||
Ok(posts) => rsx! {
|
||||
for post in posts.data.clone() {
|
||||
PostElement { post }
|
||||
}
|
||||
},
|
||||
Err(err) => {
|
||||
rsx! {
|
||||
h1 { "FATAL ERROR: {err}" }
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
result
|
||||
}*/
|
||||
|
||||
#[component]
|
||||
pub fn PostElement(post: Post) -> Element {
|
||||
rsx! {
|
||||
for block in post.blocks {
|
||||
BlockElement { block }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn BlockElement(block: Block) -> Element {
|
||||
match block {
|
||||
Block::Text { text } => rsx! {
|
||||
p { "{text}" }
|
||||
},
|
||||
Block::Gallery { images } => rsx! {
|
||||
for image in images {
|
||||
img { src: "{image.src}", alt: "{image.caption}" }
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
|
@ -1 +1,57 @@
|
|||
use dioxus::prelude::*;
|
||||
|
||||
use crate::domain::entities::{
|
||||
cursor::Paginated,
|
||||
site::{Page, Post, SiteInfo},
|
||||
};
|
||||
|
||||
#[server]
|
||||
pub async fn get_site_info() -> Result<SiteInfo, ServerFnError> {
|
||||
use crate::outbound::services::site::SiteService;
|
||||
let FromContext::<SiteService>(service) = extract().await?;
|
||||
|
||||
let headers = server_context().request_parts().headers.clone();
|
||||
let domain = match headers.get("host").and_then(|h| h.to_str().ok()) {
|
||||
Some(host) => host
|
||||
.split_once(':')
|
||||
.map(|(domain, _)| domain)
|
||||
.unwrap_or(host),
|
||||
None => "",
|
||||
}
|
||||
.to_string();
|
||||
|
||||
let site = service.get_site(domain.as_str()).await?;
|
||||
Ok(site)
|
||||
}
|
||||
|
||||
#[server]
|
||||
pub async fn get_page(domain: String, page: String) -> Result<Page, ServerFnError> {
|
||||
use crate::outbound::services::site::SiteService;
|
||||
let FromContext::<SiteService>(service) = extract().await?;
|
||||
|
||||
Ok(service.get_page(&domain, &page).await?)
|
||||
}
|
||||
|
||||
/*
|
||||
#[server]
|
||||
pub async fn get_posts(
|
||||
domain: String,
|
||||
collection_id: String,
|
||||
) -> Result<Paginated<Post, String>, ServerFnError> {
|
||||
use crate::outbound::services::site::SiteService;
|
||||
let FromContext::<SiteService>(service) = extract().await?;
|
||||
|
||||
Ok(service.get_posts(&domain, &collection_id).await?)
|
||||
}*/
|
||||
|
||||
#[server]
|
||||
pub async fn get_post(
|
||||
domain: String,
|
||||
collection_id: String,
|
||||
id: String,
|
||||
) -> Result<Post, ServerFnError> {
|
||||
use crate::outbound::services::site::SiteService;
|
||||
let FromContext::<SiteService>(service) = extract().await?;
|
||||
|
||||
Ok(service.get_post(&domain, &collection_id, &id).await?)
|
||||
}
|
||||
|
|
23
src/main.rs
23
src/main.rs
|
@ -21,7 +21,28 @@ fn main() {
|
|||
builder = builder
|
||||
.with_context_provider(
|
||||
|| {
|
||||
Box::new(SiteService::new(InMemoryStore::new()))
|
||||
let mut store = InMemoryStore::new();
|
||||
store.add_site(outbound::repository::site::SiteMetadata {
|
||||
domain: "localhost".to_string(),
|
||||
title: "Test site".to_string(),
|
||||
});
|
||||
store.add_page("localhost", domain::entities::site::Page{
|
||||
info: domain::entities::site::PageInfo {
|
||||
title: "Home".to_string(),
|
||||
name: "/".to_string(),
|
||||
order: 0,
|
||||
},
|
||||
content: domain::entities::site::PageContent::Single {
|
||||
content: domain::entities::site::Post {
|
||||
blocks: vec![{
|
||||
domain::entities::site::Block::Text {
|
||||
text: "Hello, world!".to_string(),
|
||||
}
|
||||
}],
|
||||
},
|
||||
}
|
||||
});
|
||||
Box::new(SiteService::new(store))
|
||||
}
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,16 +1,19 @@
|
|||
use std::collections::HashMap;
|
||||
|
||||
use async_trait::async_trait;
|
||||
|
||||
use crate::{
|
||||
domain::entities::site::{Page, PageInfo, SiteMetadata},
|
||||
domain::entities::site::{Page, PageInfo, Post},
|
||||
outbound::repository::{
|
||||
self,
|
||||
site::{Result, SiteRepository},
|
||||
site::{Result, SiteMetadata, SiteRepository},
|
||||
},
|
||||
};
|
||||
|
||||
pub struct InMemoryStore {
|
||||
sites: HashMap<String, SiteMetadata>,
|
||||
pages: HashMap<(String, String), Page>,
|
||||
posts: HashMap<(String, String), HashMap<String, Post>>,
|
||||
}
|
||||
|
||||
impl InMemoryStore {
|
||||
|
@ -18,6 +21,7 @@ impl InMemoryStore {
|
|||
InMemoryStore {
|
||||
sites: HashMap::new(),
|
||||
pages: HashMap::new(),
|
||||
posts: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -30,6 +34,13 @@ impl InMemoryStore {
|
|||
.insert((site.to_string(), page.info.name.clone()), page);
|
||||
}
|
||||
|
||||
pub fn add_post(&mut self, site: &str, collection_id: &str, post_id: &str, post: Post) {
|
||||
self.posts
|
||||
.entry((site.to_string(), collection_id.to_string()))
|
||||
.or_insert_with(HashMap::new)
|
||||
.insert(post_id.to_string(), post);
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub fn with_test_data() -> InMemoryStore {
|
||||
use crate::domain::entities::site::{Block, PageContent, Post};
|
||||
|
@ -75,10 +86,22 @@ impl InMemoryStore {
|
|||
},
|
||||
);
|
||||
|
||||
store.add_post(
|
||||
"example.com",
|
||||
"home_posts",
|
||||
"post_id",
|
||||
Post {
|
||||
blocks: vec![Block::Text {
|
||||
text: "Hello, world!".to_string(),
|
||||
}],
|
||||
},
|
||||
);
|
||||
|
||||
store
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl SiteRepository for InMemoryStore {
|
||||
async fn get_site_by_domain(&self, domain: &str) -> Result<SiteMetadata> {
|
||||
self.sites
|
||||
|
@ -106,13 +129,24 @@ impl SiteRepository for InMemoryStore {
|
|||
.ok_or(repository::site::Error::NotFound)
|
||||
.cloned()
|
||||
}
|
||||
|
||||
async fn get_post(&self, domain: &str, collection_id: &str, post_id: &str) -> Result<Post> {
|
||||
self.posts
|
||||
.get(&(domain.to_string(), collection_id.to_string()))
|
||||
.and_then(|posts| posts.get(post_id))
|
||||
.cloned()
|
||||
.ok_or(repository::site::Error::NotFound)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::{
|
||||
domain::entities::site::{Block, Page, PageContent, PageInfo, Post, SiteMetadata},
|
||||
outbound::repository::{self, site::SiteRepository},
|
||||
domain::entities::site::{Block, Page, PageContent, PageInfo, Post},
|
||||
outbound::repository::{
|
||||
self,
|
||||
site::{SiteMetadata, SiteRepository},
|
||||
},
|
||||
};
|
||||
|
||||
use super::InMemoryStore;
|
||||
|
@ -185,10 +219,6 @@ mod tests {
|
|||
#[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 {
|
||||
|
@ -207,10 +237,15 @@ mod tests {
|
|||
},
|
||||
);
|
||||
let page = store.get_page("example.com", "/").await.unwrap();
|
||||
let PageContent::Single { content } = page.content;
|
||||
let PageContent::Single { content } = page.content else {
|
||||
panic!("page content must be a single text block")
|
||||
};
|
||||
let Post { blocks } = content;
|
||||
assert_eq!(blocks.len(), 1);
|
||||
let Block::Text { text } = &blocks[0];
|
||||
|
||||
let Block::Text { text } = &blocks[0] else {
|
||||
panic!("page content must be a single text block")
|
||||
};
|
||||
assert_eq!(text, "Hello, world!");
|
||||
}
|
||||
|
||||
|
@ -224,4 +259,42 @@ mod tests {
|
|||
repository::site::Error::NotFound
|
||||
));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn get_post_works() {
|
||||
let mut store = InMemoryStore::new();
|
||||
store.add_post(
|
||||
"example.com",
|
||||
"home_posts",
|
||||
"test_id",
|
||||
Post {
|
||||
blocks: vec![Block::Text {
|
||||
text: "Hello, world!".to_string(),
|
||||
}],
|
||||
},
|
||||
);
|
||||
|
||||
let post = store
|
||||
.get_post("example.com", "home_posts", "test_id")
|
||||
.await
|
||||
.unwrap();
|
||||
let Post { blocks } = post;
|
||||
assert_eq!(blocks.len(), 1);
|
||||
|
||||
let Block::Text { text } = &blocks[0] else {
|
||||
panic!("page content must be a single text block")
|
||||
};
|
||||
assert_eq!(text, "Hello, world!");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn get_post_for_nonexistent_fails() {
|
||||
let store = InMemoryStore::new();
|
||||
let result = store.get_post("example.com", "home_posts", "test_id").await;
|
||||
assert!(result.is_err());
|
||||
assert!(matches!(
|
||||
result.err().unwrap(),
|
||||
repository::site::Error::NotFound
|
||||
));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,11 +1,20 @@
|
|||
use async_trait::async_trait;
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::domain::entities::site::{Page, PageInfo, SiteMetadata};
|
||||
use crate::domain::entities::site::{Page, PageInfo, Post};
|
||||
|
||||
pub trait SiteRepository {
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SiteMetadata {
|
||||
pub domain: String,
|
||||
pub title: String,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
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 get_page(&self, domain: &str, name: &str) -> Result<Page>;
|
||||
async fn get_post(&self, domain: &str, collection_id: &str, id: &str) -> Result<Post>;
|
||||
}
|
||||
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
|
|
|
@ -1,17 +1,22 @@
|
|||
use std::sync::Arc;
|
||||
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::{
|
||||
domain::entities::site::{Page, SiteInfo},
|
||||
domain::entities::site::{Page, Post, SiteInfo},
|
||||
outbound::repository::{self, site::SiteRepository},
|
||||
};
|
||||
|
||||
pub struct SiteService<SiteRepo: SiteRepository> {
|
||||
site_repository: SiteRepo,
|
||||
#[derive(Clone)]
|
||||
pub struct SiteService {
|
||||
site_repository: Arc<dyn SiteRepository>,
|
||||
}
|
||||
|
||||
impl<SiteRepo: SiteRepository> SiteService<SiteRepo> {
|
||||
pub fn new(site_repository: SiteRepo) -> Self {
|
||||
Self { site_repository }
|
||||
impl SiteService {
|
||||
pub fn new(site_repository: impl SiteRepository) -> Self {
|
||||
Self {
|
||||
site_repository: Arc::new(site_repository),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_site(&self, domain: &str) -> Result<SiteInfo> {
|
||||
|
@ -21,7 +26,11 @@ impl<SiteRepo: SiteRepository> SiteService<SiteRepo> {
|
|||
.get_pages_for_site(&info.domain)
|
||||
.await?;
|
||||
|
||||
Ok(SiteInfo { info, pages })
|
||||
Ok(SiteInfo {
|
||||
title: info.title,
|
||||
domain: info.domain,
|
||||
pages,
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn get_page(&self, domain: &str, name: &str) -> Result<Page> {
|
||||
|
@ -30,6 +39,16 @@ impl<SiteRepo: SiteRepository> SiteService<SiteRepo> {
|
|||
|
||||
Ok(page)
|
||||
}
|
||||
|
||||
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)
|
||||
.await?;
|
||||
|
||||
Ok(post)
|
||||
}
|
||||
}
|
||||
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
|
@ -64,11 +83,19 @@ mod tests {
|
|||
async fn gets_site_info() {
|
||||
let service = SiteService::new(InMemoryStore::with_test_data());
|
||||
let info = service.get_site("example.com").await.unwrap();
|
||||
assert_eq!(info.info.domain, "example.com");
|
||||
assert_eq!(info.info.title, "Test site");
|
||||
assert_eq!(info.domain, "example.com");
|
||||
assert_eq!(info.title, "Test site");
|
||||
assert_eq!(info.pages.len(), 2);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn gets_nonexistent_site() {
|
||||
let service = SiteService::new(InMemoryStore::with_test_data());
|
||||
let result = service.get_site("nonexistent.com").await;
|
||||
assert!(result.is_err());
|
||||
assert!(matches!(result.err().unwrap(), Error::NotFound));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn gets_page_data() {
|
||||
let service = SiteService::new(InMemoryStore::with_test_data());
|
||||
|
@ -80,9 +107,15 @@ mod tests {
|
|||
// page content must be a single text block
|
||||
let PageContent::Single {
|
||||
content: page_content,
|
||||
} = page.content;
|
||||
} = page.content
|
||||
else {
|
||||
panic!("page content must be a single text block");
|
||||
};
|
||||
assert_eq!(page_content.blocks.len(), 1);
|
||||
let Block::Text { text } = &page_content.blocks[0];
|
||||
|
||||
let Block::Text { text } = &page_content.blocks[0] else {
|
||||
panic!("page content must be a single text block");
|
||||
};
|
||||
assert_eq!(text, "Hello, world!");
|
||||
}
|
||||
|
||||
|
@ -93,4 +126,29 @@ mod tests {
|
|||
assert!(result.is_err());
|
||||
assert!(matches!(result.err().unwrap(), Error::NotFound));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn gets_a_single_post() {
|
||||
let service = SiteService::new(InMemoryStore::with_test_data());
|
||||
let post = service
|
||||
.get_post("example.com", "home_posts", "post_id")
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(post.blocks.len(), 1);
|
||||
|
||||
let Block::Text { text } = &post.blocks[0] else {
|
||||
panic!("page content must be a single text block");
|
||||
};
|
||||
assert_eq!(text, "Hello, world!");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn gets_nonexistent_post() {
|
||||
let service = SiteService::new(InMemoryStore::with_test_data());
|
||||
let result = service
|
||||
.get_post("example.com", "home_posts", "nonexistent")
|
||||
.await;
|
||||
assert!(result.is_err());
|
||||
assert!(matches!(result.err().unwrap(), Error::NotFound));
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue