fix: FollowTarget enum — actor_url no longer lies about holding a handle
FollowCommand.target is now FollowTarget (Identity|Handle) instead of SocialIdentity. actor_url field always holds a URL; handles go through FollowTarget::Handle. Adapter resolves handles explicitly. Type system prevents misuse in future commands.
This commit is contained in:
@@ -65,7 +65,7 @@ pub enum DomainEvent {
|
||||
},
|
||||
FollowRequested {
|
||||
follower: UserId,
|
||||
target: crate::value_objects::SocialIdentity,
|
||||
target: crate::value_objects::FollowTarget,
|
||||
},
|
||||
FollowAccepted {
|
||||
owner: UserId,
|
||||
|
||||
@@ -41,7 +41,7 @@ pub struct NoopSocialCommand;
|
||||
|
||||
#[async_trait]
|
||||
impl super::SocialCommand for NoopSocialCommand {
|
||||
async fn follow(&self, _: &UserId, _: &SocialIdentity) -> Result<(), DomainError> {
|
||||
async fn follow(&self, _: &UserId, _: &crate::value_objects::FollowTarget) -> Result<(), DomainError> {
|
||||
Ok(())
|
||||
}
|
||||
async fn unfollow(&self, _: &UserId, _: &SocialIdentity) -> Result<(), DomainError> {
|
||||
|
||||
@@ -7,14 +7,14 @@ use crate::{
|
||||
DiaryEntry, FederationFlags, RemoteActorInfo, RemoteGoalEntry, RemoteWatchlistEntry,
|
||||
WatchlistWithMovie,
|
||||
},
|
||||
value_objects::{MovieId, SocialActor, SocialIdentity, UserId},
|
||||
value_objects::{FollowTarget, MovieId, SocialActor, SocialIdentity, UserId},
|
||||
};
|
||||
|
||||
// ── Unified social ports (ADR-0002) ─────────────────────────────────────────
|
||||
|
||||
#[async_trait]
|
||||
pub trait SocialCommand: Send + Sync {
|
||||
async fn follow(&self, follower: &UserId, target: &SocialIdentity) -> Result<(), DomainError>;
|
||||
async fn follow(&self, follower: &UserId, target: &FollowTarget) -> Result<(), DomainError>;
|
||||
|
||||
async fn unfollow(&self, follower: &UserId, target: &SocialIdentity)
|
||||
-> Result<(), DomainError>;
|
||||
|
||||
@@ -893,8 +893,18 @@ impl InMemorySocialRepository {
|
||||
|
||||
#[async_trait]
|
||||
impl SocialCommand for InMemorySocialRepository {
|
||||
async fn follow(&self, follower: &UserId, target: &SocialIdentity) -> Result<(), DomainError> {
|
||||
if let SocialIdentity::Local(target_id) = target {
|
||||
async fn follow(
|
||||
&self,
|
||||
follower: &UserId,
|
||||
target: &crate::value_objects::FollowTarget,
|
||||
) -> Result<(), DomainError> {
|
||||
let identity = match target {
|
||||
crate::value_objects::FollowTarget::Identity(id) => id.clone(),
|
||||
crate::value_objects::FollowTarget::Handle(h) => SocialIdentity::Remote {
|
||||
actor_url: h.clone(),
|
||||
},
|
||||
};
|
||||
if let SocialIdentity::Local(target_id) = &identity {
|
||||
if follower == target_id {
|
||||
return Err(DomainError::ValidationError(
|
||||
"Cannot follow yourself".into(),
|
||||
@@ -904,11 +914,11 @@ impl SocialCommand for InMemorySocialRepository {
|
||||
let mut store = self.follows.lock().unwrap();
|
||||
let already = store
|
||||
.iter()
|
||||
.any(|(f, t, _)| *f == follower.value() && t == target);
|
||||
.any(|(f, t, _)| *f == follower.value() && *t == identity);
|
||||
if already {
|
||||
return Err(DomainError::ValidationError("Already following".into()));
|
||||
}
|
||||
store.push((follower.value(), target.clone(), FollowState::Pending));
|
||||
store.push((follower.value(), identity, FollowState::Pending));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use super::*;
|
||||
use crate::value_objects::{SocialIdentity, UserId};
|
||||
use crate::value_objects::{FollowTarget, SocialIdentity, UserId};
|
||||
|
||||
#[test]
|
||||
fn follow_accepted_matches() {
|
||||
@@ -22,17 +22,33 @@ fn follow_accepted_matches() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn follow_requested_local() {
|
||||
fn follow_requested_with_identity() {
|
||||
let follower = UserId::from_uuid(uuid::Uuid::new_v4());
|
||||
let target = UserId::from_uuid(uuid::Uuid::new_v4());
|
||||
let event = DomainEvent::FollowRequested {
|
||||
follower: follower.clone(),
|
||||
target: SocialIdentity::Local(target.clone()),
|
||||
target: FollowTarget::Identity(SocialIdentity::Local(target.clone())),
|
||||
};
|
||||
assert!(matches!(
|
||||
event,
|
||||
DomainEvent::FollowRequested {
|
||||
target: SocialIdentity::Local(_),
|
||||
target: FollowTarget::Identity(SocialIdentity::Local(_)),
|
||||
..
|
||||
}
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn follow_requested_with_handle() {
|
||||
let follower = UserId::from_uuid(uuid::Uuid::new_v4());
|
||||
let event = DomainEvent::FollowRequested {
|
||||
follower: follower.clone(),
|
||||
target: FollowTarget::Handle("@alice@remote.example".into()),
|
||||
};
|
||||
assert!(matches!(
|
||||
event,
|
||||
DomainEvent::FollowRequested {
|
||||
target: FollowTarget::Handle(_),
|
||||
..
|
||||
}
|
||||
));
|
||||
|
||||
@@ -16,6 +16,12 @@ impl SocialIdentity {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub enum FollowTarget {
|
||||
Identity(SocialIdentity),
|
||||
Handle(String),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct SocialActor {
|
||||
pub identity: SocialIdentity,
|
||||
|
||||
Reference in New Issue
Block a user