feat: extract and save hashtags on thought creation
Some checks failed
lint / lint (push) Has been cancelled
test / unit (push) Has been cancelled
test / integration (push) Has been cancelled
lint / lint (pull_request) Failing after 9m18s
test / unit (pull_request) Successful in 16m4s
test / integration (pull_request) Failing after 17m16s
Some checks failed
lint / lint (push) Has been cancelled
test / unit (push) Has been cancelled
test / integration (push) Has been cancelled
lint / lint (pull_request) Failing after 9m18s
test / unit (pull_request) Successful in 16m4s
test / integration (pull_request) Failing after 17m16s
This commit is contained in:
@@ -2,10 +2,34 @@ use domain::{
|
|||||||
errors::DomainError,
|
errors::DomainError,
|
||||||
events::DomainEvent,
|
events::DomainEvent,
|
||||||
models::thought::{Thought, Visibility},
|
models::thought::{Thought, Visibility},
|
||||||
ports::{EventPublisher, ThoughtRepository, UserRepository},
|
ports::{EventPublisher, TagRepository, ThoughtRepository, UserRepository},
|
||||||
value_objects::{Content, ThoughtId, UserId},
|
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> {
|
fn require_owner(thought: &Thought, user_id: &UserId) -> Result<(), DomainError> {
|
||||||
if thought.user_id != *user_id {
|
if thought.user_id != *user_id {
|
||||||
return Err(DomainError::NotFound);
|
return Err(DomainError::NotFound);
|
||||||
@@ -28,6 +52,7 @@ pub struct CreateThoughtOutput {
|
|||||||
pub async fn create_thought(
|
pub async fn create_thought(
|
||||||
thoughts: &dyn ThoughtRepository,
|
thoughts: &dyn ThoughtRepository,
|
||||||
_users: &dyn UserRepository,
|
_users: &dyn UserRepository,
|
||||||
|
tags: &dyn TagRepository,
|
||||||
events: &dyn EventPublisher,
|
events: &dyn EventPublisher,
|
||||||
input: CreateThoughtInput,
|
input: CreateThoughtInput,
|
||||||
) -> Result<CreateThoughtOutput, DomainError> {
|
) -> Result<CreateThoughtOutput, DomainError> {
|
||||||
@@ -40,13 +65,21 @@ pub async fn create_thought(
|
|||||||
let thought = Thought::new_local(
|
let thought = Thought::new_local(
|
||||||
ThoughtId::new(),
|
ThoughtId::new(),
|
||||||
input.user_id,
|
input.user_id,
|
||||||
content,
|
content.clone(),
|
||||||
input.in_reply_to_id.clone(),
|
input.in_reply_to_id.clone(),
|
||||||
visibility,
|
visibility,
|
||||||
input.content_warning,
|
input.content_warning,
|
||||||
input.sensitive,
|
input.sensitive,
|
||||||
);
|
);
|
||||||
thoughts.save(&thought).await?;
|
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
|
events
|
||||||
.publish(&DomainEvent::ThoughtCreated {
|
.publish(&DomainEvent::ThoughtCreated {
|
||||||
thought_id: thought.id.clone(),
|
thought_id: thought.id.clone(),
|
||||||
@@ -149,7 +182,7 @@ mod tests {
|
|||||||
let store = TestStore::default();
|
let store = TestStore::default();
|
||||||
let u = user();
|
let u = user();
|
||||||
store.users.lock().unwrap().push(u.clone());
|
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
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(out.thought.content.as_str(), "hello");
|
assert_eq!(out.thought.content.as_str(), "hello");
|
||||||
@@ -161,7 +194,13 @@ mod tests {
|
|||||||
let store = TestStore::default();
|
let store = TestStore::default();
|
||||||
let u = user();
|
let u = user();
|
||||||
store.users.lock().unwrap().push(u.clone());
|
store.users.lock().unwrap().push(u.clone());
|
||||||
let out = create_thought(&store, &store, &NoOpEventPublisher, input(u.id.clone()))
|
let out = create_thought(
|
||||||
|
&store,
|
||||||
|
&store,
|
||||||
|
&store,
|
||||||
|
&NoOpEventPublisher,
|
||||||
|
input(u.id.clone()),
|
||||||
|
)
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
delete_thought(&store, &NoOpEventPublisher, &out.thought.id, &u.id)
|
delete_thought(&store, &NoOpEventPublisher, &out.thought.id, &u.id)
|
||||||
@@ -185,7 +224,13 @@ mod tests {
|
|||||||
.lock()
|
.lock()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.extend([alice.clone(), bob.clone()]);
|
.extend([alice.clone(), bob.clone()]);
|
||||||
let out = create_thought(&store, &store, &NoOpEventPublisher, input(alice.id.clone()))
|
let out = create_thought(
|
||||||
|
&store,
|
||||||
|
&store,
|
||||||
|
&store,
|
||||||
|
&NoOpEventPublisher,
|
||||||
|
input(alice.id.clone()),
|
||||||
|
)
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let err = delete_thought(&store, &NoOpEventPublisher, &out.thought.id, &bob.id)
|
let err = delete_thought(&store, &NoOpEventPublisher, &out.thought.id, &bob.id)
|
||||||
@@ -199,7 +244,7 @@ mod tests {
|
|||||||
let store = TestStore::default();
|
let store = TestStore::default();
|
||||||
let alice = user();
|
let alice = user();
|
||||||
store.users.lock().unwrap().push(alice.clone());
|
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
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let tid = out.thought.id.clone();
|
let tid = out.thought.id.clone();
|
||||||
@@ -229,12 +274,19 @@ mod tests {
|
|||||||
let store = TestStore::default();
|
let store = TestStore::default();
|
||||||
let alice = user();
|
let alice = user();
|
||||||
store.users.lock().unwrap().push(alice.clone());
|
store.users.lock().unwrap().push(alice.clone());
|
||||||
let original = create_thought(&store, &store, &NoOpEventPublisher, input(alice.id.clone()))
|
let original = create_thought(
|
||||||
|
&store,
|
||||||
|
&store,
|
||||||
|
&store,
|
||||||
|
&NoOpEventPublisher,
|
||||||
|
input(alice.id.clone()),
|
||||||
|
)
|
||||||
.await
|
.await
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.thought;
|
.thought;
|
||||||
|
|
||||||
create_thought(
|
create_thought(
|
||||||
|
&store,
|
||||||
&store,
|
&store,
|
||||||
&store,
|
&store,
|
||||||
&NoOpEventPublisher,
|
&NoOpEventPublisher,
|
||||||
|
|||||||
@@ -64,6 +64,7 @@ pub async fn post_thought(
|
|||||||
let out = create_thought(
|
let out = create_thought(
|
||||||
&*s.thoughts,
|
&*s.thoughts,
|
||||||
&*s.users,
|
&*s.users,
|
||||||
|
&*s.tags,
|
||||||
&*s.events,
|
&*s.events,
|
||||||
CreateThoughtInput {
|
CreateThoughtInput {
|
||||||
user_id: uid.clone(),
|
user_id: uid.clone(),
|
||||||
|
|||||||
Reference in New Issue
Block a user