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:
@@ -1,5 +1,9 @@
|
||||
use async_trait::async_trait;
|
||||
use domain::{errors::DomainError, events::{DomainEvent, EventEnvelope}, ports::{EventConsumer, EventPublisher}};
|
||||
use domain::{
|
||||
errors::DomainError,
|
||||
events::{DomainEvent, EventEnvelope},
|
||||
ports::{EventConsumer, EventPublisher},
|
||||
};
|
||||
use event_payload::EventPayload;
|
||||
use futures::stream::BoxStream;
|
||||
|
||||
@@ -31,8 +35,8 @@ impl<T: Transport> EventPublisher for EventPublisherAdapter<T> {
|
||||
async fn publish(&self, event: &DomainEvent) -> Result<(), DomainError> {
|
||||
let payload = EventPayload::from(event);
|
||||
let subject = payload.subject();
|
||||
let bytes = serde_json::to_vec(&payload)
|
||||
.map_err(|e| DomainError::Internal(e.to_string()))?;
|
||||
let bytes =
|
||||
serde_json::to_vec(&payload).map_err(|e| DomainError::Internal(e.to_string()))?;
|
||||
tracing::debug!(subject, "publishing event");
|
||||
self.transport.publish_bytes(subject, &bytes).await
|
||||
}
|
||||
@@ -44,7 +48,7 @@ impl<T: Transport> EventPublisher for EventPublisherAdapter<T> {
|
||||
pub struct RawMessage {
|
||||
pub subject: String,
|
||||
pub payload: Vec<u8>,
|
||||
pub ack: Box<dyn Fn() + Send + Sync>,
|
||||
pub ack: Box<dyn Fn() + Send + Sync>,
|
||||
pub nack: Box<dyn Fn() + Send + Sync>,
|
||||
}
|
||||
|
||||
@@ -60,7 +64,9 @@ pub struct EventConsumerAdapter<S: MessageSource> {
|
||||
}
|
||||
|
||||
impl<S: MessageSource> EventConsumerAdapter<S> {
|
||||
pub fn new(source: S) -> Self { Self { source } }
|
||||
pub fn new(source: S) -> Self {
|
||||
Self { source }
|
||||
}
|
||||
}
|
||||
|
||||
impl<S: MessageSource> EventConsumer for EventConsumerAdapter<S> {
|
||||
@@ -90,7 +96,7 @@ impl<S: MessageSource> EventConsumer for EventConsumerAdapter<S> {
|
||||
};
|
||||
Some(Ok(EventEnvelope {
|
||||
event,
|
||||
ack: msg.ack,
|
||||
ack: msg.ack,
|
||||
nack: msg.nack,
|
||||
}))
|
||||
}
|
||||
@@ -103,8 +109,8 @@ impl<S: MessageSource> EventConsumer for EventConsumerAdapter<S> {
|
||||
mod tests {
|
||||
use super::*;
|
||||
use async_trait::async_trait;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use domain::value_objects::{ThoughtId, UserId};
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
struct SpyTransport {
|
||||
calls: Arc<Mutex<Vec<(String, Vec<u8>)>>>,
|
||||
@@ -112,13 +118,21 @@ mod tests {
|
||||
impl SpyTransport {
|
||||
fn new() -> (Self, Arc<Mutex<Vec<(String, Vec<u8>)>>>) {
|
||||
let calls = Arc::new(Mutex::new(vec![]));
|
||||
(Self { calls: calls.clone() }, calls)
|
||||
(
|
||||
Self {
|
||||
calls: calls.clone(),
|
||||
},
|
||||
calls,
|
||||
)
|
||||
}
|
||||
}
|
||||
#[async_trait]
|
||||
impl Transport for SpyTransport {
|
||||
async fn publish_bytes(&self, subject: &str, bytes: &[u8]) -> Result<(), DomainError> {
|
||||
self.calls.lock().unwrap().push((subject.to_string(), bytes.to_vec()));
|
||||
self.calls
|
||||
.lock()
|
||||
.unwrap()
|
||||
.push((subject.to_string(), bytes.to_vec()));
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -127,11 +141,14 @@ mod tests {
|
||||
async fn thought_created_routes_to_correct_subject() {
|
||||
let (spy, calls) = SpyTransport::new();
|
||||
let publisher = EventPublisherAdapter::new(spy);
|
||||
publisher.publish(&DomainEvent::ThoughtCreated {
|
||||
thought_id: ThoughtId::new(),
|
||||
user_id: UserId::new(),
|
||||
in_reply_to_id: None,
|
||||
}).await.unwrap();
|
||||
publisher
|
||||
.publish(&DomainEvent::ThoughtCreated {
|
||||
thought_id: ThoughtId::new(),
|
||||
user_id: UserId::new(),
|
||||
in_reply_to_id: None,
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
let calls = calls.lock().unwrap();
|
||||
assert_eq!(calls.len(), 1);
|
||||
assert_eq!(calls[0].0, "thoughts.created");
|
||||
@@ -141,10 +158,13 @@ mod tests {
|
||||
async fn serialized_payload_is_valid_json() {
|
||||
let (spy, calls) = SpyTransport::new();
|
||||
let publisher = EventPublisherAdapter::new(spy);
|
||||
publisher.publish(&DomainEvent::UserBlocked {
|
||||
blocker_id: UserId::new(),
|
||||
blocked_id: UserId::new(),
|
||||
}).await.unwrap();
|
||||
publisher
|
||||
.publish(&DomainEvent::UserBlocked {
|
||||
blocker_id: UserId::new(),
|
||||
blocked_id: UserId::new(),
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
let bytes = calls.lock().unwrap()[0].1.clone();
|
||||
let json: serde_json::Value = serde_json::from_slice(&bytes).expect("valid JSON");
|
||||
assert_eq!(json["type"], "UserBlocked");
|
||||
@@ -163,14 +183,16 @@ mod tests {
|
||||
let payload = EventPayload::from(&event);
|
||||
let bytes = serde_json::to_vec(&payload).unwrap();
|
||||
|
||||
struct OneMessageSource { bytes: Vec<u8> }
|
||||
struct OneMessageSource {
|
||||
bytes: Vec<u8>,
|
||||
}
|
||||
#[async_trait::async_trait]
|
||||
impl MessageSource for OneMessageSource {
|
||||
fn messages(&self) -> futures::stream::BoxStream<'_, Result<RawMessage, DomainError>> {
|
||||
let msg = RawMessage {
|
||||
subject: "thoughts.created".to_string(),
|
||||
payload: self.bytes.clone(),
|
||||
ack: Box::new(|| {}),
|
||||
ack: Box::new(|| {}),
|
||||
nack: Box::new(|| {}),
|
||||
};
|
||||
Box::pin(futures::stream::once(async { Ok(msg) }))
|
||||
@@ -194,7 +216,7 @@ mod tests {
|
||||
let msg = RawMessage {
|
||||
subject: "bad".to_string(),
|
||||
payload: b"not valid json".to_vec(),
|
||||
ack: Box::new(|| {}),
|
||||
ack: Box::new(|| {}),
|
||||
nack: Box::new(|| {}),
|
||||
};
|
||||
Box::pin(futures::stream::once(async { Ok(msg) }))
|
||||
|
||||
Reference in New Issue
Block a user