Skip to main content

atrg_feed/
types.rs

1//! Types for feed generator responses.
2//!
3//! Contains the wire types for `app.bsky.feed.describeFeedGenerator` and
4//! `app.bsky.feed.getFeedSkeleton` XRPC endpoints.
5
6use serde::{Deserialize, Serialize};
7
8/// Configuration for a single feed.
9#[derive(Debug, Clone, Deserialize)]
10pub struct FeedConfig {
11    /// Short identifier for the feed (e.g. `"my-feed"`).
12    pub id: String,
13    /// Human-readable display name.
14    pub display_name: String,
15    /// Optional description shown to users.
16    pub description: Option<String>,
17    /// Optional avatar blob reference (CID link).
18    pub avatar: Option<String>,
19}
20
21/// The skeleton response for `app.bsky.feed.getFeedSkeleton`.
22#[derive(Debug, Clone, Serialize, Deserialize)]
23pub struct FeedSkeleton {
24    /// The feed items (post AT-URIs).
25    pub feed: Vec<SkeletonItem>,
26    /// Cursor for pagination. `None` means no more results.
27    #[serde(skip_serializing_if = "Option::is_none")]
28    pub cursor: Option<String>,
29}
30
31/// A single item in a feed skeleton.
32#[derive(Debug, Clone, Serialize, Deserialize)]
33pub struct SkeletonItem {
34    /// AT-URI of the post (e.g. `at://did:plc:xxx/app.bsky.feed.post/rkey`).
35    pub post: String,
36}
37
38impl SkeletonItem {
39    /// Create a new skeleton item from an AT-URI string.
40    pub fn new(post_uri: impl Into<String>) -> Self {
41        Self {
42            post: post_uri.into(),
43        }
44    }
45}
46
47/// Description of a single feed within a feed generator.
48#[derive(Debug, Clone, Serialize, Deserialize)]
49pub struct FeedDescription {
50    /// The AT-URI of the feed generator record.
51    pub uri: String,
52    /// CID of the feed generator record (optional).
53    #[serde(skip_serializing_if = "Option::is_none")]
54    pub cid: Option<String>,
55}
56
57/// Response body for `app.bsky.feed.describeFeedGenerator`.
58#[derive(Debug, Clone, Serialize, Deserialize)]
59pub struct DescribeFeedGeneratorResponse {
60    /// DID of the feed generator service.
61    pub did: String,
62    /// List of feeds served by this generator.
63    pub feeds: Vec<FeedDescription>,
64}
65
66#[cfg(test)]
67mod tests {
68    use super::*;
69
70    #[test]
71    fn skeleton_item_new() {
72        let item = SkeletonItem::new("at://did:plc:abc/app.bsky.feed.post/123");
73        assert_eq!(item.post, "at://did:plc:abc/app.bsky.feed.post/123");
74    }
75
76    #[test]
77    fn feed_skeleton_serializes_without_cursor() {
78        let skeleton = FeedSkeleton {
79            feed: vec![SkeletonItem::new("at://did:plc:abc/app.bsky.feed.post/1")],
80            cursor: None,
81        };
82        let json = serde_json::to_value(&skeleton).unwrap();
83        assert!(json.get("cursor").is_none());
84        assert_eq!(
85            json["feed"][0]["post"],
86            "at://did:plc:abc/app.bsky.feed.post/1"
87        );
88    }
89
90    #[test]
91    fn feed_skeleton_serializes_with_cursor() {
92        let skeleton = FeedSkeleton {
93            feed: vec![],
94            cursor: Some("abc123".to_string()),
95        };
96        let json = serde_json::to_value(&skeleton).unwrap();
97        assert_eq!(json["cursor"], "abc123");
98    }
99
100    #[test]
101    fn describe_response_serializes() {
102        let resp = DescribeFeedGeneratorResponse {
103            did: "did:web:feeds.example.com".to_string(),
104            feeds: vec![FeedDescription {
105                uri: "at://did:web:feeds.example.com/app.bsky.feed.generator/my-feed".to_string(),
106                cid: None,
107            }],
108        };
109        let json = serde_json::to_value(&resp).unwrap();
110        assert_eq!(json["did"], "did:web:feeds.example.com");
111        assert_eq!(json["feeds"].as_array().unwrap().len(), 1);
112        assert!(json["feeds"][0].get("cid").is_none());
113    }
114}