feat: v2 rewrite — hexagonal arch, ActivityPub federation, NATS, deployment-ready #1
@@ -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,
|
||||
|
||||
@@ -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(),
|
||||
|
||||
Reference in New Issue
Block a user