diff --git a/crates/application/src/use_cases/thoughts.rs b/crates/application/src/use_cases/thoughts.rs index 9d2f3bc..38abdfe 100644 --- a/crates/application/src/use_cases/thoughts.rs +++ b/crates/application/src/use_cases/thoughts.rs @@ -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 { + 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 { @@ -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, diff --git a/crates/presentation/src/handlers/thoughts.rs b/crates/presentation/src/handlers/thoughts.rs index 0281fab..73085a7 100644 --- a/crates/presentation/src/handlers/thoughts.rs +++ b/crates/presentation/src/handlers/thoughts.rs @@ -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(),