feat: v2 rewrite — hexagonal arch, ActivityPub federation, NATS, deployment-ready #1

Merged
GKaszewski merged 334 commits from v2 into master 2026-05-16 09:42:43 +00:00
2 changed files with 67 additions and 14 deletions
Showing only changes of commit 24bfda8458 - Show all commits

View File

@@ -2,10 +2,34 @@ use domain::{
errors::DomainError,
events::DomainEvent,
models::thought::{Thought, Visibility},
ports::{EventPublisher, ThoughtRepository, UserRepository},
ports::{EventPublisher, TagRepository, ThoughtRepository, UserRepository},
value_objects::{Content, ThoughtId, UserId},
};
fn extract_hashtags(content: &str) -> Vec<String> {
let mut tags = Vec::new();
let mut chars = content.char_indices().peekable();
while let Some((_, c)) = chars.next() {
if c == '#'
&& chars
.peek()
.map(|(_, nc)| nc.is_alphanumeric())
.unwrap_or(false)
{
let tag: String = chars
.by_ref()
.take_while(|(_, nc)| nc.is_alphanumeric() || *nc == '_')
.map(|(_, nc)| nc)
.collect();
if !tag.is_empty() {
tags.push(tag.to_lowercase());
}
}
}
tags.dedup();
tags
}
fn require_owner(thought: &Thought, user_id: &UserId) -> Result<(), DomainError> {
if thought.user_id != *user_id {
return Err(DomainError::NotFound);
@@ -28,6 +52,7 @@ pub struct CreateThoughtOutput {
pub async fn create_thought(
thoughts: &dyn ThoughtRepository,
_users: &dyn UserRepository,
tags: &dyn TagRepository,
events: &dyn EventPublisher,
input: CreateThoughtInput,
) -> Result<CreateThoughtOutput, DomainError> {
@@ -40,13 +65,21 @@ pub async fn create_thought(
let thought = Thought::new_local(
ThoughtId::new(),
input.user_id,
content,
content.clone(),
input.in_reply_to_id.clone(),
visibility,
input.content_warning,
input.sensitive,
);
thoughts.save(&thought).await?;
// Extract and attach hashtags from content.
for tag_name in extract_hashtags(content.as_str()) {
if let Ok(tag) = tags.find_or_create(&tag_name).await {
let _ = tags.attach_to_thought(&thought.id, tag.id).await;
}
}
events
.publish(&DomainEvent::ThoughtCreated {
thought_id: thought.id.clone(),
@@ -149,7 +182,7 @@ mod tests {
let store = TestStore::default();
let u = user();
store.users.lock().unwrap().push(u.clone());
let out = create_thought(&store, &store, &store, input(u.id.clone()))
let out = create_thought(&store, &store, &store, &store, input(u.id.clone()))
.await
.unwrap();
assert_eq!(out.thought.content.as_str(), "hello");
@@ -161,9 +194,15 @@ mod tests {
let store = TestStore::default();
let u = user();
store.users.lock().unwrap().push(u.clone());
let out = create_thought(&store, &store, &NoOpEventPublisher, input(u.id.clone()))
.await
.unwrap();
let out = create_thought(
&store,
&store,
&store,
&NoOpEventPublisher,
input(u.id.clone()),
)
.await
.unwrap();
delete_thought(&store, &NoOpEventPublisher, &out.thought.id, &u.id)
.await
.unwrap();
@@ -185,9 +224,15 @@ mod tests {
.lock()
.unwrap()
.extend([alice.clone(), bob.clone()]);
let out = create_thought(&store, &store, &NoOpEventPublisher, input(alice.id.clone()))
.await
.unwrap();
let out = create_thought(
&store,
&store,
&store,
&NoOpEventPublisher,
input(alice.id.clone()),
)
.await
.unwrap();
let err = delete_thought(&store, &NoOpEventPublisher, &out.thought.id, &bob.id)
.await
.unwrap_err();
@@ -199,7 +244,7 @@ mod tests {
let store = TestStore::default();
let alice = user();
store.users.lock().unwrap().push(alice.clone());
let out = create_thought(&store, &store, &store, input(alice.id.clone()))
let out = create_thought(&store, &store, &store, &store, input(alice.id.clone()))
.await
.unwrap();
let tid = out.thought.id.clone();
@@ -229,12 +274,19 @@ mod tests {
let store = TestStore::default();
let alice = user();
store.users.lock().unwrap().push(alice.clone());
let original = create_thought(&store, &store, &NoOpEventPublisher, input(alice.id.clone()))
.await
.unwrap()
.thought;
let original = create_thought(
&store,
&store,
&store,
&NoOpEventPublisher,
input(alice.id.clone()),
)
.await
.unwrap()
.thought;
create_thought(
&store,
&store,
&store,
&NoOpEventPublisher,

View File

@@ -64,6 +64,7 @@ pub async fn post_thought(
let out = create_thought(
&*s.thoughts,
&*s.users,
&*s.tags,
&*s.events,
CreateThoughtInput {
user_id: uid.clone(),