feat: implement merge readiness plan to close gaps between v2 and v1
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 5m8s
test / unit (pull_request) Successful in 16m18s
test / integration (pull_request) Failing after 16m59s
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 5m8s
test / unit (pull_request) Successful in 16m18s
test / integration (pull_request) Failing after 16m59s
- Task 1: Fix feed response hydration by adding `to_thought_response` helper and updating feed handlers to return full `ThoughtResponse`. - Task 2: Wire follower/following REST routes for user feeds. - Task 3: Add user listing and count endpoints, including `GET /users` and `GET /users/count`. - Task 4: Implement popular tags feature with `GET /tags/popular`. - Task 5: Enhance configuration with HOST, CORS_ORIGINS, and optional rate limiting using tower-governor.
This commit is contained in:
@@ -74,20 +74,20 @@ impl EventPayload {
|
||||
/// Returns the NATS subject for this event.
|
||||
pub fn subject(&self) -> &'static str {
|
||||
match self {
|
||||
Self::ThoughtCreated { .. } => "thoughts.created",
|
||||
Self::ThoughtDeleted { .. } => "thoughts.deleted",
|
||||
Self::ThoughtUpdated { .. } => "thoughts.updated",
|
||||
Self::LikeAdded { .. } => "likes.added",
|
||||
Self::LikeRemoved { .. } => "likes.removed",
|
||||
Self::BoostAdded { .. } => "boosts.added",
|
||||
Self::BoostRemoved { .. } => "boosts.removed",
|
||||
Self::ThoughtCreated { .. } => "thoughts.created",
|
||||
Self::ThoughtDeleted { .. } => "thoughts.deleted",
|
||||
Self::ThoughtUpdated { .. } => "thoughts.updated",
|
||||
Self::LikeAdded { .. } => "likes.added",
|
||||
Self::LikeRemoved { .. } => "likes.removed",
|
||||
Self::BoostAdded { .. } => "boosts.added",
|
||||
Self::BoostRemoved { .. } => "boosts.removed",
|
||||
Self::FollowRequested { .. } => "follows.requested",
|
||||
Self::FollowAccepted { .. } => "follows.accepted",
|
||||
Self::FollowRejected { .. } => "follows.rejected",
|
||||
Self::Unfollowed { .. } => "follows.removed",
|
||||
Self::UserBlocked { .. } => "users.blocked",
|
||||
Self::UserUnblocked { .. } => "users.unblocked",
|
||||
Self::UserRegistered { .. } => "users.registered",
|
||||
Self::FollowAccepted { .. } => "follows.accepted",
|
||||
Self::FollowRejected { .. } => "follows.rejected",
|
||||
Self::Unfollowed { .. } => "follows.removed",
|
||||
Self::UserBlocked { .. } => "users.blocked",
|
||||
Self::UserUnblocked { .. } => "users.unblocked",
|
||||
Self::UserRegistered { .. } => "users.registered",
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -97,46 +97,102 @@ impl EventPayload {
|
||||
impl From<&DomainEvent> for EventPayload {
|
||||
fn from(e: &DomainEvent) -> Self {
|
||||
match e {
|
||||
DomainEvent::ThoughtCreated { thought_id, user_id, in_reply_to_id } => Self::ThoughtCreated {
|
||||
DomainEvent::ThoughtCreated {
|
||||
thought_id,
|
||||
user_id,
|
||||
in_reply_to_id,
|
||||
} => Self::ThoughtCreated {
|
||||
thought_id: thought_id.to_string(),
|
||||
user_id: user_id.to_string(),
|
||||
in_reply_to_id: in_reply_to_id.as_ref().map(|x| x.to_string()),
|
||||
},
|
||||
DomainEvent::ThoughtDeleted { thought_id, user_id } => Self::ThoughtDeleted {
|
||||
thought_id: thought_id.to_string(), user_id: user_id.to_string(),
|
||||
DomainEvent::ThoughtDeleted {
|
||||
thought_id,
|
||||
user_id,
|
||||
} => Self::ThoughtDeleted {
|
||||
thought_id: thought_id.to_string(),
|
||||
user_id: user_id.to_string(),
|
||||
},
|
||||
DomainEvent::ThoughtUpdated { thought_id, user_id } => Self::ThoughtUpdated {
|
||||
thought_id: thought_id.to_string(), user_id: user_id.to_string(),
|
||||
DomainEvent::ThoughtUpdated {
|
||||
thought_id,
|
||||
user_id,
|
||||
} => Self::ThoughtUpdated {
|
||||
thought_id: thought_id.to_string(),
|
||||
user_id: user_id.to_string(),
|
||||
},
|
||||
DomainEvent::LikeAdded { like_id, user_id, thought_id } => Self::LikeAdded {
|
||||
like_id: like_id.to_string(), user_id: user_id.to_string(), thought_id: thought_id.to_string(),
|
||||
DomainEvent::LikeAdded {
|
||||
like_id,
|
||||
user_id,
|
||||
thought_id,
|
||||
} => Self::LikeAdded {
|
||||
like_id: like_id.to_string(),
|
||||
user_id: user_id.to_string(),
|
||||
thought_id: thought_id.to_string(),
|
||||
},
|
||||
DomainEvent::LikeRemoved { user_id, thought_id } => Self::LikeRemoved {
|
||||
user_id: user_id.to_string(), thought_id: thought_id.to_string(),
|
||||
DomainEvent::LikeRemoved {
|
||||
user_id,
|
||||
thought_id,
|
||||
} => Self::LikeRemoved {
|
||||
user_id: user_id.to_string(),
|
||||
thought_id: thought_id.to_string(),
|
||||
},
|
||||
DomainEvent::BoostAdded { boost_id, user_id, thought_id } => Self::BoostAdded {
|
||||
boost_id: boost_id.to_string(), user_id: user_id.to_string(), thought_id: thought_id.to_string(),
|
||||
DomainEvent::BoostAdded {
|
||||
boost_id,
|
||||
user_id,
|
||||
thought_id,
|
||||
} => Self::BoostAdded {
|
||||
boost_id: boost_id.to_string(),
|
||||
user_id: user_id.to_string(),
|
||||
thought_id: thought_id.to_string(),
|
||||
},
|
||||
DomainEvent::BoostRemoved { user_id, thought_id } => Self::BoostRemoved {
|
||||
user_id: user_id.to_string(), thought_id: thought_id.to_string(),
|
||||
DomainEvent::BoostRemoved {
|
||||
user_id,
|
||||
thought_id,
|
||||
} => Self::BoostRemoved {
|
||||
user_id: user_id.to_string(),
|
||||
thought_id: thought_id.to_string(),
|
||||
},
|
||||
DomainEvent::FollowRequested { follower_id, following_id } => Self::FollowRequested {
|
||||
follower_id: follower_id.to_string(), following_id: following_id.to_string(),
|
||||
DomainEvent::FollowRequested {
|
||||
follower_id,
|
||||
following_id,
|
||||
} => Self::FollowRequested {
|
||||
follower_id: follower_id.to_string(),
|
||||
following_id: following_id.to_string(),
|
||||
},
|
||||
DomainEvent::FollowAccepted { follower_id, following_id } => Self::FollowAccepted {
|
||||
follower_id: follower_id.to_string(), following_id: following_id.to_string(),
|
||||
DomainEvent::FollowAccepted {
|
||||
follower_id,
|
||||
following_id,
|
||||
} => Self::FollowAccepted {
|
||||
follower_id: follower_id.to_string(),
|
||||
following_id: following_id.to_string(),
|
||||
},
|
||||
DomainEvent::FollowRejected { follower_id, following_id } => Self::FollowRejected {
|
||||
follower_id: follower_id.to_string(), following_id: following_id.to_string(),
|
||||
DomainEvent::FollowRejected {
|
||||
follower_id,
|
||||
following_id,
|
||||
} => Self::FollowRejected {
|
||||
follower_id: follower_id.to_string(),
|
||||
following_id: following_id.to_string(),
|
||||
},
|
||||
DomainEvent::Unfollowed { follower_id, following_id } => Self::Unfollowed {
|
||||
follower_id: follower_id.to_string(), following_id: following_id.to_string(),
|
||||
DomainEvent::Unfollowed {
|
||||
follower_id,
|
||||
following_id,
|
||||
} => Self::Unfollowed {
|
||||
follower_id: follower_id.to_string(),
|
||||
following_id: following_id.to_string(),
|
||||
},
|
||||
DomainEvent::UserBlocked { blocker_id, blocked_id } => Self::UserBlocked {
|
||||
blocker_id: blocker_id.to_string(), blocked_id: blocked_id.to_string(),
|
||||
DomainEvent::UserBlocked {
|
||||
blocker_id,
|
||||
blocked_id,
|
||||
} => Self::UserBlocked {
|
||||
blocker_id: blocker_id.to_string(),
|
||||
blocked_id: blocked_id.to_string(),
|
||||
},
|
||||
DomainEvent::UserUnblocked { blocker_id, blocked_id } => Self::UserUnblocked {
|
||||
blocker_id: blocker_id.to_string(), blocked_id: blocked_id.to_string(),
|
||||
DomainEvent::UserUnblocked {
|
||||
blocker_id,
|
||||
blocked_id,
|
||||
} => Self::UserUnblocked {
|
||||
blocker_id: blocker_id.to_string(),
|
||||
blocked_id: blocked_id.to_string(),
|
||||
},
|
||||
DomainEvent::UserRegistered { user_id } => Self::UserRegistered {
|
||||
user_id: user_id.to_string(),
|
||||
@@ -157,60 +213,102 @@ impl TryFrom<EventPayload> for DomainEvent {
|
||||
|
||||
fn try_from(p: EventPayload) -> Result<Self, DomainError> {
|
||||
Ok(match p {
|
||||
EventPayload::ThoughtCreated { thought_id, user_id, in_reply_to_id } => DomainEvent::ThoughtCreated {
|
||||
EventPayload::ThoughtCreated {
|
||||
thought_id,
|
||||
user_id,
|
||||
in_reply_to_id,
|
||||
} => DomainEvent::ThoughtCreated {
|
||||
thought_id: ThoughtId::from_uuid(parse_uuid(&thought_id, "thought_id")?),
|
||||
user_id: UserId::from_uuid(parse_uuid(&user_id, "user_id")?),
|
||||
in_reply_to_id: in_reply_to_id
|
||||
.map(|s| parse_uuid(&s, "in_reply_to_id").map(ThoughtId::from_uuid))
|
||||
.transpose()?,
|
||||
},
|
||||
EventPayload::ThoughtDeleted { thought_id, user_id } => DomainEvent::ThoughtDeleted {
|
||||
EventPayload::ThoughtDeleted {
|
||||
thought_id,
|
||||
user_id,
|
||||
} => DomainEvent::ThoughtDeleted {
|
||||
thought_id: ThoughtId::from_uuid(parse_uuid(&thought_id, "thought_id")?),
|
||||
user_id: UserId::from_uuid(parse_uuid(&user_id, "user_id")?),
|
||||
},
|
||||
EventPayload::ThoughtUpdated { thought_id, user_id } => DomainEvent::ThoughtUpdated {
|
||||
EventPayload::ThoughtUpdated {
|
||||
thought_id,
|
||||
user_id,
|
||||
} => DomainEvent::ThoughtUpdated {
|
||||
thought_id: ThoughtId::from_uuid(parse_uuid(&thought_id, "thought_id")?),
|
||||
user_id: UserId::from_uuid(parse_uuid(&user_id, "user_id")?),
|
||||
},
|
||||
EventPayload::LikeAdded { like_id, user_id, thought_id } => DomainEvent::LikeAdded {
|
||||
EventPayload::LikeAdded {
|
||||
like_id,
|
||||
user_id,
|
||||
thought_id,
|
||||
} => DomainEvent::LikeAdded {
|
||||
like_id: LikeId::from_uuid(parse_uuid(&like_id, "like_id")?),
|
||||
user_id: UserId::from_uuid(parse_uuid(&user_id, "user_id")?),
|
||||
thought_id: ThoughtId::from_uuid(parse_uuid(&thought_id, "thought_id")?),
|
||||
},
|
||||
EventPayload::LikeRemoved { user_id, thought_id } => DomainEvent::LikeRemoved {
|
||||
EventPayload::LikeRemoved {
|
||||
user_id,
|
||||
thought_id,
|
||||
} => DomainEvent::LikeRemoved {
|
||||
user_id: UserId::from_uuid(parse_uuid(&user_id, "user_id")?),
|
||||
thought_id: ThoughtId::from_uuid(parse_uuid(&thought_id, "thought_id")?),
|
||||
},
|
||||
EventPayload::BoostAdded { boost_id, user_id, thought_id } => DomainEvent::BoostAdded {
|
||||
EventPayload::BoostAdded {
|
||||
boost_id,
|
||||
user_id,
|
||||
thought_id,
|
||||
} => DomainEvent::BoostAdded {
|
||||
boost_id: BoostId::from_uuid(parse_uuid(&boost_id, "boost_id")?),
|
||||
user_id: UserId::from_uuid(parse_uuid(&user_id, "user_id")?),
|
||||
thought_id: ThoughtId::from_uuid(parse_uuid(&thought_id, "thought_id")?),
|
||||
},
|
||||
EventPayload::BoostRemoved { user_id, thought_id } => DomainEvent::BoostRemoved {
|
||||
EventPayload::BoostRemoved {
|
||||
user_id,
|
||||
thought_id,
|
||||
} => DomainEvent::BoostRemoved {
|
||||
user_id: UserId::from_uuid(parse_uuid(&user_id, "user_id")?),
|
||||
thought_id: ThoughtId::from_uuid(parse_uuid(&thought_id, "thought_id")?),
|
||||
},
|
||||
EventPayload::FollowRequested { follower_id, following_id } => DomainEvent::FollowRequested {
|
||||
EventPayload::FollowRequested {
|
||||
follower_id,
|
||||
following_id,
|
||||
} => DomainEvent::FollowRequested {
|
||||
follower_id: UserId::from_uuid(parse_uuid(&follower_id, "follower_id")?),
|
||||
following_id: UserId::from_uuid(parse_uuid(&following_id, "following_id")?),
|
||||
},
|
||||
EventPayload::FollowAccepted { follower_id, following_id } => DomainEvent::FollowAccepted {
|
||||
EventPayload::FollowAccepted {
|
||||
follower_id,
|
||||
following_id,
|
||||
} => DomainEvent::FollowAccepted {
|
||||
follower_id: UserId::from_uuid(parse_uuid(&follower_id, "follower_id")?),
|
||||
following_id: UserId::from_uuid(parse_uuid(&following_id, "following_id")?),
|
||||
},
|
||||
EventPayload::FollowRejected { follower_id, following_id } => DomainEvent::FollowRejected {
|
||||
EventPayload::FollowRejected {
|
||||
follower_id,
|
||||
following_id,
|
||||
} => DomainEvent::FollowRejected {
|
||||
follower_id: UserId::from_uuid(parse_uuid(&follower_id, "follower_id")?),
|
||||
following_id: UserId::from_uuid(parse_uuid(&following_id, "following_id")?),
|
||||
},
|
||||
EventPayload::Unfollowed { follower_id, following_id } => DomainEvent::Unfollowed {
|
||||
EventPayload::Unfollowed {
|
||||
follower_id,
|
||||
following_id,
|
||||
} => DomainEvent::Unfollowed {
|
||||
follower_id: UserId::from_uuid(parse_uuid(&follower_id, "follower_id")?),
|
||||
following_id: UserId::from_uuid(parse_uuid(&following_id, "following_id")?),
|
||||
},
|
||||
EventPayload::UserBlocked { blocker_id, blocked_id } => DomainEvent::UserBlocked {
|
||||
EventPayload::UserBlocked {
|
||||
blocker_id,
|
||||
blocked_id,
|
||||
} => DomainEvent::UserBlocked {
|
||||
blocker_id: UserId::from_uuid(parse_uuid(&blocker_id, "blocker_id")?),
|
||||
blocked_id: UserId::from_uuid(parse_uuid(&blocked_id, "blocked_id")?),
|
||||
},
|
||||
EventPayload::UserUnblocked { blocker_id, blocked_id } => DomainEvent::UserUnblocked {
|
||||
EventPayload::UserUnblocked {
|
||||
blocker_id,
|
||||
blocked_id,
|
||||
} => DomainEvent::UserUnblocked {
|
||||
blocker_id: UserId::from_uuid(parse_uuid(&blocker_id, "blocker_id")?),
|
||||
blocked_id: UserId::from_uuid(parse_uuid(&blocked_id, "blocked_id")?),
|
||||
},
|
||||
@@ -240,22 +338,65 @@ mod tests {
|
||||
#[test]
|
||||
fn all_subjects_are_unique() {
|
||||
let samples: &[EventPayload] = &[
|
||||
EventPayload::ThoughtCreated { thought_id: "a".into(), user_id: "b".into(), in_reply_to_id: None },
|
||||
EventPayload::ThoughtDeleted { thought_id: "a".into(), user_id: "b".into() },
|
||||
EventPayload::ThoughtUpdated { thought_id: "a".into(), user_id: "b".into() },
|
||||
EventPayload::LikeAdded { like_id: "a".into(), user_id: "b".into(), thought_id: "c".into() },
|
||||
EventPayload::LikeRemoved { user_id: "b".into(), thought_id: "c".into() },
|
||||
EventPayload::BoostAdded { boost_id: "a".into(), user_id: "b".into(), thought_id: "c".into() },
|
||||
EventPayload::BoostRemoved { user_id: "b".into(), thought_id: "c".into() },
|
||||
EventPayload::FollowRequested { follower_id: "a".into(), following_id: "b".into() },
|
||||
EventPayload::FollowAccepted { follower_id: "a".into(), following_id: "b".into() },
|
||||
EventPayload::FollowRejected { follower_id: "a".into(), following_id: "b".into() },
|
||||
EventPayload::Unfollowed { follower_id: "a".into(), following_id: "b".into() },
|
||||
EventPayload::UserBlocked { blocker_id: "a".into(), blocked_id: "b".into() },
|
||||
EventPayload::ThoughtCreated {
|
||||
thought_id: "a".into(),
|
||||
user_id: "b".into(),
|
||||
in_reply_to_id: None,
|
||||
},
|
||||
EventPayload::ThoughtDeleted {
|
||||
thought_id: "a".into(),
|
||||
user_id: "b".into(),
|
||||
},
|
||||
EventPayload::ThoughtUpdated {
|
||||
thought_id: "a".into(),
|
||||
user_id: "b".into(),
|
||||
},
|
||||
EventPayload::LikeAdded {
|
||||
like_id: "a".into(),
|
||||
user_id: "b".into(),
|
||||
thought_id: "c".into(),
|
||||
},
|
||||
EventPayload::LikeRemoved {
|
||||
user_id: "b".into(),
|
||||
thought_id: "c".into(),
|
||||
},
|
||||
EventPayload::BoostAdded {
|
||||
boost_id: "a".into(),
|
||||
user_id: "b".into(),
|
||||
thought_id: "c".into(),
|
||||
},
|
||||
EventPayload::BoostRemoved {
|
||||
user_id: "b".into(),
|
||||
thought_id: "c".into(),
|
||||
},
|
||||
EventPayload::FollowRequested {
|
||||
follower_id: "a".into(),
|
||||
following_id: "b".into(),
|
||||
},
|
||||
EventPayload::FollowAccepted {
|
||||
follower_id: "a".into(),
|
||||
following_id: "b".into(),
|
||||
},
|
||||
EventPayload::FollowRejected {
|
||||
follower_id: "a".into(),
|
||||
following_id: "b".into(),
|
||||
},
|
||||
EventPayload::Unfollowed {
|
||||
follower_id: "a".into(),
|
||||
following_id: "b".into(),
|
||||
},
|
||||
EventPayload::UserBlocked {
|
||||
blocker_id: "a".into(),
|
||||
blocked_id: "b".into(),
|
||||
},
|
||||
];
|
||||
let mut subjects: Vec<&str> = samples.iter().map(|p| p.subject()).collect();
|
||||
subjects.sort();
|
||||
subjects.dedup();
|
||||
assert_eq!(subjects.len(), samples.len(), "each event must have a unique subject");
|
||||
assert_eq!(
|
||||
subjects.len(),
|
||||
samples.len(),
|
||||
"each event must have a unique subject"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user