Now let's take a silly one
1use knot_types::{AccountDid, AtUri, Cid, Collection, Rkey, ServiceDid};
2use serde::{Deserialize, Serialize};
3use url::Url;
4
5use crate::AtprotoError;
6
7pub const PUT_RECORD_METHOD: &str = "com.atproto.repo.putRecord";
8
9#[derive(Debug, Clone, PartialEq, Eq)]
10pub struct PointerReceipt {
11 pub uri: AtUri,
12 pub cid: Cid,
13}
14
15pub(crate) fn pds_service_did(pds: &Url) -> Result<ServiceDid, AtprotoError> {
16 let bad = || AtprotoError::BadPdsEndpoint {
17 pds: pds.as_str().to_string(),
18 };
19 let host = pds
20 .host_str()
21 .filter(|host| !host.is_empty())
22 .ok_or_else(bad)?;
23 let authority = pds
24 .port()
25 .map_or_else(|| host.to_string(), |port| format!("{host}%3A{port}"));
26 let msid = std::iter::once(authority)
27 .chain(
28 pds.path_segments()
29 .into_iter()
30 .flatten()
31 .filter(|segment| !segment.is_empty())
32 .map(str::to_string),
33 )
34 .collect::<Vec<_>>()
35 .join(":");
36 ServiceDid::new(format!("did:web:{msid}")).map_err(|_| bad())
37}
38
39#[derive(Serialize)]
40struct PutRecordInput<'a, R: Serialize> {
41 repo: &'a AccountDid,
42 collection: &'static str,
43 rkey: &'a Rkey,
44 record: &'a R,
45}
46
47pub(crate) fn put_record_body<R: Collection + Serialize>(
48 subject: &AccountDid,
49 rkey: &Rkey,
50 record: &R,
51) -> Result<Vec<u8>, AtprotoError> {
52 serde_json::to_vec(&PutRecordInput {
53 repo: subject,
54 collection: R::NSID,
55 rkey,
56 record,
57 })
58 .map_err(|error| AtprotoError::PointerEncode(error.to_string()))
59}
60
61#[derive(Deserialize)]
62struct PutRecordOutput {
63 uri: String,
64 cid: String,
65}
66
67pub(crate) fn receipt_from_response(body: &[u8]) -> Result<PointerReceipt, AtprotoError> {
68 let output: PutRecordOutput = serde_json::from_slice(body)
69 .map_err(|error| AtprotoError::MalformedReceipt(error.to_string()))?;
70 let uri = AtUri::new_owned(&output.uri)
71 .map_err(|error| AtprotoError::MalformedReceipt(error.to_string()))?;
72 let cid = Cid::new_owned(output.cid.as_bytes())
73 .map_err(|error| AtprotoError::MalformedReceipt(error.to_string()))
74 .and_then(|cid: Cid| {
75 cid.is_valid().then_some(cid).ok_or_else(|| {
76 AtprotoError::MalformedReceipt(format!("cid {:?} does not parse", output.cid))
77 })
78 })?;
79 Ok(PointerReceipt { uri, cid })
80}
81
82#[cfg(test)]
83mod tests {
84 use super::*;
85
86 fn url(value: &str) -> Url {
87 Url::parse(value).unwrap()
88 }
89
90 #[test]
91 fn the_pds_service_did_is_the_web_did_of_its_host() {
92 let derived = pds_service_did(&url("https://pds.oyster.cafe")).unwrap();
93 assert_eq!(derived.as_str(), "did:web:pds.oyster.cafe");
94 }
95
96 #[test]
97 fn a_pds_port_is_percent_encoded_into_the_service_did() {
98 let derived = pds_service_did(&url("https://pds.oyster.cafe:8443")).unwrap();
99 assert_eq!(derived.as_str(), "did:web:pds.oyster.cafe%3A8443");
100 }
101
102 #[test]
103 fn a_pds_base_path_becomes_did_path_segments() {
104 let derived = pds_service_did(&url("https://shared.host/account-pds")).unwrap();
105 assert_eq!(derived.as_str(), "did:web:shared.host:account-pds");
106 }
107
108 #[test]
109 fn a_hostless_pds_endpoint_is_refused() {
110 let error = pds_service_did(&url("unix:/run/pds.sock")).unwrap_err();
111 assert!(matches!(error, AtprotoError::BadPdsEndpoint { .. }));
112 }
113
114 #[test]
115 fn a_receipt_round_trips_its_uri_and_cid() {
116 let body = serde_json::json!({
117 "uri": "at://did:plc:squid/sh.tangled.knot.member/3jzfcijpj2z2a",
118 "cid": "bafyreidfayvfuwqa7qlnopdjiqrxzs6blmoeu4rujcjtnci5beludirz2a"
119 });
120 let receipt = receipt_from_response(&serde_json::to_vec(&body).unwrap()).unwrap();
121 assert_eq!(
122 receipt.uri.as_str(),
123 "at://did:plc:squid/sh.tangled.knot.member/3jzfcijpj2z2a"
124 );
125 assert_eq!(
126 receipt.cid.as_str(),
127 "bafyreidfayvfuwqa7qlnopdjiqrxzs6blmoeu4rujcjtnci5beludirz2a"
128 );
129 }
130
131 #[test]
132 fn a_garbage_receipt_is_a_typed_error() {
133 assert!(matches!(
134 receipt_from_response(b"not json"),
135 Err(AtprotoError::MalformedReceipt(_))
136 ));
137 let bad_uri = serde_json::json!({ "uri": "http://nope", "cid": "bafyreidfayvfuwqa7qlnopdjiqrxzs6blmoeu4rujcjtnci5beludirz2a" });
138 assert!(matches!(
139 receipt_from_response(&serde_json::to_vec(&bad_uri).unwrap()),
140 Err(AtprotoError::MalformedReceipt(_))
141 ));
142 let bad_cid = serde_json::json!({ "uri": "at://did:plc:squid/sh.tangled.knot.member/3jzfcijpj2z2a", "cid": "not-a-cid" });
143 assert!(matches!(
144 receipt_from_response(&serde_json::to_vec(&bad_cid).unwrap()),
145 Err(AtprotoError::MalformedReceipt(_))
146 ));
147 }
148}