Skip to main content

atrg_repo/
types.rs

1//! Shared types for AT Protocol record repository operations.
2
3use serde::{Deserialize, Serialize};
4
5/// A strong reference to a record (URI + CID).
6///
7/// Used as the return type for record creation and update operations,
8/// providing both the AT-URI and the content hash of the written record.
9#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
10pub struct StrongRef {
11    /// The AT-URI of the record (e.g. `at://did:plc:xxx/app.bsky.feed.post/rkey`).
12    pub uri: String,
13    /// The CID (content identifier) hash of the record.
14    pub cid: String,
15}
16
17/// A reference to an uploaded blob.
18///
19/// Returned by blob upload operations. Embed this in record values
20/// to reference uploaded media (images, video, etc.).
21#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
22pub struct BlobRef {
23    /// Always `"blob"`.
24    #[serde(rename = "$type")]
25    pub blob_type: String,
26    /// The CID link to the blob content.
27    #[serde(rename = "ref")]
28    pub reference: BlobLink,
29    /// The MIME type of the blob (e.g. `"image/png"`).
30    #[serde(rename = "mimeType")]
31    pub mime_type: String,
32    /// The size of the blob in bytes.
33    pub size: u64,
34}
35
36/// A CID link used within a [`BlobRef`].
37#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
38pub struct BlobLink {
39    /// The CID string.
40    #[serde(rename = "$link")]
41    pub link: String,
42}
43
44/// A paginated response of records.
45///
46/// AT Protocol APIs use cursor-based pagination. When `cursor` is `Some`,
47/// pass it to the next request to fetch the following page.
48#[derive(Debug, Clone, Serialize, Deserialize)]
49pub struct Page<T> {
50    /// The records in this page.
51    pub records: Vec<T>,
52    /// Opaque cursor for fetching the next page. `None` if this is the last page.
53    pub cursor: Option<String>,
54}
55
56/// A single record with its repository metadata.
57#[derive(Debug, Clone, Serialize, Deserialize)]
58pub struct Record<T> {
59    /// The AT-URI of the record.
60    pub uri: String,
61    /// The CID (content identifier) hash of the record.
62    pub cid: String,
63    /// The record value.
64    pub value: T,
65}
66
67#[cfg(test)]
68mod tests {
69    use super::*;
70
71    #[test]
72    fn strong_ref_serde_round_trip() {
73        let strong = StrongRef {
74            uri: "at://did:plc:abc123/app.bsky.feed.post/3k2la".into(),
75            cid: "bafyreib2rxk3rybkba".into(),
76        };
77        let json = serde_json::to_string(&strong).unwrap();
78        let decoded: StrongRef = serde_json::from_str(&json).unwrap();
79        assert_eq!(strong, decoded);
80    }
81
82    #[test]
83    fn blob_ref_serde_round_trip() {
84        let blob = BlobRef {
85            blob_type: "blob".into(),
86            reference: BlobLink {
87                link: "bafyreib2rxk3rybkba".into(),
88            },
89            mime_type: "image/png".into(),
90            size: 12345,
91        };
92        let json = serde_json::to_string(&blob).unwrap();
93        let decoded: BlobRef = serde_json::from_str(&json).unwrap();
94        assert_eq!(blob, decoded);
95    }
96
97    #[test]
98    fn blob_ref_json_field_names() {
99        let blob = BlobRef {
100            blob_type: "blob".into(),
101            reference: BlobLink {
102                link: "bafyreib2rxk3rybkba".into(),
103            },
104            mime_type: "image/jpeg".into(),
105            size: 999,
106        };
107        let val: serde_json::Value = serde_json::to_value(&blob).unwrap();
108        assert!(val.get("$type").is_some());
109        assert!(val.get("ref").is_some());
110        assert!(val.get("mimeType").is_some());
111        let ref_obj = val.get("ref").unwrap();
112        assert!(ref_obj.get("$link").is_some());
113    }
114
115    #[test]
116    fn page_serde_round_trip() {
117        let page: Page<StrongRef> = Page {
118            records: vec![StrongRef {
119                uri: "at://did:plc:abc/col.name/rkey".into(),
120                cid: "bafyxyz".into(),
121            }],
122            cursor: Some("next_cursor".into()),
123        };
124        let json = serde_json::to_string(&page).unwrap();
125        let decoded: Page<StrongRef> = serde_json::from_str(&json).unwrap();
126        assert_eq!(decoded.records.len(), 1);
127        assert_eq!(decoded.cursor.as_deref(), Some("next_cursor"));
128    }
129
130    #[test]
131    fn page_last_page_has_no_cursor() {
132        let page: Page<StrongRef> = Page {
133            records: vec![],
134            cursor: None,
135        };
136        let json = serde_json::to_string(&page).unwrap();
137        let decoded: Page<StrongRef> = serde_json::from_str(&json).unwrap();
138        assert!(decoded.cursor.is_none());
139    }
140
141    #[test]
142    fn record_serde_round_trip() {
143        let record: Record<serde_json::Value> = Record {
144            uri: "at://did:plc:abc/col.name/rkey".into(),
145            cid: "bafyxyz".into(),
146            value: serde_json::json!({"text": "hello"}),
147        };
148        let json = serde_json::to_string(&record).unwrap();
149        let decoded: Record<serde_json::Value> = serde_json::from_str(&json).unwrap();
150        assert_eq!(decoded.uri, "at://did:plc:abc/col.name/rkey");
151        assert_eq!(decoded.value["text"], "hello");
152    }
153}