A better Rust ATProto crate
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

readme update, initial pretty codegen changes

+513 -2
+1 -2
README.md
··· 135 135 136 136 - ["the most straightforward interface to atproto I've encountered so far."](https://bsky.app/profile/offline.mountainherder.xyz/post/3m3xwewzs3k2v) - @offline.mountainherder.xyz 137 137 - "It has saved me a lot of time already! Well worth a few beers and or microcontrollers" - [@baileytownsend.dev](https://bsky.app/profile/baileytownsend.dev) 138 - - ["This is what your library allowed me to do in an hour!!! Thank you!!!"](https://bsky.app/profile/desertthunder.dev/post/3mhhbcula6224) 139 - - @desertthunder.dev 138 + - ["This is what your library allowed me to do in an hour!!! Thank you!!!"](https://bsky.app/profile/desertthunder.dev/post/3mhhbcula6224) - @desertthunder.dev 140 139 141 140 142 141 ## Development
+3
crates/jacquard-common/src/xrpc.rs
··· 13 13 #[cfg(feature = "streaming")] 14 14 pub mod streaming; 15 15 16 + /// Hand-written XRPC types for com.atproto endpoints (bootstrap types). 17 + pub mod atproto; 18 + 16 19 use alloc::borrow::ToOwned; 17 20 use alloc::boxed::Box; 18 21 use alloc::string::{String, ToString};
+509
crates/jacquard-common/src/xrpc/atproto.rs
··· 1 + //! Hand-written XRPC endpoint types for com.atproto endpoints. 2 + //! 3 + //! These types are vendored in jacquard-common to break the circular dependency 4 + //! between jacquard-lexgen/jacquard-identity and jacquard-api. They provide minimal 5 + //! implementations sufficient for bootstrap code generation without builders or 6 + //! validation helpers. 7 + 8 + use crate::{CowStr, IntoStatic}; 9 + use crate::types::string::{AtUri, Cid, Did, Handle, Nsid}; 10 + use crate::types::ident::AtIdentifier; 11 + use crate::types::value::Data; 12 + use crate::xrpc::{GenericError, XrpcMethod, XrpcRequest, XrpcResp}; 13 + use core::error::Error; 14 + use core::fmt::{self, Display}; 15 + use serde::{Deserialize, Serialize}; 16 + 17 + // ============================================================================ 18 + // com.atproto.repo.listRecords 19 + // ============================================================================ 20 + 21 + /// Request for com.atproto.repo.listRecords. 22 + #[derive( 23 + Serialize, 24 + Deserialize, 25 + Debug, 26 + Clone, 27 + PartialEq, 28 + Eq, 29 + jacquard_derive::IntoStatic, 30 + )] 31 + #[serde(rename_all = "camelCase")] 32 + pub struct ListRecords<'a> { 33 + #[serde(borrow)] 34 + pub collection: Nsid<'a>, 35 + #[serde(skip_serializing_if = "Option::is_none")] 36 + #[serde(borrow)] 37 + pub cursor: Option<CowStr<'a>>, 38 + #[serde(skip_serializing_if = "Option::is_none")] 39 + pub limit: Option<i64>, 40 + #[serde(borrow)] 41 + pub repo: AtIdentifier<'a>, 42 + #[serde(skip_serializing_if = "Option::is_none")] 43 + pub reverse: Option<bool>, 44 + } 45 + 46 + /// Output for com.atproto.repo.listRecords. 47 + #[derive( 48 + Serialize, 49 + Deserialize, 50 + Debug, 51 + Clone, 52 + PartialEq, 53 + Eq, 54 + jacquard_derive::IntoStatic, 55 + )] 56 + #[serde(rename_all = "camelCase")] 57 + pub struct ListRecordsOutput<'a> { 58 + #[serde(skip_serializing_if = "Option::is_none")] 59 + #[serde(borrow)] 60 + pub cursor: Option<CowStr<'a>>, 61 + #[serde(borrow)] 62 + pub records: Vec<ListRecordsRecord<'a>>, 63 + } 64 + 65 + /// A single record in a list response. 66 + #[derive( 67 + Serialize, 68 + Deserialize, 69 + Debug, 70 + Clone, 71 + PartialEq, 72 + Eq, 73 + jacquard_derive::IntoStatic, 74 + )] 75 + #[serde(rename_all = "camelCase")] 76 + pub struct ListRecordsRecord<'a> { 77 + #[serde(skip_serializing_if = "Option::is_none")] 78 + #[serde(borrow)] 79 + pub cid: Option<Cid<'a>>, 80 + #[serde(borrow)] 81 + pub uri: AtUri<'a>, 82 + #[serde(borrow)] 83 + pub value: Data<'a>, 84 + } 85 + 86 + /// Response marker for com.atproto.repo.listRecords. 87 + pub struct ListRecordsResponse; 88 + 89 + impl XrpcResp for ListRecordsResponse { 90 + const NSID: &'static str = "com.atproto.repo.listRecords"; 91 + const ENCODING: &'static str = "application/json"; 92 + type Output<'de> = ListRecordsOutput<'de>; 93 + type Err<'de> = GenericError<'de>; 94 + } 95 + 96 + impl<'a> XrpcRequest for ListRecords<'a> { 97 + const NSID: &'static str = "com.atproto.repo.listRecords"; 98 + const METHOD: XrpcMethod = XrpcMethod::Query; 99 + type Response = ListRecordsResponse; 100 + } 101 + 102 + // ============================================================================ 103 + // com.atproto.repo.getRecord 104 + // ============================================================================ 105 + 106 + /// Request for com.atproto.repo.getRecord. 107 + #[derive( 108 + Serialize, 109 + Deserialize, 110 + Debug, 111 + Clone, 112 + PartialEq, 113 + Eq, 114 + jacquard_derive::IntoStatic, 115 + )] 116 + #[serde(rename_all = "camelCase")] 117 + pub struct GetRecord<'a> { 118 + #[serde(skip_serializing_if = "Option::is_none")] 119 + #[serde(borrow)] 120 + pub cid: Option<Cid<'a>>, 121 + #[serde(borrow)] 122 + pub collection: Nsid<'a>, 123 + #[serde(borrow)] 124 + pub repo: AtIdentifier<'a>, 125 + #[serde(borrow)] 126 + pub rkey: CowStr<'a>, 127 + } 128 + 129 + /// Output for com.atproto.repo.getRecord. 130 + #[derive( 131 + Serialize, 132 + Deserialize, 133 + Debug, 134 + Clone, 135 + PartialEq, 136 + Eq, 137 + jacquard_derive::IntoStatic, 138 + )] 139 + #[serde(rename_all = "camelCase")] 140 + pub struct GetRecordOutput<'a> { 141 + #[serde(skip_serializing_if = "Option::is_none")] 142 + #[serde(borrow)] 143 + pub cid: Option<Cid<'a>>, 144 + #[serde(borrow)] 145 + pub uri: AtUri<'a>, 146 + #[serde(borrow)] 147 + pub value: Data<'a>, 148 + } 149 + 150 + /// Error type for com.atproto.repo.getRecord. 151 + #[derive( 152 + Serialize, 153 + Deserialize, 154 + Debug, 155 + Clone, 156 + PartialEq, 157 + Eq, 158 + jacquard_derive::IntoStatic, 159 + )] 160 + #[serde(tag = "error", content = "message")] 161 + #[serde(bound(deserialize = "'de: 'a"))] 162 + pub enum GetRecordError<'a> { 163 + #[serde(rename = "RecordNotFound")] 164 + RecordNotFound(Option<CowStr<'a>>), 165 + } 166 + 167 + impl<'a> Display for GetRecordError<'a> { 168 + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 169 + match self { 170 + Self::RecordNotFound(msg) => { 171 + write!(f, "RecordNotFound")?; 172 + if let Some(msg) = msg { 173 + write!(f, ": {}", msg)?; 174 + } 175 + Ok(()) 176 + } 177 + } 178 + } 179 + } 180 + 181 + impl Error for GetRecordError<'_> {} 182 + 183 + /// Response marker for com.atproto.repo.getRecord. 184 + pub struct GetRecordResponse; 185 + 186 + impl XrpcResp for GetRecordResponse { 187 + const NSID: &'static str = "com.atproto.repo.getRecord"; 188 + const ENCODING: &'static str = "application/json"; 189 + type Output<'de> = GetRecordOutput<'de>; 190 + type Err<'de> = GetRecordError<'de>; 191 + } 192 + 193 + impl<'a> XrpcRequest for GetRecord<'a> { 194 + const NSID: &'static str = "com.atproto.repo.getRecord"; 195 + const METHOD: XrpcMethod = XrpcMethod::Query; 196 + type Response = GetRecordResponse; 197 + } 198 + 199 + // ============================================================================ 200 + // com.atproto.identity.resolveHandle 201 + // ============================================================================ 202 + 203 + /// Request for com.atproto.identity.resolveHandle. 204 + #[derive( 205 + Serialize, 206 + Deserialize, 207 + Debug, 208 + Clone, 209 + PartialEq, 210 + Eq, 211 + jacquard_derive::IntoStatic, 212 + )] 213 + #[serde(rename_all = "camelCase")] 214 + pub struct ResolveHandle<'a> { 215 + #[serde(borrow)] 216 + pub handle: Handle<'a>, 217 + } 218 + 219 + /// Output for com.atproto.identity.resolveHandle. 220 + #[derive( 221 + Serialize, 222 + Deserialize, 223 + Debug, 224 + Clone, 225 + PartialEq, 226 + Eq, 227 + jacquard_derive::IntoStatic, 228 + )] 229 + #[serde(rename_all = "camelCase")] 230 + pub struct ResolveHandleOutput<'a> { 231 + #[serde(borrow)] 232 + pub did: Did<'a>, 233 + } 234 + 235 + /// Error type for com.atproto.identity.resolveHandle. 236 + #[derive( 237 + Serialize, 238 + Deserialize, 239 + Debug, 240 + Clone, 241 + PartialEq, 242 + Eq, 243 + jacquard_derive::IntoStatic, 244 + )] 245 + #[serde(tag = "error", content = "message")] 246 + #[serde(bound(deserialize = "'de: 'a"))] 247 + pub enum ResolveHandleError<'a> { 248 + #[serde(rename = "HandleNotFound")] 249 + HandleNotFound(Option<CowStr<'a>>), 250 + } 251 + 252 + impl<'a> Display for ResolveHandleError<'a> { 253 + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 254 + match self { 255 + Self::HandleNotFound(msg) => { 256 + write!(f, "HandleNotFound")?; 257 + if let Some(msg) = msg { 258 + write!(f, ": {}", msg)?; 259 + } 260 + Ok(()) 261 + } 262 + } 263 + } 264 + } 265 + 266 + impl Error for ResolveHandleError<'_> {} 267 + 268 + /// Response marker for com.atproto.identity.resolveHandle. 269 + pub struct ResolveHandleResponse; 270 + 271 + impl XrpcResp for ResolveHandleResponse { 272 + const NSID: &'static str = "com.atproto.identity.resolveHandle"; 273 + const ENCODING: &'static str = "application/json"; 274 + type Output<'de> = ResolveHandleOutput<'de>; 275 + type Err<'de> = ResolveHandleError<'de>; 276 + } 277 + 278 + impl<'a> XrpcRequest for ResolveHandle<'a> { 279 + const NSID: &'static str = "com.atproto.identity.resolveHandle"; 280 + const METHOD: XrpcMethod = XrpcMethod::Query; 281 + type Response = ResolveHandleResponse; 282 + } 283 + 284 + // ============================================================================ 285 + // com.atproto.identity.resolveDid 286 + // ============================================================================ 287 + 288 + /// Request for com.atproto.identity.resolveDid. 289 + #[derive( 290 + Serialize, 291 + Deserialize, 292 + Debug, 293 + Clone, 294 + PartialEq, 295 + Eq, 296 + jacquard_derive::IntoStatic, 297 + )] 298 + #[serde(rename_all = "camelCase")] 299 + pub struct ResolveDid<'a> { 300 + #[serde(borrow)] 301 + pub did: Did<'a>, 302 + } 303 + 304 + /// Output for com.atproto.identity.resolveDid. 305 + #[derive( 306 + Serialize, 307 + Deserialize, 308 + Debug, 309 + Clone, 310 + PartialEq, 311 + Eq, 312 + jacquard_derive::IntoStatic, 313 + )] 314 + #[serde(rename_all = "camelCase")] 315 + pub struct ResolveDidOutput<'a> { 316 + #[serde(borrow)] 317 + pub did_doc: Data<'a>, 318 + } 319 + 320 + /// Error type for com.atproto.identity.resolveDid. 321 + #[derive( 322 + Serialize, 323 + Deserialize, 324 + Debug, 325 + Clone, 326 + PartialEq, 327 + Eq, 328 + jacquard_derive::IntoStatic, 329 + )] 330 + #[serde(tag = "error", content = "message")] 331 + #[serde(bound(deserialize = "'de: 'a"))] 332 + pub enum ResolveDidError<'a> { 333 + #[serde(rename = "DidNotFound")] 334 + DidNotFound(Option<CowStr<'a>>), 335 + #[serde(rename = "DidDeactivated")] 336 + DidDeactivated(Option<CowStr<'a>>), 337 + } 338 + 339 + impl<'a> Display for ResolveDidError<'a> { 340 + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 341 + match self { 342 + Self::DidNotFound(msg) => { 343 + write!(f, "DidNotFound")?; 344 + if let Some(msg) = msg { 345 + write!(f, ": {}", msg)?; 346 + } 347 + Ok(()) 348 + } 349 + Self::DidDeactivated(msg) => { 350 + write!(f, "DidDeactivated")?; 351 + if let Some(msg) = msg { 352 + write!(f, ": {}", msg)?; 353 + } 354 + Ok(()) 355 + } 356 + } 357 + } 358 + } 359 + 360 + impl Error for ResolveDidError<'_> {} 361 + 362 + /// Response marker for com.atproto.identity.resolveDid. 363 + pub struct ResolveDidResponse; 364 + 365 + impl XrpcResp for ResolveDidResponse { 366 + const NSID: &'static str = "com.atproto.identity.resolveDid"; 367 + const ENCODING: &'static str = "application/json"; 368 + type Output<'de> = ResolveDidOutput<'de>; 369 + type Err<'de> = ResolveDidError<'de>; 370 + } 371 + 372 + impl<'a> XrpcRequest for ResolveDid<'a> { 373 + const NSID: &'static str = "com.atproto.identity.resolveDid"; 374 + const METHOD: XrpcMethod = XrpcMethod::Query; 375 + type Response = ResolveDidResponse; 376 + } 377 + 378 + // ============================================================================ 379 + // Tests 380 + // ============================================================================ 381 + 382 + #[cfg(test)] 383 + mod tests { 384 + use super::*; 385 + use crate::IntoStatic; 386 + use serde_json::json; 387 + 388 + #[test] 389 + fn test_list_records_serializes() { 390 + let req = ListRecords { 391 + repo: AtIdentifier::new("test.bsky.social").unwrap().into_static().into(), 392 + collection: Nsid::new("app.bsky.feed.post").unwrap().into_static(), 393 + cursor: None, 394 + limit: Some(50), 395 + reverse: None, 396 + }; 397 + 398 + let json = serde_json::to_value(&req).unwrap(); 399 + assert_eq!(json["repo"], "test.bsky.social"); 400 + assert_eq!(json["collection"], "app.bsky.feed.post"); 401 + assert_eq!(json["limit"], 50); 402 + assert!(!json.as_object().unwrap().contains_key("cursor")); 403 + } 404 + 405 + #[test] 406 + fn test_list_records_output_deserializes() { 407 + let json_str = r#"{ 408 + "records": [ 409 + { 410 + "uri": "at://did:plc:test/app.bsky.feed.post/123", 411 + "cid": "bafy123", 412 + "value": {} 413 + } 414 + ], 415 + "cursor": "page2" 416 + }"#; 417 + 418 + let output: ListRecordsOutput = serde_json::from_str(json_str).unwrap(); 419 + assert_eq!(output.records.len(), 1); 420 + assert!(output.cursor.is_some()); 421 + assert_eq!(output.cursor.as_ref().unwrap().as_str(), "page2"); 422 + } 423 + 424 + #[test] 425 + fn test_get_record_output_deserializes() { 426 + let json_str = r#"{ 427 + "uri": "at://did:plc:test/app.bsky.feed.post/123", 428 + "cid": "bafy123", 429 + "value": {} 430 + }"#; 431 + 432 + let output: GetRecordOutput = serde_json::from_str(json_str).unwrap(); 433 + assert!(output.cid.is_some()); 434 + assert_eq!(output.cid.as_ref().unwrap().as_str(), "bafy123"); 435 + } 436 + 437 + #[test] 438 + fn test_get_record_error_deserializes() { 439 + let json_str = r#"{"error": "RecordNotFound", "message": "not found"}"#; 440 + let error: GetRecordError = serde_json::from_str(json_str).unwrap(); 441 + assert!(matches!(error, GetRecordError::RecordNotFound(Some(_)))); 442 + } 443 + 444 + #[test] 445 + fn test_resolve_handle_output_deserializes() { 446 + let json_str = r#"{"did": "did:plc:abc123"}"#; 447 + let output: ResolveHandleOutput = serde_json::from_str(json_str).unwrap(); 448 + assert_eq!(output.did.as_str(), "did:plc:abc123"); 449 + } 450 + 451 + #[test] 452 + fn test_resolve_handle_error_deserializes() { 453 + let json_str = r#"{"error": "HandleNotFound", "message": "handle not found"}"#; 454 + let error: ResolveHandleError = serde_json::from_str(json_str).unwrap(); 455 + assert!(matches!(error, ResolveHandleError::HandleNotFound(Some(_)))); 456 + } 457 + 458 + #[test] 459 + fn test_resolve_did_output_deserializes() { 460 + let json_str = r#"{"didDoc": {}}"#; 461 + let output: ResolveDidOutput = serde_json::from_str(json_str).unwrap(); 462 + // Just verify it parses without error 463 + let _ = output; 464 + } 465 + 466 + #[test] 467 + fn test_resolve_did_error_deserializes_not_found() { 468 + let json_str = r#"{"error": "DidNotFound", "message": "did not found"}"#; 469 + let error: ResolveDidError = serde_json::from_str(json_str).unwrap(); 470 + assert!(matches!(error, ResolveDidError::DidNotFound(Some(_)))); 471 + } 472 + 473 + #[test] 474 + fn test_resolve_did_error_deserializes_deactivated() { 475 + let json_str = r#"{"error": "DidDeactivated", "message": "did is deactivated"}"#; 476 + let error: ResolveDidError = serde_json::from_str(json_str).unwrap(); 477 + assert!(matches!(error, ResolveDidError::DidDeactivated(Some(_)))); 478 + } 479 + 480 + #[test] 481 + fn test_types_implement_into_static() { 482 + let list_records = ListRecords { 483 + repo: AtIdentifier::new("test.bsky.social").unwrap().into_static().into(), 484 + collection: Nsid::new("app.bsky.feed.post").unwrap().into_static(), 485 + cursor: None, 486 + limit: Some(50), 487 + reverse: None, 488 + }; 489 + let _static = list_records.into_static(); 490 + 491 + let get_record = GetRecord { 492 + repo: AtIdentifier::new("test.bsky.social").unwrap().into_static().into(), 493 + collection: Nsid::new("app.bsky.feed.post").unwrap().into_static(), 494 + rkey: CowStr::from("abc123").into_static(), 495 + cid: None, 496 + }; 497 + let _static = get_record.into_static(); 498 + 499 + let resolve_handle = ResolveHandle { 500 + handle: Handle::new("test.bsky.social").unwrap().into_static(), 501 + }; 502 + let _static = resolve_handle.into_static(); 503 + 504 + let resolve_did = ResolveDid { 505 + did: Did::new("did:plc:abc123").unwrap().into_static(), 506 + }; 507 + let _static = resolve_did.into_static(); 508 + } 509 + }