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