426a121f23b76660d4e726c990a4ce183c1c0384 / frogue / src / activitypub / types.rs
Raw
//! Hand-rolled ActivityPub vocabulary types.
//!
//! No `activitystreams` crates (unmaintained); hand-rolled fits the codebase.
//! Fields are snake_case in Rust, renamed to camelCase for JSON.
use serde::{Deserialize, Serialize};
/// The ActivityStreams and Security contexts.
pub const CONTEXT: [&str; 2] = [
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1",
];
/// The ForgeFed context including ActivityStreams, Security, and ForgeFed.
pub const FORGEFED_CONTEXT: [&str; 3] = [
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1",
"https://forgefed.org/ns",
];
/// An ActivityPub actor (Person for users, Repository for repositories).
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub struct Actor {
#[serde(rename = "@context")]
pub context: Vec<&'static str>,
pub id: String,
#[serde(rename = "type")]
pub kind: &'static str,
pub preferred_username: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub summary: Option<String>,
pub inbox: String,
pub outbox: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub followers: Option<String>,
pub public_key: PublicKeyDoc,
/// ForgeFed: owner's Person actor URL (for Repository actors).
#[serde(skip_serializing_if = "Option::is_none")]
pub attributed_to: Option<String>,
/// ForgeFed: HTTP clone URL (for Repository actors).
#[serde(skip_serializing_if = "Option::is_none")]
pub clone_uri: Option<String>,
/// ForgeFed: array of push URLs - HTTP and SSH (for Repository actors).
#[serde(skip_serializing_if = "Option::is_none")]
pub push_uri: Option<Vec<String>>,
/// ForgeFed: creation timestamp in RFC3339 format (for Repository actors).
#[serde(skip_serializing_if = "Option::is_none")]
pub published: Option<String>,
/// ForgeFed: ticket tracker actor URL (self-referential for repos that track their own tickets).
#[serde(skip_serializing_if = "Option::is_none")]
pub tickets_tracked_by: Option<String>,
/// ForgeFed: patch tracker actor URL (self-referential for repos that track their own patches).
#[serde(skip_serializing_if = "Option::is_none")]
pub send_patches_to: Option<String>,
/// ForgeFed: repositories whose tickets this actor tracks (bidirectional back-reference).
#[serde(skip_serializing_if = "Option::is_none")]
pub tracks_tickets_for: Option<Vec<String>>,
/// ForgeFed: repositories whose patches this actor tracks (bidirectional back-reference).
#[serde(skip_serializing_if = "Option::is_none")]
pub tracks_patches_for: Option<Vec<String>>,
}
/// A public key document, embedded in an actor.
#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PublicKeyDoc {
pub id: String,
pub owner: String,
pub public_key_pem: String,
}
/// An ActivityPub Note.
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub struct Note {
#[serde(rename = "@context")]
pub context: Vec<&'static str>,
pub id: String,
#[serde(rename = "type")]
pub kind: &'static str,
pub attributed_to: String,
pub content: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub media_type: Option<String>,
pub url: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub in_reply_to: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub source: Option<NoteSource>,
pub published: String,
pub to: Vec<String>,
pub cc: Vec<String>,
}
/// Source content for a Note (original markdown before rendering to HTML).
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub struct NoteSource {
pub media_type: String,
pub content: String,
}
/// A Create activity wrapping a Note.
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub struct Create {
#[serde(rename = "@context")]
pub context: Vec<&'static str>,
pub id: String,
#[serde(rename = "type")]
pub kind: &'static str,
pub actor: String,
pub object: Note,
pub published: String,
pub to: Vec<String>,
pub cc: Vec<String>,
}
/// An Accept activity (used for Follow acceptance and ForgeFed responses).
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub struct Accept {
#[serde(rename = "@context")]
pub context: Vec<&'static str>,
pub id: String,
#[serde(rename = "type")]
pub kind: &'static str,
pub actor: String,
pub object: serde_json::Value,
#[serde(skip_serializing_if = "Option::is_none")]
pub result: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub to: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub cc: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub published: Option<String>,
}
/// Inbound activities the inbox accepts. Unknown types are tolerated.
#[derive(Deserialize)]
#[serde(tag = "type")]
pub enum InboxActivity {
Follow(FollowActivity),
Undo(UndoActivity),
Like(ReactionActivity),
Announce(AnnounceActivity),
EmojiReact(ReactionActivity),
Accept(AcceptRejectActivity),
Reject(AcceptRejectActivity),
Create(CreateActivity),
Update(UpdateActivity),
Delete(DeleteActivity),
Offer(OfferActivity),
Resolve(ResolveActivity),
Edit(EditActivity),
Add(AddActivity),
#[serde(other)]
Unknown,
}
/// A Follow activity.
#[derive(Deserialize)]
pub struct FollowActivity {
pub id: String,
pub actor: String,
pub object: ObjectRef,
}
/// An Undo activity.
#[derive(Deserialize)]
pub struct UndoActivity {
pub id: String,
pub actor: String,
pub object: Box<UndoObject>,
}
/// A reaction (Like, Announce, EmojiReact).
#[derive(Deserialize)]
pub struct ReactionActivity {
pub id: String,
pub actor: String,
pub object: ObjectRef,
#[serde(default)]
pub content: Option<String>,
}
/// An Announce activity (boost) that can contain an embedded Note.
#[derive(Deserialize)]
pub struct AnnounceActivity {
pub id: String,
pub actor: String,
pub object: AnnounceObject,
}
/// Announce.object can be a URL or an embedded Note with full content.
#[derive(Deserialize)]
#[serde(untagged)]
pub enum AnnounceObject {
Url(String),
Embedded(AnnounceEmbedded),
}
impl AnnounceObject {
pub fn id(&self) -> &str {
match self {
AnnounceObject::Url(url) => url,
AnnounceObject::Embedded(embedded) => &embedded.id,
}
}
}
/// An embedded object in an Announce activity (e.g., a Note).
#[derive(Deserialize)]
pub struct AnnounceEmbedded {
pub id: String,
#[serde(rename = "type")]
pub kind: String,
#[serde(default)]
pub content: Option<String>,
#[serde(default)]
pub published: Option<String>,
#[serde(default)]
pub attributed_to: Option<String>,
}
/// An Accept or Reject activity (used for Follow responses).
#[derive(Deserialize)]
pub struct AcceptRejectActivity {
pub id: String,
pub actor: String,
pub object: ObjectRef,
#[serde(default)]
pub result: Option<String>,
}
/// An object field is either a URL string or an embedded document.
#[derive(Deserialize, Debug)]
#[serde(untagged)]
pub enum ObjectRef {
Url(String),
Embedded(EmbeddedObject),
}
impl ObjectRef {
pub fn id(&self) -> &str {
match self {
ObjectRef::Url(url) => url,
ObjectRef::Embedded(embedded) => &embedded.id,
}
}
}
/// An embedded object with an id and type.
#[derive(Deserialize, Debug)]
pub struct EmbeddedObject {
pub id: String,
#[serde(rename = "type")]
pub kind: String,
#[serde(default)]
pub summary: Option<String>,
#[serde(default)]
pub content: Option<String>,
}
/// Undo.object is the activity being undone.
#[derive(Deserialize)]
#[serde(untagged)]
pub enum UndoObject {
Embedded(UndoEmbedded),
Url(String),
}
impl UndoObject {
pub fn id(&self) -> &str {
match self {
UndoObject::Embedded(embedded) => &embedded.id,
UndoObject::Url(url) => url,
}
}
pub fn kind(&self) -> &str {
match self {
UndoObject::Embedded(embedded) => &embedded.kind,
UndoObject::Url(_) => "Unknown",
}
}
}
/// An embedded object being undone.
#[derive(Deserialize)]
pub struct UndoEmbedded {
pub id: String,
#[serde(rename = "type")]
pub kind: String,
#[serde(default)]
pub object: Option<ObjectRef>,
}
/// A Create activity (e.g., creating a Note).
#[derive(Deserialize)]
pub struct CreateActivity {
pub id: String,
pub actor: String,
pub object: CreateObject,
}
/// Create.object can be an embedded object (like a Note) or a URL.
#[derive(Deserialize)]
#[serde(untagged)]
pub enum CreateObject {
Url(String),
Embedded(CreateEmbedded),
}
impl CreateObject {
pub fn id(&self) -> &str {
match self {
CreateObject::Url(url) => url,
CreateObject::Embedded(embedded) => &embedded.id,
}
}
}
/// An embedded object in a Create activity (e.g., a Note).
#[derive(Deserialize)]
pub struct CreateEmbedded {
pub id: String,
#[serde(rename = "type")]
pub kind: String,
#[serde(default)]
pub content: Option<String>,
#[serde(default)]
pub published: Option<String>,
#[serde(default)]
pub attributed_to: Option<String>,
#[serde(default, rename = "inReplyTo")]
pub in_reply_to: Option<String>,
#[serde(default)]
pub source: Option<NoteSourceIn>,
}
/// An Update activity (e.g., updating a Note).
#[derive(Deserialize)]
pub struct UpdateActivity {
pub id: String,
pub actor: String,
pub object: UpdateObject,
}
/// Update.object can be an embedded object (like a Note) or a URL.
#[derive(Deserialize)]
#[serde(untagged)]
pub enum UpdateObject {
Url(String),
Embedded(UpdateEmbedded),
}
impl UpdateObject {
pub fn id(&self) -> &str {
match self {
UpdateObject::Url(url) => url,
UpdateObject::Embedded(embedded) => &embedded.id,
}
}
}
/// An embedded object in an Update activity (e.g., a Note).
#[derive(Deserialize)]
pub struct UpdateEmbedded {
pub id: String,
#[serde(rename = "type")]
pub kind: String,
#[serde(default)]
pub content: Option<String>,
#[serde(default)]
pub published: Option<String>,
}
/// A Delete activity (e.g., deleting a Note).
#[derive(Deserialize)]
pub struct DeleteActivity {
pub id: String,
pub actor: String,
pub object: DeleteObject,
}
/// Delete.object can be an embedded object or a URL.
#[derive(Deserialize)]
#[serde(untagged)]
pub enum DeleteObject {
Url(String),
Embedded(DeleteEmbedded),
}
impl DeleteObject {
pub fn id(&self) -> &str {
match self {
DeleteObject::Url(url) => url,
DeleteObject::Embedded(embedded) => &embedded.id,
}
}
}
/// An embedded object in a Delete activity.
#[derive(Deserialize)]
pub struct DeleteEmbedded {
pub id: String,
#[serde(rename = "type")]
pub kind: String,
}
/// A ForgeFed Offer activity (for creating tickets and merge requests).
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub struct Offer {
#[serde(rename = "@context")]
pub context: Vec<&'static str>,
pub id: String,
#[serde(rename = "type")]
pub kind: &'static str,
pub actor: String,
pub object: serde_json::Value,
pub target: String,
pub to: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub cc: Option<Vec<String>>,
pub published: String,
}
/// A ForgeFed Resolve activity (for closing tickets).
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub struct Resolve {
#[serde(rename = "@context")]
pub context: Vec<&'static str>,
pub id: String,
#[serde(rename = "type")]
pub kind: &'static str,
pub actor: String,
pub object: String,
pub to: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub cc: Option<Vec<String>>,
pub published: String,
}
/// A ForgeFed Undo activity (for reopening tickets).
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub struct Undo {
#[serde(rename = "@context")]
pub context: Vec<&'static str>,
pub id: String,
#[serde(rename = "type")]
pub kind: &'static str,
pub actor: String,
pub object: Resolve,
pub to: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub cc: Option<Vec<String>>,
pub published: String,
}
/// A ForgeFed Edit activity (for editing tickets).
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub struct Edit {
#[serde(rename = "@context")]
pub context: Vec<&'static str>,
pub id: String,
#[serde(rename = "type")]
pub kind: &'static str,
pub actor: String,
pub object: serde_json::Value,
pub to: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub cc: Option<Vec<String>>,
pub published: String,
}
/// A ForgeFed Delete activity (for deleting tickets).
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub struct Delete {
#[serde(rename = "@context")]
pub context: Vec<&'static str>,
pub id: String,
#[serde(rename = "type")]
pub kind: &'static str,
pub actor: String,
pub object: String,
pub to: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub cc: Option<Vec<String>>,
pub published: String,
}
/// A ForgeFed Add activity (for patch set updates).
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub struct Add {
#[serde(rename = "@context")]
pub context: Vec<&'static str>,
pub id: String,
#[serde(rename = "type")]
pub kind: &'static str,
pub actor: String,
pub object: serde_json::Value,
pub target: String,
pub to: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub cc: Option<Vec<String>>,
pub published: String,
}
// --- ForgeFed inbound activity types (Deserialize) ---
/// Inbound ForgeFed Offer activity (for receiving tickets and merge requests).
#[derive(Deserialize, Debug)]
pub struct OfferActivity {
pub id: String,
pub actor: String,
pub object: OfferObject,
#[serde(default)]
pub target: Option<ObjectRef>,
}
/// Offer.object can be a URL (fetch via signed_get) or an embedded Ticket/Patch.
#[derive(Deserialize, Debug)]
#[serde(untagged)]
pub enum OfferObject {
Url(String),
Embedded(OfferTicketEmbed),
}
impl OfferObject {
pub fn id(&self) -> Option<&str> {
match self {
OfferObject::Url(url) => Some(url),
OfferObject::Embedded(embedded) => embedded.id.as_deref(),
}
}
}
/// An embedded Ticket or Patch object in an Offer activity.
#[derive(Deserialize, Debug)]
pub struct OfferTicketEmbed {
#[serde(default)]
pub id: Option<String>,
#[serde(rename = "type")]
pub kind: String,
#[serde(default)]
pub attributed_to: Option<String>,
#[serde(default)]
pub summary: Option<String>,
#[serde(default)]
pub content: Option<String>,
#[serde(default, alias = "attachment")]
pub attachment: Vec<MrAttachment>,
#[serde(default, rename = "context")]
pub tracker: Option<String>,
#[serde(default)]
pub replies: Option<String>,
#[serde(default)]
pub is_resolved: Option<bool>,
#[serde(default)]
pub media_type: Option<String>,
#[serde(default)]
pub published: Option<String>,
}
/// An attachment in a Ticket/Patch (e.g., a patch set or branch reference).
#[derive(Deserialize, Debug)]
pub struct MrAttachment {
#[serde(rename = "type")]
pub kind: String,
#[serde(default)]
pub name: Option<String>,
#[serde(default)]
pub media_type: Option<String>,
#[serde(default)]
pub url: Option<String>,
#[serde(default)]
pub branch: Option<BranchOrUrl>,
#[serde(default)]
pub content: Option<PatchCollectionIn>,
#[serde(default)]
pub author: Option<String>,
#[serde(default)]
pub commit_id: Option<String>,
}
/// A patch collection (ordered list of patches).
#[derive(Deserialize, Debug, Default)]
pub struct PatchCollectionIn {
#[serde(default, alias = "orderedItems", alias = "items")]
pub items: Vec<PatchIn>,
#[serde(default)]
pub total_items: Option<i64>,
}
/// A single patch in a patch collection.
#[derive(Deserialize, Debug)]
pub struct PatchIn {
#[serde(rename = "type")]
pub kind: String,
#[serde(default)]
pub id: Option<String>,
#[serde(default)]
pub author: Option<String>,
#[serde(default)]
pub commit_id: Option<String>,
#[serde(default)]
pub summary: Option<String>,
}
/// A branch reference or URL.
#[derive(Deserialize, Debug)]
#[serde(untagged)]
pub enum BranchOrUrl {
Branch(BranchRef),
Url(String),
}
/// A branch reference with context, name, and git ref.
#[derive(Deserialize, Debug)]
pub struct BranchRef {
#[serde(rename = "type")]
pub kind: String,
#[serde(default)]
pub context: Option<String>,
#[serde(default)]
pub name: Option<String>,
#[serde(default, rename = "ref")]
pub git_ref: Option<String>,
}
/// Inbound ForgeFed Resolve activity (for closing tickets/patches).
#[derive(Deserialize, Debug)]
pub struct ResolveActivity {
pub id: String,
pub actor: String,
pub object: ObjectRef,
}
/// Inbound ForgeFed Edit activity (for editing tickets/comments).
#[derive(Deserialize, Debug)]
pub struct EditActivity {
pub id: String,
pub actor: String,
pub object: ObjectRef,
}
/// Inbound ForgeFed Add activity (for patch set updates).
#[derive(Deserialize, Debug)]
pub struct AddActivity {
pub id: String,
pub actor: String,
pub object: ObjectRef,
#[serde(default)]
pub target: Option<ObjectRef>,
}
/// Source information for an inbound Note (markdown content).
#[derive(Deserialize, Debug)]
pub struct NoteSourceIn {
#[serde(rename = "mediaType")]
pub media_type: String,
pub content: String,
}
/// A ForgeFed Ticket object (issue/tracker item).
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub struct Ticket {
#[serde(rename = "@context")]
pub context: Vec<&'static str>,
#[serde(skip_serializing_if = "Option::is_none")]
pub id: Option<String>,
#[serde(rename = "type")]
pub kind: &'static str,
pub attributed_to: String,
pub summary: String,
pub content: String,
pub media_type: String,
pub source: TicketSource,
#[serde(rename = "context")]
pub tracker: String,
pub is_resolved: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub resolved_by: Option<String>,
pub replies: String,
pub published: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub updated: Option<String>,
}
/// Source information for a Ticket (markdown content).
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub struct TicketSource {
pub media_type: String,
pub content: String,
}
/// A ForgeFed Patch (MergeRequest) - a Ticket with patch attachments.
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub struct Patch {
#[serde(rename = "@context")]
pub context: Vec<&'static str>,
pub id: String,
#[serde(rename = "type")]
pub kind: &'static str,
pub attributed_to: String,
pub summary: String,
pub content: String,
pub media_type: String,
pub source: TicketSource,
#[serde(rename = "context")]
pub tracker: String,
pub is_resolved: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub resolved_by: Option<String>,
pub replies: String,
pub published: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub updated: Option<String>,
pub attachment: PatchAttachment,
}
/// Patch attachment for a MergeRequest.
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub struct PatchAttachment {
#[serde(rename = "type")]
pub kind: &'static str,
pub origin: String,
pub target: String,
pub object: PatchCollection,
}
/// Collection of patches in a MergeRequest.
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub struct PatchCollection {
#[serde(rename = "type")]
pub kind: &'static str,
pub total_items: u64,
pub ordered_items: Vec<PatchObject>,
}
/// Individual patch object in a collection.
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub struct PatchObject {
#[serde(rename = "type")]
pub kind: &'static str,
pub media_type: String,
pub attributed_to: String,
pub content: String,
}
/// A ForgeFed Commit object (served at /~{owner}/{repo}/commits/{hash}).
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub struct Commit {
#[serde(rename = "@context")]
pub context: Vec<&'static str>,
pub id: String,
#[serde(rename = "type")]
pub kind: &'static str,
#[serde(skip_serializing_if = "Option::is_none")]
pub attributed_to: Option<String>,
pub hash: String,
pub created: String,
pub summary: String,
}
/// A ForgeFed Push activity (emitted when commits are pushed to a repository).
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub struct Push {
#[serde(rename = "@context")]
pub context: Vec<&'static str>,
pub id: String,
#[serde(rename = "type")]
pub kind: &'static str,
pub actor: String,
pub attributed_to: String,
pub target: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub hash_before: Option<String>,
pub hash_after: String,
pub object: PushCommitCollection,
pub published: String,
pub to: Vec<String>,
}
/// An OrderedCollection of commits embedded in a Push activity.
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub struct PushCommitCollection {
#[serde(rename = "type")]
pub kind: &'static str,
pub total_items: u64,
pub ordered_items: Vec<Commit>,
}
/// An OrderedCollection (outbox, followers).
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub struct OrderedCollection {
#[serde(rename = "@context")]
pub context: Vec<&'static str>,
pub id: String,
#[serde(rename = "type")]
pub kind: &'static str,
pub total_items: u64,
#[serde(skip_serializing_if = "Option::is_none")]
pub first: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub ordered_items: Option<Vec<serde_json::Value>>,
}
/// An OrderedCollectionPage (outbox paging).
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub struct OrderedCollectionPage {
#[serde(rename = "@context")]
pub context: Vec<&'static str>,
pub id: String,
#[serde(rename = "type")]
pub kind: &'static str,
pub total_items: u64,
pub part_of: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub next: Option<String>,
pub ordered_items: Vec<serde_json::Value>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn deserialize_follow() {
let json = r#"{
"type": "Follow",
"id": "https://remote.example/activities/123",
"actor": "https://remote.example/users/bob",
"object": "https://local.example/~alice"
}"#;
let activity: InboxActivity = serde_json::from_str(json).expect("should parse");
match activity {
InboxActivity::Follow(f) => {
assert_eq!(f.id, "https://remote.example/activities/123");
assert_eq!(f.actor, "https://remote.example/users/bob");
assert_eq!(f.object.id(), "https://local.example/~alice");
}
_ => panic!("expected Follow"),
}
}
#[test]
fn deserialize_emoji_react() {
let json = r#"{
"type": "EmojiReact",
"id": "https://remote.example/activities/456",
"actor": "https://remote.example/users/bob",
"object": "https://local.example/~alice/notes/abc",
"content": "🔥"
}"#;
let activity: InboxActivity = serde_json::from_str(json).expect("should parse");
match activity {
InboxActivity::EmojiReact(r) => {
assert_eq!(r.content, Some("🔥".to_string()));
}
_ => panic!("expected EmojiReact"),
}
}
#[test]
fn deserialize_undo_with_embedded() {
let json = r#"{
"type": "Undo",
"id": "https://remote.example/activities/789",
"actor": "https://remote.example/users/bob",
"object": {
"type": "Follow",
"id": "https://remote.example/activities/123"
}
}"#;
let activity: InboxActivity = serde_json::from_str(json).expect("should parse");
match activity {
InboxActivity::Undo(u) => {
assert_eq!(u.object.id(), "https://remote.example/activities/123");
assert_eq!(u.object.kind(), "Follow");
}
_ => panic!("expected Undo"),
}
}
#[test]
fn unknown_type_becomes_unknown() {
let json = r#"{
"type": "Move",
"id": "https://remote.example/activities/999",
"actor": "https://remote.example/users/bob"
}"#;
let activity: InboxActivity = serde_json::from_str(json).expect("should parse");
assert!(matches!(activity, InboxActivity::Unknown));
}
}