docs pass.

claude may have helped some. i made them read the specs first.

Orual 30ecb131 42f14e72

+10 -4
crates/jacquard-common/src/cowstr.rs
··· 17 17 /// `<str as ToOwned>::Owned` is `String`, and not `SmolStr`. 18 18 #[derive(Clone)] 19 19 pub enum CowStr<'s> { 20 + /// &str varaiant 20 21 Borrowed(&'s str), 22 + /// Smolstr variant 21 23 Owned(SmolStr), 22 24 } 23 25 24 26 impl CowStr<'static> { 25 27 /// Create a new `CowStr` by copying from a `&str` — this might allocate 26 - /// if the `compact_str` feature is disabled, or if the string is longer 27 - /// than `MAX_INLINE_SIZE`. 28 + /// if the string is longer than `MAX_INLINE_SIZE`. 28 29 pub fn copy_from_str(s: &str) -> Self { 29 30 Self::Owned(SmolStr::from(s)) 30 31 } 31 32 33 + /// Create a new owned `CowStr` from a static &str without allocating 32 34 pub fn new_static(s: &'static str) -> Self { 33 35 Self::Owned(SmolStr::new_static(s)) 34 36 } ··· 36 38 37 39 impl<'s> CowStr<'s> { 38 40 #[inline] 41 + /// Borrow and decode a byte slice as utf8 into a CowStr 39 42 pub fn from_utf8(s: &'s [u8]) -> Result<Self, std::str::Utf8Error> { 40 43 Ok(Self::Borrowed(std::str::from_utf8(s)?)) 41 44 } 42 45 43 46 #[inline] 44 - pub fn from_utf8_owned(s: Vec<u8>) -> Result<Self, std::str::Utf8Error> { 45 - Ok(Self::Owned(SmolStr::new(std::str::from_utf8(&s)?))) 47 + /// Take bytes and decode them as utf8 into an owned CowStr. Might allocate. 48 + pub fn from_utf8_owned(s: impl AsRef<[u8]>) -> Result<Self, std::str::Utf8Error> { 49 + Ok(Self::Owned(SmolStr::new(std::str::from_utf8(&s.as_ref())?))) 46 50 } 47 51 48 52 #[inline] 53 + /// Take bytes and decode them as utf8, skipping invalid characters, taking ownership. 54 + /// Will allocate, uses String::from_utf8_lossy() internally for now. 49 55 pub fn from_utf8_lossy(s: &'s [u8]) -> Self { 50 56 Self::Owned(String::from_utf8_lossy(&s).into()) 51 57 }
+9
crates/jacquard-common/src/lib.rs
··· 1 + //! Common types for the jacquard implementation of atproto 2 + 3 + #![warn(missing_docs)] 4 + 5 + /// A copy-on-write immutable string type that uses [`SmolStr`] for 6 + /// the "owned" variant. 1 7 #[macro_use] 2 8 pub mod cowstr; 3 9 #[macro_use] 10 + /// trait for taking ownership of most borrowed types in jacquard. 4 11 pub mod into_static; 12 + /// Helper macros for common patterns 5 13 pub mod macros; 14 + /// Baseline fundamental AT Protocol data types. 6 15 pub mod types; 7 16 8 17 pub use cowstr::CowStr;
+51 -16
crates/jacquard-common/src/types.rs
··· 1 1 use serde::{Deserialize, Serialize}; 2 2 3 + /// AT Protocol URI (at://) types and validation 3 4 pub mod aturi; 5 + /// Blob references for binary data 4 6 pub mod blob; 7 + /// Content Identifier (CID) types for IPLD 5 8 pub mod cid; 9 + /// Repository collection trait for records 6 10 pub mod collection; 11 + /// AT Protocol datetime string type 7 12 pub mod datetime; 13 + /// Decentralized Identifier (DID) types and validation 8 14 pub mod did; 15 + /// AT Protocol handle types and validation 9 16 pub mod handle; 17 + /// AT Protocol identifier types (handle or DID) 10 18 pub mod ident; 19 + /// Integer type with validation 11 20 pub mod integer; 21 + /// Language tag types per BCP 47 12 22 pub mod language; 23 + /// CID link wrapper for JSON serialization 13 24 pub mod link; 25 + /// Namespaced Identifier (NSID) types and validation 14 26 pub mod nsid; 27 + /// Record key types and validation 15 28 pub mod recordkey; 29 + /// String types with format validation 16 30 pub mod string; 31 + /// Timestamp Identifier (TID) types and generation 17 32 pub mod tid; 33 + /// URI types with scheme validation 18 34 pub mod uri; 35 + /// Generic data value types for lexicon data model 19 36 pub mod value; 37 + /// XRPC protocol types and traits 20 38 pub mod xrpc; 21 39 22 40 /// Trait for a constant string literal type ··· 25 43 const LITERAL: &'static str; 26 44 } 27 45 46 + /// top-level domains which are not allowed in at:// handles or dids 28 47 pub const DISALLOWED_TLDS: &[&str] = &[ 29 48 ".local", 30 49 ".arpa", ··· 39 58 // "should" "never" actually resolve and get registered in production 40 59 ]; 41 60 61 + /// checks if a string ends with anything from the provided list of strings. 42 62 pub fn ends_with(string: impl AsRef<str>, list: &[&str]) -> bool { 43 63 let string = string.as_ref(); 44 64 for item in list { ··· 51 71 52 72 #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Hash)] 53 73 #[serde(rename_all = "kebab-case")] 74 + /// Valid types in the AT protocol [data model](https://atproto.com/specs/data-model). Type marker only, used in concert with `[Data<'_>]`. 54 75 pub enum DataModelType { 76 + /// Null type. IPLD type `null`, JSON type `Null`, CBOR Special Value (major 7) 55 77 Null, 78 + /// Boolean type. IPLD type `boolean`, JSON type Boolean, CBOR Special Value (major 7) 56 79 Boolean, 80 + /// Integer type. IPLD type `integer`, JSON type Number, CBOR Special Value (major 7) 57 81 Integer, 82 + /// Byte type. IPLD type `bytes`, in JSON a `{ "$bytes": bytes }` Object, CBOR Byte String (major 2) 58 83 Bytes, 84 + /// CID (content identifier) link. IPLD type `link`, in JSON a `{ "$link": cid }` Object, CBOR CID (tag 42) 59 85 CidLink, 86 + /// Blob type. No special IPLD type. in JSON a `{ "$type": "blob" }` Object. in CBOR a `{ "$type": "blob" }` Map. 60 87 Blob, 88 + /// Array type. IPLD type `list`. JSON type `Array`, CBOR type Array (major 4) 61 89 Array, 90 + /// Object type. IPLD type `map`. JSON type `Object`, CBOR type Map (major 5). keys are always SmolStr. 62 91 Object, 63 92 #[serde(untagged)] 93 + /// String type (lots of variants). JSON String, CBOR UTF-8 String (major 3) 64 94 String(LexiconStringType), 65 95 } 66 96 67 - #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Hash)] 68 - #[serde(rename_all = "kebab-case")] 69 - pub enum LexiconType { 70 - Params, 71 - Token, 72 - Ref, 73 - Union, 74 - Unknown, 75 - Record, 76 - Query, 77 - Procedure, 78 - Subscription, 79 - #[serde(untagged)] 80 - DataModel(DataModelType), 81 - } 82 - 97 + /// Lexicon string format types for typed strings in the AT Protocol data model 83 98 #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Hash)] 84 99 #[serde(rename_all = "kebab-case")] 85 100 pub enum LexiconStringType { 101 + /// ISO 8601 datetime string 86 102 Datetime, 103 + /// AT Protocol URI (at://) 87 104 AtUri, 105 + /// Decentralized Identifier 88 106 Did, 107 + /// AT Protocol handle 89 108 Handle, 109 + /// Handle or DID 90 110 AtIdentifier, 111 + /// Namespaced Identifier 91 112 Nsid, 113 + /// Content Identifier 92 114 Cid, 115 + /// BCP 47 language tag 93 116 Language, 117 + /// Timestamp Identifier 94 118 Tid, 119 + /// Record key 95 120 RecordKey, 121 + /// URI with type constraint 96 122 Uri(UriType), 123 + /// Plain string 97 124 #[serde(untagged)] 98 125 String, 99 126 } 100 127 128 + /// URI scheme types for lexicon URI format constraints 101 129 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] 102 130 #[serde(tag = "type")] 103 131 pub enum UriType { 132 + /// DID URI (did:) 104 133 Did, 134 + /// AT Protocol URI (at://) 105 135 At, 136 + /// HTTPS URI 106 137 Https, 138 + /// WebSocket Secure URI 107 139 Wss, 140 + /// CID URI 108 141 Cid, 142 + /// DNS name 109 143 Dns, 144 + /// Any valid URI 110 145 Any, 111 146 }
+31 -4
crates/jacquard-common/src/types/aturi.rs
··· 12 12 use std::sync::LazyLock; 13 13 use std::{ops::Deref, str::FromStr}; 14 14 15 - /// at:// URI type 15 + /// AT Protocol URI (`at://`) for referencing records in repositories 16 + /// 17 + /// AT URIs provide a way to reference records using either a DID or handle as the authority. 18 + /// They're not content-addressed, so the record's contents can change over time. 19 + /// 20 + /// Format: `at://AUTHORITY[/COLLECTION[/RKEY]][#FRAGMENT]` 21 + /// - Authority: DID or handle identifying the repository (required) 22 + /// - Collection: NSID of the record type (optional) 23 + /// - Record key (rkey): specific record identifier (optional) 24 + /// - Fragment: sub-resource identifier (optional, limited support) 16 25 /// 17 - /// based on the regex here: [](https://github.com/bluesky-social/atproto/blob/main/packages/syntax/src/aturi_validation.ts) 26 + /// Examples: 27 + /// - `at://alice.bsky.social` 28 + /// - `at://did:plc:abc123/app.bsky.feed.post/3jk5` 18 29 /// 19 - /// Doesn't support the query segment, but then neither does the Typescript SDK. 30 + /// See: <https://atproto.com/specs/at-uri-scheme> 20 31 #[derive(PartialEq, Eq, Debug)] 21 32 pub struct AtUri<'u> { 22 33 inner: Inner<'u>, ··· 81 92 } 82 93 } 83 94 84 - /// at:// URI path component (current subset) 95 + /// Path component of an AT URI (collection and optional record key) 96 + /// 97 + /// Represents the `/COLLECTION[/RKEY]` portion of an AT URI. 85 98 #[derive(Clone, PartialEq, Eq, Hash, Debug)] 86 99 pub struct RepoPath<'u> { 100 + /// Collection NSID (e.g., `app.bsky.feed.post`) 87 101 pub collection: Nsid<'u>, 102 + /// Optional record key identifying a specific record 88 103 pub rkey: Option<RecordKey<Rkey<'u>>>, 89 104 } 90 105 ··· 99 114 } 100 115 } 101 116 117 + /// Owned (static lifetime) version of `RepoPath` 102 118 pub type UriPathBuf = RepoPath<'static>; 103 119 120 + /// Regex for AT URI validation per AT Protocol spec 104 121 pub static ATURI_REGEX: LazyLock<Regex> = LazyLock::new(|| { 105 122 // Fragment allows: / and \ and other special chars. In raw string, backslashes are literal. 106 123 Regex::new(r##"^at://(?<authority>[a-zA-Z0-9._:%-]+)(/(?<collection>[a-zA-Z0-9-.]+)(/(?<rkey>[a-zA-Z0-9._~:@!$&%')(*+,;=-]+))?)?(#(?<fragment>/[a-zA-Z0-9._~:@!$&%')(*+,;=\-\[\]/\\]*))?$"##).unwrap() ··· 154 171 } 155 172 } 156 173 174 + /// Infallible constructor for when you know the URI is valid 175 + /// 176 + /// Panics on invalid URIs. Use this when manually constructing URIs from trusted sources. 157 177 pub fn raw(uri: &'u str) -> Self { 158 178 if let Some(parts) = ATURI_REGEX.captures(uri) { 159 179 if let Some(authority) = parts.name("authority") { ··· 275 295 }) 276 296 } 277 297 298 + /// Get the full URI as a string slice 278 299 pub fn as_str(&self) -> &str { 279 300 { 280 301 let this = &self.inner.borrow_uri(); ··· 282 303 } 283 304 } 284 305 306 + /// Get the authority component (DID or handle) 285 307 pub fn authority(&self) -> &AtIdentifier<'_> { 286 308 self.inner.borrow_authority() 287 309 } 288 310 311 + /// Get the path component (collection and optional rkey) 289 312 pub fn path(&self) -> &Option<RepoPath<'_>> { 290 313 self.inner.borrow_path() 291 314 } 292 315 316 + /// Get the fragment component if present 293 317 pub fn fragment(&self) -> &Option<CowStr<'_>> { 294 318 self.inner.borrow_fragment() 295 319 } 296 320 321 + /// Get the collection NSID from the path, if present 297 322 pub fn collection(&self) -> Option<&Nsid<'_>> { 298 323 self.inner.borrow_path().as_ref().map(|p| &p.collection) 299 324 } 300 325 326 + /// Get the record key from the path, if present 301 327 pub fn rkey(&self) -> Option<&RecordKey<Rkey<'_>>> { 302 328 self.inner 303 329 .borrow_path() ··· 400 426 } 401 427 } 402 428 429 + /// Fallible constructor, validates, doesn't allocate (static lifetime) 403 430 pub fn new_static(uri: &'static str) -> Result<Self, AtStrError> { 404 431 let uri = uri.as_ref(); 405 432 if let Some(parts) = ATURI_REGEX.captures(uri) {
+25 -7
crates/jacquard-common/src/types/blob.rs
··· 12 12 str::FromStr, 13 13 }; 14 14 15 + /// Blob reference for binary data in AT Protocol 16 + /// 17 + /// Blobs represent uploaded binary data (images, videos, etc.) stored separately from records. 18 + /// They include a CID reference, MIME type, and size information. 19 + /// 20 + /// Serialization differs between formats: 21 + /// - JSON: `ref` is serialized as `{"$link": "cid_string"}` 22 + /// - CBOR: `ref` is the raw CID 15 23 #[derive(Deserialize, Debug, Clone, PartialEq, Eq, Hash)] 16 24 #[serde(rename_all = "camelCase")] 17 25 pub struct Blob<'b> { 26 + /// CID (Content Identifier) reference to the blob data 18 27 pub r#ref: Cid<'b>, 28 + /// MIME type of the blob (e.g., "image/png", "video/mp4") 19 29 #[serde(borrow)] 20 30 pub mime_type: MimeType<'b>, 31 + /// Size of the blob in bytes 21 32 pub size: usize, 22 33 } 23 34 ··· 65 76 } 66 77 } 67 78 68 - /// Current, typed blob reference. 69 - /// Quite dislike this nesting, but it serves the same purpose as it did in Atrium 70 - /// Couple of helper methods and conversions to make it less annoying. 71 - /// TODO: revisit nesting and maybe hand-roll a serde impl that supports this sans nesting 79 + /// Tagged blob reference with `$type` field for serde 80 + /// 81 + /// This enum provides the `{"$type": "blob"}` wrapper expected by AT Protocol's JSON format. 82 + /// Currently only contains the `Blob` variant, but the enum structure supports future extensions. 72 83 #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] 73 84 #[serde(tag = "$type", rename_all = "lowercase")] 74 85 pub enum BlobRef<'r> { 86 + /// Blob variant with embedded blob data 75 87 #[serde(borrow)] 76 88 Blob(Blob<'r>), 77 89 } 78 90 79 91 impl<'r> BlobRef<'r> { 92 + /// Get the inner blob reference 80 93 pub fn blob(&self) -> &Blob<'r> { 81 94 match self { 82 95 BlobRef::Blob(blob) => blob, ··· 108 121 } 109 122 } 110 123 111 - /// Wrapper for file type 124 + /// MIME type identifier for blob data 125 + /// 126 + /// Used to specify the content type of blobs. Supports patterns like "image/*" and "*/*". 112 127 #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize)] 113 128 #[serde(transparent)] 114 129 #[repr(transparent)] ··· 120 135 Ok(Self(CowStr::Borrowed(mime_type))) 121 136 } 122 137 138 + /// Fallible constructor, validates, takes ownership 123 139 pub fn new_owned(mime_type: impl AsRef<str>) -> Self { 124 140 Self(CowStr::Owned(mime_type.as_ref().to_smolstr())) 125 141 } 126 142 143 + /// Fallible constructor, validates, doesn't allocate 127 144 pub fn new_static(mime_type: &'static str) -> Self { 128 145 Self(CowStr::new_static(mime_type)) 129 146 } 130 147 131 - /// Fallible constructor from an existing CowStr, borrows 148 + /// Fallible constructor from an existing CowStr 132 149 pub fn from_cowstr(mime_type: CowStr<'m>) -> Result<MimeType<'m>, &'static str> { 133 150 Ok(Self(mime_type)) 134 151 } 135 152 136 - /// Infallible constructor 153 + /// Infallible constructor for trusted MIME type strings 137 154 pub fn raw(mime_type: &'m str) -> Self { 138 155 Self(CowStr::Borrowed(mime_type)) 139 156 } 140 157 158 + /// Get the MIME type as a string slice 141 159 pub fn as_str(&self) -> &str { 142 160 { 143 161 let this = &self.0;
+44 -10
crates/jacquard-common/src/types/cid.rs
··· 4 4 use smol_str::ToSmolStr; 5 5 use std::{convert::Infallible, fmt, marker::PhantomData, ops::Deref, str::FromStr}; 6 6 7 - /// raw 7 + /// CID codec for AT Protocol (raw) 8 8 pub const ATP_CID_CODEC: u64 = 0x55; 9 9 10 - /// SHA-256 10 + /// CID hash function for AT Protocol (SHA-256) 11 11 pub const ATP_CID_HASH: u64 = 0x12; 12 12 13 - /// base 32 13 + /// CID encoding base for AT Protocol (base32 lowercase) 14 14 pub const ATP_CID_BASE: multibase::Base = multibase::Base::Base32Lower; 15 15 16 - #[derive(Debug, Clone, PartialEq, Eq, Hash)] 17 - /// Either the string form of a cid or the ipld form 18 - /// For the IPLD form we also cache the string representation for later use. 16 + /// Content Identifier (CID) for IPLD data in AT Protocol 17 + /// 18 + /// CIDs are self-describing content addresses used to reference IPLD data. 19 + /// This type supports both string and parsed IPLD forms, with string caching 20 + /// for the parsed form to optimize serialization. 19 21 /// 20 - /// Default on deserialization matches the format (if we get bytes, we try to decode) 22 + /// Deserialization automatically detects the format (bytes trigger IPLD parsing). 23 + #[derive(Debug, Clone, PartialEq, Eq, Hash)] 21 24 pub enum Cid<'c> { 22 - Ipld { cid: IpldCid, s: CowStr<'c> }, 25 + /// Parsed IPLD CID with cached string representation 26 + Ipld { 27 + /// Parsed CID structure 28 + cid: IpldCid, 29 + /// Cached base32 string form 30 + s: CowStr<'c>, 31 + }, 32 + /// String-only form (not yet parsed) 23 33 Str(CowStr<'c>), 24 34 } 25 35 36 + /// Errors that can occur when working with CIDs 26 37 #[derive(Debug, thiserror::Error, miette::Diagnostic)] 27 38 pub enum Error { 39 + /// Invalid IPLD CID structure 28 40 #[error("Invalid IPLD CID {:?}", 0)] 29 41 Ipld(#[from] cid::Error), 42 + /// Invalid UTF-8 in CID string 30 43 #[error("{:?}", 0)] 31 44 Utf8(#[from] std::str::Utf8Error), 32 45 } 33 46 34 47 impl<'c> Cid<'c> { 48 + /// Parse a CID from bytes (tries IPLD first, falls back to UTF-8 string) 35 49 pub fn new(cid: &'c [u8]) -> Result<Self, Error> { 36 50 if let Ok(cid) = IpldCid::try_from(cid.as_ref()) { 37 51 Ok(Self::ipld(cid)) ··· 41 55 } 42 56 } 43 57 58 + /// Parse a CID from bytes into an owned (static lifetime) value 44 59 pub fn new_owned(cid: &[u8]) -> Result<Cid<'static>, Error> { 45 60 if let Ok(cid) = IpldCid::try_from(cid.as_ref()) { 46 61 Ok(Self::ipld(cid)) ··· 50 65 } 51 66 } 52 67 68 + /// Construct a CID from a parsed IPLD CID 53 69 pub fn ipld(cid: IpldCid) -> Cid<'static> { 54 70 let s = CowStr::Owned( 55 71 cid.to_string_of_base(ATP_CID_BASE) ··· 59 75 Cid::Ipld { cid, s } 60 76 } 61 77 78 + /// Construct a CID from a string slice (borrows) 62 79 pub fn str(cid: &'c str) -> Self { 63 80 Self::Str(CowStr::Borrowed(cid)) 64 81 } 65 82 83 + /// Construct a CID from a CowStr 66 84 pub fn cow_str(cid: CowStr<'c>) -> Self { 67 85 Self::Str(cid) 68 86 } 69 87 88 + /// Convert to a parsed IPLD CID (parses if needed) 70 89 pub fn to_ipld(&self) -> Result<IpldCid, cid::Error> { 71 90 match self { 72 91 Cid::Ipld { cid, s: _ } => Ok(cid.clone()), ··· 74 93 } 75 94 } 76 95 96 + /// Get the CID as a string slice 77 97 pub fn as_str(&self) -> &str { 78 98 match self { 79 99 Cid::Ipld { cid: _, s } => s.as_ref(), ··· 218 238 } 219 239 } 220 240 221 - /// CID link wrapper that serializes as {"$link": "cid"} in JSON 222 - /// and as raw CID in CBOR 241 + /// CID link wrapper for JSON `{"$link": "cid"}` serialization 242 + /// 243 + /// Wraps a `Cid` and handles format-specific serialization: 244 + /// - JSON: `{"$link": "cid_string"}` 245 + /// - CBOR: raw CID bytes 246 + /// 247 + /// Used in the AT Protocol data model to represent IPLD links in JSON. 223 248 #[derive(Debug, Clone, PartialEq, Eq, Hash)] 224 249 #[repr(transparent)] 225 250 pub struct CidLink<'c>(pub Cid<'c>); 226 251 227 252 impl<'c> CidLink<'c> { 253 + /// Parse a CID link from bytes 228 254 pub fn new(cid: &'c [u8]) -> Result<Self, Error> { 229 255 Ok(Self(Cid::new(cid)?)) 230 256 } 231 257 258 + /// Parse a CID link from bytes into an owned value 232 259 pub fn new_owned(cid: &[u8]) -> Result<CidLink<'static>, Error> { 233 260 Ok(CidLink(Cid::new_owned(cid)?)) 234 261 } 235 262 263 + /// Construct a CID link from a static string 236 264 pub fn new_static(cid: &'static str) -> Self { 237 265 Self(Cid::str(cid)) 238 266 } 239 267 268 + /// Construct a CID link from a parsed IPLD CID 240 269 pub fn ipld(cid: IpldCid) -> CidLink<'static> { 241 270 CidLink(Cid::ipld(cid)) 242 271 } 243 272 273 + /// Construct a CID link from a string slice 244 274 pub fn str(cid: &'c str) -> Self { 245 275 Self(Cid::str(cid)) 246 276 } 247 277 278 + /// Construct a CID link from a CowStr 248 279 pub fn cow_str(cid: CowStr<'c>) -> Self { 249 280 Self(Cid::cow_str(cid)) 250 281 } 251 282 283 + /// Get the CID as a string slice 252 284 pub fn as_str(&self) -> &str { 253 285 self.0.as_str() 254 286 } 255 287 288 + /// Convert to a parsed IPLD CID 256 289 pub fn to_ipld(&self) -> Result<IpldCid, cid::Error> { 257 290 self.0.to_ipld() 258 291 } 259 292 293 + /// Unwrap into the inner Cid 260 294 pub fn into_inner(self) -> Cid<'c> { 261 295 self.0 262 296 }
+14 -3
crates/jacquard-common/src/types/datetime.rs
··· 9 9 use crate::{CowStr, IntoStatic}; 10 10 use regex::Regex; 11 11 12 + /// Regex for ISO 8601 datetime validation per AT Protocol spec 12 13 pub static ISO8601_REGEX: LazyLock<Regex> = LazyLock::new(|| { 13 14 Regex::new(r"^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\.[0-9]+)?(Z|(\+[0-9]{2}|\-[0-9][1-9]):[0-9]{2})$").unwrap() 14 15 }); 15 16 16 - /// A Lexicon timestamp. 17 + /// AT Protocol datetime (ISO 8601 with specific requirements) 18 + /// 19 + /// Lexicon datetimes use ISO 8601 format with these requirements: 20 + /// - Must include timezone (strongly prefer UTC with 'Z') 21 + /// - Requires whole seconds precision minimum 22 + /// - Supports millisecond and microsecond precision 23 + /// - Uses uppercase 'T' to separate date and time 24 + /// 25 + /// Examples: `"1985-04-12T23:20:50.123Z"`, `"2023-01-01T00:00:00+00:00"` 26 + /// 27 + /// The serialized form is preserved during parsing to ensure exact round-trip serialization. 17 28 #[derive(Clone, Debug, Eq, Hash)] 18 29 pub struct Datetime { 19 - /// Serialized form. Preserved during parsing to ensure round-trip re-serialization. 30 + /// Serialized form preserved from parsing for round-trip consistency 20 31 serialized: CowStr<'static>, 21 - /// Parsed form. 32 + /// Parsed datetime value for comparisons and operations 22 33 dt: chrono::DateTime<chrono::FixedOffset>, 23 34 } 24 35
+15
crates/jacquard-common/src/types/did.rs
··· 7 7 use std::sync::LazyLock; 8 8 use std::{ops::Deref, str::FromStr}; 9 9 10 + /// Decentralized Identifier (DID) for AT Protocol accounts 11 + /// 12 + /// DIDs are the persistent, long-term account identifiers in AT Protocol. Unlike handles, 13 + /// which can change, a DID permanently identifies an account across the network. 14 + /// 15 + /// Supported DID methods: 16 + /// - `did:plc` - Bluesky's novel DID method 17 + /// - `did:web` - Based on HTTPS and DNS 18 + /// 19 + /// Validation enforces a maximum length of 2048 characters and uses the pattern: 20 + /// `did:[method]:[method-specific-id]` where the method is lowercase ASCII and the 21 + /// method-specific-id allows alphanumerics, dots, colons, hyphens, underscores, and percent signs. 22 + /// 23 + /// See: <https://atproto.com/specs/did> 10 24 #[derive(Clone, PartialEq, Eq, Serialize, Hash)] 11 25 #[serde(transparent)] 12 26 #[repr(transparent)] ··· 94 108 Self(CowStr::Borrowed(did)) 95 109 } 96 110 111 + /// Get the DID as a string slice 97 112 pub fn as_str(&self) -> &str { 98 113 { 99 114 let this = &self.0;
+19 -2
crates/jacquard-common/src/types/handle.rs
··· 8 8 use std::sync::LazyLock; 9 9 use std::{ops::Deref, str::FromStr}; 10 10 11 + /// AT Protocol handle (human-readable account identifier) 12 + /// 13 + /// Handles are user-friendly account identifiers that must resolve to a DID through DNS 14 + /// or HTTPS. Unlike DIDs, handles can change over time, though they remain an important 15 + /// part of user identity. 16 + /// 17 + /// Format rules: 18 + /// - Maximum 253 characters 19 + /// - At least two segments separated by dots (e.g., "alice.bsky.social") 20 + /// - Each segment is 1-63 characters of ASCII letters, numbers, and hyphens 21 + /// - Segments cannot start or end with a hyphen 22 + /// - Final segment (TLD) cannot start with a digit 23 + /// - Case-insensitive (normalized to lowercase) 24 + /// 25 + /// Certain TLDs are disallowed (.local, .localhost, .arpa, .invalid, .internal, .example, .alt, .onion). 26 + /// 27 + /// See: <https://atproto.com/specs/handle> 11 28 #[derive(Clone, PartialEq, Eq, Serialize, Hash)] 12 29 #[serde(transparent)] 13 30 #[repr(transparent)] 14 31 pub struct Handle<'h>(CowStr<'h>); 15 32 33 + /// Regex for handle validation per AT Protocol spec 16 34 pub static HANDLE_REGEX: LazyLock<Regex> = LazyLock::new(|| { 17 35 Regex::new(r"^([a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?$").unwrap() 18 36 }); 19 - 20 - /// AT Protocol handle 21 37 impl<'h> Handle<'h> { 22 38 /// Fallible constructor, validates, borrows from input 23 39 /// ··· 127 143 Self(CowStr::Borrowed(stripped)) 128 144 } 129 145 146 + /// Get the handle as a string slice 130 147 pub fn as_str(&self) -> &str { 131 148 { 132 149 let this = &self.0;
+10 -1
crates/jacquard-common/src/types/ident.rs
··· 8 8 9 9 use crate::CowStr; 10 10 11 - /// An AT Protocol identifier. 11 + /// AT Protocol identifier (either a DID or handle) 12 + /// 13 + /// Represents the union of DIDs and handles, which can both be used to identify 14 + /// accounts in AT Protocol. DIDs are permanent identifiers, while handles are 15 + /// human-friendly and can change. 16 + /// 17 + /// Automatically determines whether a string is a DID or a handle during parsing. 12 18 #[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize, Hash)] 13 19 #[serde(untagged)] 14 20 pub enum AtIdentifier<'i> { 21 + /// DID variant 15 22 #[serde(borrow)] 16 23 Did(Did<'i>), 24 + /// Handle variant 17 25 Handle(Handle<'i>), 18 26 } 19 27 ··· 73 81 } 74 82 } 75 83 84 + /// Get the identifier as a string slice 76 85 pub fn as_str(&self) -> &str { 77 86 match self { 78 87 AtIdentifier::Did(did) => did.as_str(),
+7 -2
crates/jacquard-common/src/types/language.rs
··· 5 5 6 6 use crate::CowStr; 7 7 8 - /// An IETF language tag. 8 + /// IETF BCP 47 language tag for AT Protocol 9 + /// 10 + /// Language tags identify natural languages following the BCP 47 standard. They consist of 11 + /// a 2-3 character language code (e.g., "en", "ja") with optional regional subtags (e.g., "pt-BR"). 9 12 /// 10 - /// Uses langtag crate for validation, but is stored as a SmolStr for size/avoiding allocations 13 + /// Examples: `"ja"` (Japanese), `"pt-BR"` (Brazilian Portuguese), `"en-US"` (US English) 11 14 /// 15 + /// Language tags require semantic parsing rather than simple string comparison. 16 + /// Uses the `langtag` crate for validation but stores as `SmolStr` for efficiency. 12 17 /// TODO: Implement langtag-style semantic matching for this type, delegating to langtag 13 18 #[derive(Clone, Debug, PartialEq, Eq, Serialize, Hash)] 14 19 #[serde(transparent)]
+17 -3
crates/jacquard-common/src/types/nsid.rs
··· 8 8 use std::sync::LazyLock; 9 9 use std::{ops::Deref, str::FromStr}; 10 10 11 - /// Namespaced Identifier (NSID) 11 + /// Namespaced Identifier (NSID) for Lexicon schemas and XRPC endpoints 12 + /// 13 + /// NSIDs provide globally unique identifiers for Lexicon schemas, record types, and XRPC methods. 14 + /// They're structured as reversed domain names with a camelCase name segment. 15 + /// 16 + /// Format: `domain.authority.name` (e.g., `com.example.fooBar`) 17 + /// - Domain authority: reversed domain name (≤253 chars, lowercase, dots separate segments) 18 + /// - Name: camelCase identifier (letters and numbers only, cannot start with a digit) 19 + /// 20 + /// Validation rules: 21 + /// - Minimum 3 segments 22 + /// - Maximum 317 characters total 23 + /// - Each domain segment is 1-63 characters 24 + /// - Case-sensitive 12 25 /// 13 - /// Stored as SmolStr to ease lifetime issues and because, despite the fact that NSIDs *can* be 317 characters, most are quite short 14 - /// TODO: consider if this should go back to CowStr, or be broken up into segments 26 + /// See: <https://atproto.com/specs/nsid> 15 27 #[derive(Clone, PartialEq, Eq, Serialize, Hash)] 16 28 #[serde(transparent)] 17 29 #[repr(transparent)] 18 30 pub struct Nsid<'n>(CowStr<'n>); 19 31 32 + /// Regex for NSID validation per AT Protocol spec 20 33 pub static NSID_REGEX: LazyLock<Regex> = LazyLock::new(|| { 21 34 Regex::new(r"^[a-zA-Z]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+(\.[a-zA-Z][a-zA-Z0-9]{0,62})$").unwrap() 22 35 }); ··· 100 113 &self.0[split + 1..] 101 114 } 102 115 116 + /// Get the NSID as a string slice 103 117 pub fn as_str(&self) -> &str { 104 118 { 105 119 let this = &self.0;
+30 -10
crates/jacquard-common/src/types/recordkey.rs
··· 9 9 use std::sync::LazyLock; 10 10 use std::{ops::Deref, str::FromStr}; 11 11 12 - /// Trait for generic typed record keys 12 + /// Trait for typed record key implementations 13 13 /// 14 - /// This is deliberately public (so that consumers can develop specialized record key types), 15 - /// but is marked as unsafe, because the implementer is expected to uphold the invariants 16 - /// required by this trait, namely compliance with the [spec](https://atproto.com/specs/record-key) 17 - /// as described by [`RKEY_REGEX`]. 14 + /// Allows different record key types (TID, NSID, literals, generic strings) while 15 + /// maintaining validation guarantees. Implementers must ensure compliance with the 16 + /// AT Protocol [record key specification](https://atproto.com/specs/record-key). 18 17 /// 19 - /// This crate provides implementations for TID, NSID, literals, and generic strings 18 + /// # Safety 19 + /// Implementations must ensure the string representation matches [`RKEY_REGEX`] and 20 + /// is not "." or "..". Built-in implementations: `Tid`, `Nsid`, `Literal<T>`, `Rkey<'_>`. 20 21 pub unsafe trait RecordKeyType: Clone + Serialize { 22 + /// Get the record key as a string slice 21 23 fn as_str(&self) -> &str; 22 24 } 23 25 26 + /// Wrapper for typed record keys 27 + /// 28 + /// Provides a generic container for different record key types while preserving their 29 + /// specific validation guarantees through the `RecordKeyType` trait. 24 30 #[derive(Clone, PartialEq, Eq, Serialize, Deserialize, Hash, Debug)] 25 31 #[serde(transparent)] 26 32 #[repr(transparent)] ··· 56 62 } 57 63 } 58 64 59 - /// ATProto Record Key (type `any`) 60 - /// Catch-all for any string meeting the overall Record Key requirements detailed [](https://atproto.com/specs/record-key) 65 + /// AT Protocol record key (generic "any" type) 66 + /// 67 + /// Record keys uniquely identify records within a collection. This is the catch-all 68 + /// type for any valid record key string (1-512 characters of alphanumerics, dots, 69 + /// hyphens, underscores, colons, tildes). 70 + /// 71 + /// Common record key types: 72 + /// - TID: timestamp-based (most common) 73 + /// - Literal: fixed keys like "self" 74 + /// - NSID: namespaced identifiers 75 + /// - Any: flexible strings matching the validation rules 76 + /// 77 + /// See: <https://atproto.com/specs/record-key> 61 78 #[derive(Clone, PartialEq, Eq, Serialize, Hash)] 62 79 #[serde(transparent)] 63 80 #[repr(transparent)] ··· 69 86 } 70 87 } 71 88 89 + /// Regex for record key validation per AT Protocol spec 72 90 pub static RKEY_REGEX: LazyLock<Regex> = 73 91 LazyLock::new(|| Regex::new(r"^[a-zA-Z0-9.\-_:~]{1,512}$").unwrap()); 74 92 75 - /// AT Protocol rkey 76 93 impl<'r> Rkey<'r> { 77 94 /// Fallible constructor, validates, borrows from input 78 95 pub fn new(rkey: &'r str) -> Result<Self, AtStrError> { ··· 89 106 } 90 107 } 91 108 92 - /// Fallible constructor, validates, borrows from input 109 + /// Fallible constructor, validates, takes ownership 93 110 pub fn new_owned(rkey: impl AsRef<str>) -> Result<Self, AtStrError> { 94 111 let rkey = rkey.as_ref(); 95 112 if [".", ".."].contains(&rkey) { ··· 140 157 Self(CowStr::Borrowed(rkey)) 141 158 } 142 159 160 + /// Get the record key as a string slice 143 161 pub fn as_str(&self) -> &str { 144 162 { 145 163 let this = &self.0; ··· 265 283 } 266 284 267 285 #[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)] 286 + /// Key for a record where only one of an NSID is supposed to exist 268 287 pub struct SelfRecord; 269 288 270 289 impl Literal for SelfRecord { ··· 326 345 } 327 346 } 328 347 348 + /// Get the literal record key as a string slice 329 349 pub fn as_str(&self) -> &str { 330 350 T::LITERAL 331 351 }
+57 -3
crates/jacquard-common/src/types/string.rs
··· 21 21 }, 22 22 }; 23 23 24 - /// ATProto string value 24 + /// Polymorphic AT Protocol string value 25 + /// 26 + /// Represents any AT Protocol string type, automatically detecting and parsing 27 + /// into the appropriate variant. Used internally for generic value handling. 28 + /// 29 + /// Variants are checked in order from most specific to least specific. Note that 30 + /// record keys are intentionally NOT parsed from bare strings as the validation 31 + /// is too permissive and would catch too many values. 25 32 #[derive(Debug, Clone, PartialEq, Eq, Hash)] 26 33 pub enum AtprotoStr<'s> { 34 + /// ISO 8601 datetime 27 35 Datetime(Datetime), 36 + /// BCP 47 language tag 28 37 Language(Language), 38 + /// Timestamp identifier 29 39 Tid(Tid), 40 + /// Namespaced identifier 30 41 Nsid(Nsid<'s>), 42 + /// Decentralized identifier 31 43 Did(Did<'s>), 44 + /// Account handle 32 45 Handle(Handle<'s>), 46 + /// Identifier (DID or handle) 33 47 AtIdentifier(AtIdentifier<'s>), 48 + /// AT URI 34 49 AtUri(AtUri<'s>), 50 + /// Generic URI 35 51 Uri(Uri<'s>), 52 + /// Content identifier 36 53 Cid(Cid<'s>), 54 + /// Record key 37 55 RecordKey(RecordKey<Rkey<'s>>), 56 + /// Plain string (fallback) 38 57 String(CowStr<'s>), 39 58 } 40 59 ··· 77 96 } 78 97 } 79 98 99 + /// Get the string value regardless of variant 80 100 pub fn as_str(&self) -> &str { 81 101 match self { 82 102 Self::Datetime(datetime) => datetime.as_str(), ··· 238 258 help("if something doesn't match the spec, contact the crate author") 239 259 )] 240 260 pub struct AtStrError { 261 + /// AT Protocol spec name this error relates to 241 262 pub spec: SmolStr, 263 + /// The source string that failed to parse 242 264 #[source_code] 243 265 pub source: String, 266 + /// The specific kind of parsing error 244 267 #[source] 245 268 #[diagnostic_source] 246 269 pub kind: StrParseKind, 247 270 } 248 271 249 272 impl AtStrError { 273 + /// Create a new AT string parsing error 250 274 pub fn new(spec: &'static str, source: String, kind: StrParseKind) -> Self { 251 275 Self { 252 276 spec: SmolStr::new_static(spec), ··· 255 279 } 256 280 } 257 281 282 + /// Wrap an existing error with a new spec context 258 283 pub fn wrap(spec: &'static str, source: String, error: AtStrError) -> Self { 259 284 if let Some(span) = match &error.kind { 260 285 StrParseKind::Disallowed { problem, .. } => problem, ··· 309 334 } 310 335 } 311 336 337 + /// Create an error for a string that exceeds the maximum length 312 338 pub fn too_long(spec: &'static str, source: &str, max: usize, actual: usize) -> Self { 313 339 Self { 314 340 spec: SmolStr::new_static(spec), ··· 317 343 } 318 344 } 319 345 346 + /// Create an error for a string below the minimum length 320 347 pub fn too_short(spec: &'static str, source: &str, min: usize, actual: usize) -> Self { 321 348 Self { 322 349 spec: SmolStr::new_static(spec), ··· 348 375 } 349 376 350 377 /// missing component, with the span where it was expected to be founf 378 + /// Create an error for a missing component at a specific span 351 379 pub fn missing_from( 352 380 spec: &'static str, 353 381 source: &str, ··· 364 392 } 365 393 } 366 394 395 + /// Create an error for a regex validation failure 367 396 pub fn regex(spec: &'static str, source: &str, message: SmolStr) -> Self { 368 397 Self { 369 398 spec: SmolStr::new_static(spec), ··· 376 405 } 377 406 } 378 407 408 + /// Kinds of parsing errors for AT Protocol string types 379 409 #[derive(Debug, thiserror::Error, miette::Diagnostic)] 380 410 pub enum StrParseKind { 411 + /// Regex pattern validation failed 381 412 #[error("regex failure - {message}")] 382 413 #[diagnostic(code(jacquard::types::string::regex_fail))] 383 414 RegexFail { 415 + /// Optional span highlighting the problem area 384 416 #[label] 385 417 span: Option<SourceSpan>, 418 + /// Help message explaining the failure 386 419 #[help] 387 420 message: SmolStr, 388 421 }, 422 + /// String exceeds maximum allowed length 389 423 #[error("string too long (allowed: {max}, actual: {actual})")] 390 424 #[diagnostic(code(jacquard::types::string::wrong_length))] 391 - TooLong { max: usize, actual: usize }, 425 + TooLong { 426 + /// Maximum allowed length 427 + max: usize, 428 + /// Actual string length 429 + actual: usize, 430 + }, 392 431 432 + /// String is below minimum required length 393 433 #[error("string too short (allowed: {min}, actual: {actual})")] 394 434 #[diagnostic(code(jacquard::types::string::wrong_length))] 395 - TooShort { min: usize, actual: usize }, 435 + TooShort { 436 + /// Minimum required length 437 + min: usize, 438 + /// Actual string length 439 + actual: usize, 440 + }, 441 + /// String contains disallowed values 396 442 #[error("disallowed - {message}")] 397 443 #[diagnostic(code(jacquard::types::string::disallowed))] 398 444 Disallowed { 445 + /// Optional span highlighting the disallowed content 399 446 #[label] 400 447 problem: Option<SourceSpan>, 448 + /// Help message about what's disallowed 401 449 #[help] 402 450 message: SmolStr, 403 451 }, 452 + /// Required component is missing 404 453 #[error("missing - {message}")] 405 454 #[diagnostic(code(jacquard::atstr::missing_component))] 406 455 MissingComponent { 456 + /// Optional span where the component should be 407 457 #[label] 408 458 span: Option<SourceSpan>, 459 + /// Help message about what's missing 409 460 #[help] 410 461 message: SmolStr, 411 462 }, 463 + /// Wraps another error with additional context 412 464 #[error("{err:?}")] 413 465 #[diagnostic(code(jacquard::atstr::inner))] 414 466 Wrap { 467 + /// Optional span in the outer context 415 468 #[label] 416 469 span: Option<SourceSpan>, 470 + /// The wrapped inner error 417 471 #[source] 418 472 err: Arc<AtStrError>, 419 473 },
+26 -3
crates/jacquard-common/src/types/tid.rs
··· 28 28 builder.finish() 29 29 } 30 30 31 + /// Regex for TID validation per AT Protocol spec 31 32 static TID_REGEX: LazyLock<Regex> = LazyLock::new(|| { 32 33 Regex::new(r"^[234567abcdefghij][234567abcdefghijklmnopqrstuvwxyz]{12}$").unwrap() 33 34 }); 34 35 35 - /// A [Timestamp Identifier]. 36 + /// Timestamp Identifier (TID) for record keys and commit revisions 37 + /// 38 + /// TIDs are compact, sortable identifiers based on timestamps. They're used as record keys 39 + /// and repository commit revision numbers in AT Protocol. 40 + /// 41 + /// Format: 42 + /// - Always 13 ASCII characters 43 + /// - Base32-sortable encoding (`234567abcdefghijklmnopqrstuvwxyz`) 44 + /// - First 53 bits: microseconds since UNIX epoch 45 + /// - Final 10 bits: random clock identifier for collision resistance 46 + /// 47 + /// TIDs are sortable by timestamp and suitable for use in URLs. Generate new TIDs with 48 + /// `Tid::now()` or `Tid::now_with_clock_id()`. 36 49 /// 37 - /// [Timestamp Identifier]: https://atproto.com/specs/tid 50 + /// See: <https://atproto.com/specs/tid> 38 51 #[derive(Clone, Debug, PartialEq, Eq, Serialize, Hash)] 39 52 #[serde(transparent)] 40 53 #[repr(transparent)] ··· 105 118 Self(s32_encode(tid)) 106 119 } 107 120 121 + /// Construct a TID from a timestamp (in microseconds) and clock ID 108 122 pub fn from_time(timestamp: usize, clkid: u32) -> Self { 109 123 let str = smol_str::format_smolstr!( 110 124 "{0}{1:2>2}", ··· 114 128 Self(str) 115 129 } 116 130 131 + /// Extract the timestamp component (microseconds since UNIX epoch) 117 132 pub fn timestamp(&self) -> usize { 118 133 s32decode(self.0[0..11].to_owned()) 119 134 } 120 135 121 - // newer > older 136 + /// Compare two TIDs chronologically (newer > older) 137 + /// 138 + /// Returns 1 if self is newer, -1 if older, 0 if equal 122 139 pub fn compare_to(&self, other: &Tid) -> i8 { 123 140 if self.0 > other.0 { 124 141 return 1; ··· 129 146 0 130 147 } 131 148 149 + /// Check if this TID is newer than another 132 150 pub fn newer_than(&self, other: &Tid) -> bool { 133 151 self.compare_to(other) > 0 134 152 } 135 153 154 + /// Check if this TID is older than another 136 155 pub fn older_than(&self, other: &Tid) -> bool { 137 156 self.compare_to(other) < 0 138 157 } 139 158 159 + /// Generate the next TID in sequence after the given TID 140 160 pub fn next_str(prev: Option<Tid>) -> Result<Self, AtStrError> { 141 161 let prev = match prev { 142 162 None => None, ··· 173 193 } 174 194 } 175 195 196 + /// Decode a base32-sortable string into a usize 176 197 pub fn s32decode(s: String) -> usize { 177 198 let mut i: usize = 0; 178 199 for c in s.chars() { ··· 273 294 } 274 295 275 296 impl Ticker { 297 + /// Create a new TID generator with random clock ID 276 298 pub fn new() -> Self { 277 299 let mut ticker = Self { 278 300 last_timestamp: 0, ··· 284 306 ticker 285 307 } 286 308 309 + /// Generate the next TID, optionally ensuring it's after the given TID 287 310 pub fn next(&mut self, prev: Option<Tid>) -> Tid { 288 311 let now = SystemTime::now() 289 312 .duration_since(SystemTime::UNIX_EPOCH)
+19 -2
crates/jacquard-common/src/types/uri.rs
··· 7 7 types::{aturi::AtUri, cid::Cid, did::Did, string::AtStrError}, 8 8 }; 9 9 10 - /// URI with best-available contextual type 11 - /// TODO: figure out wtf a DNS uri should look like 10 + /// Generic URI with type-specific parsing 11 + /// 12 + /// Automatically detects and parses URIs into the appropriate variant based on 13 + /// the scheme prefix. Used in lexicon where URIs can be of various types. 14 + /// 15 + /// Variants are checked by prefix: `did:`, `at://`, `https://`, `wss://`, `ipld://` 12 16 #[derive(Debug, Clone, PartialEq, Eq, Hash)] 13 17 pub enum Uri<'u> { 18 + /// DID URI (did:) 14 19 Did(Did<'u>), 20 + /// AT Protocol URI (at://) 15 21 At(AtUri<'u>), 22 + /// HTTPS URL 16 23 Https(Url), 24 + /// WebSocket Secure URL 17 25 Wss(Url), 26 + /// IPLD CID URI 18 27 Cid(Cid<'u>), 28 + /// Unrecognized URI scheme (catch-all) 19 29 Any(CowStr<'u>), 20 30 } 21 31 32 + /// Errors that can occur when parsing URIs 22 33 #[derive(Debug, thiserror::Error, miette::Diagnostic)] 23 34 pub enum UriParseError { 35 + /// AT Protocol string parsing error 24 36 #[error("Invalid atproto string: {0}")] 25 37 At(#[from] AtStrError), 38 + /// Generic URL parsing error 26 39 #[error(transparent)] 27 40 Url(#[from] url::ParseError), 41 + /// CID parsing error 28 42 #[error(transparent)] 29 43 Cid(#[from] crate::types::cid::Error), 30 44 } 31 45 32 46 impl<'u> Uri<'u> { 47 + /// Parse a URI from a string slice, borrowing 33 48 pub fn new(uri: &'u str) -> Result<Self, UriParseError> { 34 49 if uri.starts_with("did:") { 35 50 Ok(Uri::Did(Did::new(uri)?)) ··· 46 61 } 47 62 } 48 63 64 + /// Parse a URI from a string, taking ownership 49 65 pub fn new_owned(uri: impl AsRef<str>) -> Result<Uri<'static>, UriParseError> { 50 66 let uri = uri.as_ref(); 51 67 if uri.starts_with("did:") { ··· 63 79 } 64 80 } 65 81 82 + /// Get the URI as a string slice 66 83 pub fn as_str(&self) -> &str { 67 84 match self { 68 85 Uri::Did(did) => did.as_str(),
+47
crates/jacquard-common/src/types/value.rs
··· 7 7 use smol_str::{SmolStr, ToSmolStr}; 8 8 use std::collections::BTreeMap; 9 9 10 + /// Conversion utilities for Data types 10 11 pub mod convert; 12 + /// String parsing for AT Protocol types 11 13 pub mod parsing; 14 + /// Serde implementations for Data types 12 15 pub mod serde_impl; 13 16 14 17 #[cfg(test)] 15 18 mod tests; 16 19 20 + /// AT Protocol data model value 21 + /// 22 + /// Represents any valid value in the AT Protocol data model, which supports JSON and CBOR 23 + /// serialization with specific constraints (no floats, CID links, blobs with metadata). 24 + /// 25 + /// This is the generic "unknown data" type used for lexicon values, extra fields captured 26 + /// by `#[lexicon]`, and IPLD data structures. 17 27 #[derive(Debug, Clone, PartialEq, Eq)] 18 28 pub enum Data<'s> { 29 + /// Null value 19 30 Null, 31 + /// Boolean value 20 32 Boolean(bool), 33 + /// Integer value (no floats in AT Protocol) 21 34 Integer(i64), 35 + /// String value (parsed into specific AT Protocol types when possible) 22 36 String(AtprotoStr<'s>), 37 + /// Raw bytes 23 38 Bytes(Bytes), 39 + /// CID link reference 24 40 CidLink(Cid<'s>), 41 + /// Array of values 25 42 Array(Array<'s>), 43 + /// Object/map of values 26 44 Object(Object<'s>), 45 + /// Blob reference with metadata 27 46 Blob(Blob<'s>), 28 47 } 29 48 49 + /// Errors that can occur when working with AT Protocol data 30 50 #[derive(Debug, Clone, PartialEq, Eq, thiserror::Error, miette::Diagnostic)] 31 51 pub enum AtDataError { 52 + /// Floating point numbers are not allowed in AT Protocol 32 53 #[error("floating point numbers not allowed in AT protocol data")] 33 54 FloatNotAllowed, 34 55 } 35 56 36 57 impl<'s> Data<'s> { 58 + /// Get the data model type of this value 37 59 pub fn data_type(&self) -> DataModelType { 38 60 match self { 39 61 Data::Null => DataModelType::Null, ··· 69 91 Data::Blob(_) => DataModelType::Blob, 70 92 } 71 93 } 94 + /// Parse a Data value from a JSON value 72 95 pub fn from_json(json: &'s serde_json::Value) -> Result<Self, AtDataError> { 73 96 Ok(if let Some(value) = json.as_bool() { 74 97 Self::Boolean(value) ··· 87 110 }) 88 111 } 89 112 113 + /// Parse a Data value from an IPLD value (CBOR) 90 114 pub fn from_cbor(cbor: &'s Ipld) -> Result<Self, AtDataError> { 91 115 Ok(match cbor { 92 116 Ipld::Null => Data::Null, ··· 121 145 } 122 146 } 123 147 148 + /// Array of AT Protocol data values 124 149 #[derive(Debug, Clone, PartialEq, Eq)] 125 150 pub struct Array<'s>(pub Vec<Data<'s>>); 126 151 ··· 132 157 } 133 158 134 159 impl<'s> Array<'s> { 160 + /// Parse an array from JSON values 135 161 pub fn from_json(json: &'s Vec<serde_json::Value>) -> Result<Self, AtDataError> { 136 162 let mut array = Vec::with_capacity(json.len()); 137 163 for item in json { ··· 139 165 } 140 166 Ok(Self(array)) 141 167 } 168 + /// Parse an array from IPLD values (CBOR) 142 169 pub fn from_cbor(cbor: &'s Vec<Ipld>) -> Result<Self, AtDataError> { 143 170 let mut array = Vec::with_capacity(cbor.len()); 144 171 for item in cbor { ··· 148 175 } 149 176 } 150 177 178 + /// Object/map of AT Protocol data values 151 179 #[derive(Debug, Clone, PartialEq, Eq)] 152 180 pub struct Object<'s>(pub BTreeMap<SmolStr, Data<'s>>); 153 181 ··· 159 187 } 160 188 161 189 impl<'s> Object<'s> { 190 + /// Parse an object from a JSON map with type inference 191 + /// 192 + /// Uses key names to infer the appropriate AT Protocol types for values. 162 193 pub fn from_json( 163 194 json: &'s serde_json::Map<String, serde_json::Value>, 164 195 ) -> Result<Data<'s>, AtDataError> { ··· 232 263 Ok(Data::Object(Object(map))) 233 264 } 234 265 266 + /// Parse an object from IPLD (CBOR) with type inference 267 + /// 268 + /// Uses key names to infer the appropriate AT Protocol types for values. 235 269 pub fn from_cbor(cbor: &'s BTreeMap<String, Ipld>) -> Result<Data<'s>, AtDataError> { 236 270 if let Some(Ipld::String(type_field)) = cbor.get("$type") { 237 271 if parsing::infer_from_type(type_field) == DataModelType::Blob { ··· 288 322 /// E.g. lower-level services, PDS implementations, firehose indexers, relay implementations. 289 323 #[derive(Debug, Clone, PartialEq, Eq)] 290 324 pub enum RawData<'s> { 325 + /// Null value 291 326 Null, 327 + /// Boolean value 292 328 Boolean(bool), 329 + /// Signed integer 293 330 SignedInt(i64), 331 + /// Unsigned integer 294 332 UnsignedInt(u64), 333 + /// String value (no type inference) 295 334 String(CowStr<'s>), 335 + /// Raw bytes 296 336 Bytes(Bytes), 337 + /// CID link reference 297 338 CidLink(Cid<'s>), 339 + /// Array of raw values 298 340 Array(Vec<RawData<'s>>), 341 + /// Object/map of raw values 299 342 Object(BTreeMap<SmolStr, RawData<'s>>), 343 + /// Valid blob reference 300 344 Blob(Blob<'s>), 345 + /// Invalid blob structure (captured for debugging) 301 346 InvalidBlob(Box<RawData<'s>>), 347 + /// Invalid number format, generally a floating point number (captured as bytes) 302 348 InvalidNumber(Bytes), 349 + /// Invalid/unknown data (captured as bytes) 303 350 InvalidData(Bytes), 304 351 }
+6
crates/jacquard-common/src/types/value/parsing.rs
··· 17 17 use std::{collections::BTreeMap, str::FromStr}; 18 18 use url::Url; 19 19 20 + /// Insert a string into an at:// `Data<'_>` map, inferring its type. 20 21 pub fn insert_string<'s>( 21 22 map: &mut BTreeMap<SmolStr, Data<'s>>, 22 23 key: &'s str, ··· 231 232 } 232 233 } 233 234 235 + /// Convert an ipld map to a atproto data model blob if it matches the format 234 236 pub fn cbor_to_blob<'b>(blob: &'b BTreeMap<String, Ipld>) -> Option<Blob<'b>> { 235 237 let mime_type = blob.get("mimeType").and_then(|o| { 236 238 if let Ipld::String(string) = o { ··· 267 269 None 268 270 } 269 271 272 + /// convert a JSON object to an atproto data model blob if it matches the format 270 273 pub fn json_to_blob<'b>(blob: &'b serde_json::Map<String, serde_json::Value>) -> Option<Blob<'b>> { 271 274 let mime_type = blob.get("mimeType").and_then(|v| v.as_str()); 272 275 if let Some(value) = blob.get("ref") { ··· 297 300 None 298 301 } 299 302 303 + /// Infer if something with a "$type" field is a blob or an object 300 304 pub fn infer_from_type(type_field: &str) -> DataModelType { 301 305 match type_field { 302 306 "blob" => DataModelType::Blob, ··· 304 308 } 305 309 } 306 310 311 + /// decode a base64 byte string into atproto data 307 312 pub fn decode_bytes<'s>(bytes: &str) -> Data<'s> { 308 313 // First one should just work. rest are insurance. 309 314 if let Ok(bytes) = BASE64_STANDARD.decode(bytes) { ··· 319 324 } 320 325 } 321 326 327 + /// decode a base64 byte string into atproto raw unvalidated data 322 328 pub fn decode_raw_bytes<'s>(bytes: &str) -> RawData<'s> { 323 329 // First one should just work. rest are insurance. 324 330 if let Ok(bytes) = BASE64_STANDARD.decode(bytes) {
+1
crates/jacquard-common/src/types/xrpc.rs
··· 45 45 } 46 46 } 47 47 48 + /// Get the body encoding type for this method (procedures only) 48 49 pub const fn body_encoding(&self) -> Option<&'static str> { 49 50 match self { 50 51 Self::Query => None,
+9 -4
crates/jacquard/Cargo.toml
··· 1 1 [package] 2 - authors.workspace = true 3 - # If you change the name here, you must also do it in flake.nix (and run `cargo generate-lockfile` afterwards) 4 2 name = "jacquard" 5 - description = "A simple Rust project using Nix" 3 + description = "Simple and powerful AT Procotol implementation" 4 + edition.workspace = true 6 5 version.workspace = true 7 - edition.workspace = true 6 + authors.workspace = true 7 + repository.workspace = true 8 + keywords.workspace = true 9 + categories.workspace = true 10 + readme.workspace = true 11 + documentation.workspace = true 12 + exclude.workspace = true 8 13 9 14 [features] 10 15 default = ["api_all"]
+45 -7
crates/jacquard/src/client.rs
··· 1 + //! XRPC client implementation for AT Protocol 2 + //! 3 + //! This module provides HTTP and XRPC client traits along with an authenticated 4 + //! client implementation that manages session tokens. 5 + 1 6 mod error; 2 7 mod response; 3 8 ··· 56 61 } 57 62 } 58 63 64 + /// HTTP client trait for sending raw HTTP requests 59 65 pub trait HttpClient { 66 + /// Error type returned by the HTTP client 60 67 type Error: std::error::Error + Display + Send + Sync + 'static; 61 68 /// Send an HTTP request and return the response. 62 69 fn send_http( ··· 64 71 request: Request<Vec<u8>>, 65 72 ) -> impl Future<Output = core::result::Result<http::Response<Vec<u8>>, Self::Error>>; 66 73 } 67 - /// XRPC client trait 74 + /// XRPC client trait for AT Protocol RPC calls 68 75 pub trait XrpcClient: HttpClient { 76 + /// Get the base URI for XRPC requests (e.g., "https://bsky.social") 69 77 fn base_uri(&self) -> CowStr<'_>; 78 + /// Get the authorization token for XRPC requests 70 79 #[allow(unused_variables)] 71 80 fn authorization_token( 72 81 &self, ··· 93 102 94 103 pub(crate) const NSID_REFRESH_SESSION: &str = "com.atproto.server.refreshSession"; 95 104 105 + /// Authorization token types for XRPC requests 96 106 pub enum AuthorizationToken<'s> { 107 + /// Bearer token (access JWT, refresh JWT to refresh the session) 97 108 Bearer(CowStr<'s>), 109 + /// DPoP token (proof-of-possession) for OAuth 98 110 Dpop(CowStr<'s>), 99 111 } 100 112 ··· 109 121 } 110 122 } 111 123 112 - /// HTTP headers which can be used in XPRC requests. 124 + /// HTTP headers commonly used in XRPC requests 113 125 pub enum Header { 126 + /// Content-Type header 114 127 ContentType, 128 + /// Authorization header 115 129 Authorization, 130 + /// `atproto-proxy` header - specifies which service (app server or other atproto service) the user's PDS should forward requests to as appropriate. 131 + /// 132 + /// See: <https://atproto.com/specs/xrpc#service-proxying> 116 133 AtprotoProxy, 134 + /// `atproto-accept-labelers` header used by clients to request labels from specific labelers to be included and applied in the response. See [label](https://atproto.com/specs/label) specification for details. 117 135 AtprotoAcceptLabelers, 118 136 } 119 137 ··· 210 228 Ok(Response::new(buffer, status)) 211 229 } 212 230 213 - /// Session information from createSession 231 + /// Session information from `com.atproto.server.createSession` 232 + /// 233 + /// Contains the access and refresh tokens along with user identity information. 214 234 #[derive(Debug, Clone)] 215 235 pub struct Session { 236 + /// Access token (JWT) used for authenticated requests 216 237 pub access_jwt: CowStr<'static>, 238 + /// Refresh token (JWT) used to obtain new access tokens 217 239 pub refresh_jwt: CowStr<'static>, 240 + /// User's DID (Decentralized Identifier) 218 241 pub did: Did<'static>, 242 + /// User's handle (e.g., "alice.bsky.social") 219 243 pub handle: Handle<'static>, 220 244 } 221 245 ··· 232 256 } 233 257 } 234 258 235 - /// Authenticated XRPC client that includes session tokens 259 + /// Authenticated XRPC client wrapper that manages session tokens 260 + /// 261 + /// Wraps an HTTP client and adds automatic Bearer token authentication for XRPC requests. 262 + /// Handles both access tokens for regular requests and refresh tokens for session refresh. 236 263 pub struct AuthenticatedClient<C> { 237 264 client: C, 238 265 base_uri: CowStr<'static>, ··· 241 268 242 269 impl<C> AuthenticatedClient<C> { 243 270 /// Create a new authenticated client with a base URI 271 + /// 272 + /// # Example 273 + /// ```ignore 274 + /// let client = AuthenticatedClient::new( 275 + /// reqwest::Client::new(), 276 + /// CowStr::from("https://bsky.social") 277 + /// ); 278 + /// ``` 244 279 pub fn new(client: C, base_uri: CowStr<'static>) -> Self { 245 280 Self { 246 281 client, ··· 249 284 } 250 285 } 251 286 252 - /// Set the session 287 + /// Set the session obtained from `createSession` or `refreshSession` 253 288 pub fn set_session(&mut self, session: Session) { 254 289 self.session = Some(session); 255 290 } 256 291 257 - /// Get the current session 292 + /// Get the current session if one exists 258 293 pub fn session(&self) -> Option<&Session> { 259 294 self.session.as_ref() 260 295 } 261 296 262 - /// Clear the session 297 + /// Clear the current session locally 298 + /// 299 + /// Note: This only clears the local session state. To properly revoke the session 300 + /// server-side, use `com.atproto.server.deleteSession` before calling this. 263 301 pub fn clear_session(&mut self) { 264 302 self.session = None; 265 303 }
+23 -1
crates/jacquard/src/client/error.rs
··· 1 + //! Error types for XRPC client operations 2 + 1 3 use bytes::Bytes; 2 4 3 - /// Client error type 5 + /// Client error type wrapping all possible error conditions 4 6 #[derive(Debug, thiserror::Error, miette::Diagnostic)] 5 7 pub enum ClientError { 6 8 /// HTTP transport error ··· 44 46 ), 45 47 } 46 48 49 + /// Transport-level errors that occur during HTTP communication 47 50 #[derive(Debug, thiserror::Error, miette::Diagnostic)] 48 51 pub enum TransportError { 52 + /// Failed to establish connection to server 49 53 #[error("Connection error: {0}")] 50 54 Connect(String), 51 55 56 + /// Request timed out 52 57 #[error("Request timeout")] 53 58 Timeout, 54 59 60 + /// Request construction failed (malformed URI, headers, etc.) 55 61 #[error("Invalid request: {0}")] 56 62 InvalidRequest(String), 57 63 64 + /// Other transport error 58 65 #[error("Transport error: {0}")] 59 66 Other(Box<dyn std::error::Error + Send + Sync>), 60 67 } ··· 62 69 // Re-export EncodeError from common 63 70 pub use jacquard_common::types::xrpc::EncodeError; 64 71 72 + /// Response deserialization errors 65 73 #[derive(Debug, thiserror::Error, miette::Diagnostic)] 66 74 pub enum DecodeError { 75 + /// JSON deserialization failed 67 76 #[error("Failed to deserialize JSON: {0}")] 68 77 Json( 69 78 #[from] 70 79 #[source] 71 80 serde_json::Error, 72 81 ), 82 + /// CBOR deserialization failed (local I/O) 73 83 #[error("Failed to deserialize CBOR: {0}")] 74 84 CborLocal( 75 85 #[from] 76 86 #[source] 77 87 serde_ipld_dagcbor::DecodeError<std::io::Error>, 78 88 ), 89 + /// CBOR deserialization failed (remote/reqwest) 79 90 #[error("Failed to deserialize CBOR: {0}")] 80 91 CborRemote( 81 92 #[from] ··· 84 95 ), 85 96 } 86 97 98 + /// HTTP error response (non-200 status codes outside of XRPC error handling) 87 99 #[derive(Debug, thiserror::Error, miette::Diagnostic)] 88 100 pub struct HttpError { 101 + /// HTTP status code 89 102 pub status: http::StatusCode, 103 + /// Response body if available 90 104 pub body: Option<Bytes>, 91 105 } 92 106 ··· 102 116 } 103 117 } 104 118 119 + /// Authentication and authorization errors 105 120 #[derive(Debug, thiserror::Error, miette::Diagnostic)] 106 121 pub enum AuthError { 122 + /// Access token has expired (use refresh token to get a new one) 107 123 #[error("Access token expired")] 108 124 TokenExpired, 109 125 126 + /// Access token is invalid or malformed 110 127 #[error("Invalid access token")] 111 128 InvalidToken, 112 129 130 + /// Token refresh request failed 113 131 #[error("Token refresh failed")] 114 132 RefreshFailed, 115 133 134 + /// Request requires authentication but none was provided 116 135 #[error("No authentication provided")] 117 136 NotAuthenticated, 137 + 138 + /// Other authentication error 118 139 #[error("Authentication error: {0:?}")] 119 140 Other(http::HeaderValue), 120 141 } 121 142 143 + /// Result type for client operations 122 144 pub type Result<T> = std::result::Result<T, ClientError>; 123 145 124 146 impl From<reqwest::Error> for TransportError {
+29 -21
crates/jacquard/src/client/response.rs
··· 1 + //! XRPC response parsing and error handling 2 + 1 3 use bytes::Bytes; 2 4 use http::StatusCode; 3 5 use jacquard_common::IntoStatic; 6 + use jacquard_common::smol_str::SmolStr; 4 7 use jacquard_common::types::xrpc::XrpcRequest; 5 8 use serde::Deserialize; 6 9 use std::marker::PhantomData; ··· 10 13 /// XRPC response wrapper that owns the response buffer 11 14 /// 12 15 /// Allows borrowing from the buffer when parsing to avoid unnecessary allocations. 16 + /// Supports both borrowed parsing (with `parse()`) and owned parsing (with `into_output()`). 13 17 pub struct Response<R: XrpcRequest> { 14 18 buffer: Bytes, 15 19 status: StatusCode, ··· 74 78 // 401: always auth error 75 79 } else { 76 80 match serde_json::from_slice::<GenericXrpcError>(&self.buffer) { 77 - Ok(generic) => { 78 - match generic.error.as_str() { 79 - "ExpiredToken" => Err(XrpcError::Auth(AuthError::TokenExpired)), 80 - "InvalidToken" => Err(XrpcError::Auth(AuthError::InvalidToken)), 81 - _ => Err(XrpcError::Auth(AuthError::NotAuthenticated)), 82 - } 83 - } 81 + Ok(generic) => match generic.error.as_str() { 82 + "ExpiredToken" => Err(XrpcError::Auth(AuthError::TokenExpired)), 83 + "InvalidToken" => Err(XrpcError::Auth(AuthError::InvalidToken)), 84 + _ => Err(XrpcError::Auth(AuthError::NotAuthenticated)), 85 + }, 84 86 Err(e) => Err(XrpcError::Decode(e)), 85 87 } 86 88 } ··· 120 122 match serde_json::from_slice::<GenericXrpcError>(&self.buffer) { 121 123 Ok(generic) => { 122 124 // Map auth-related errors to AuthError 123 - match generic.error.as_str() { 125 + match generic.error.as_ref() { 124 126 "ExpiredToken" => Err(XrpcError::Auth(AuthError::TokenExpired)), 125 127 "InvalidToken" => Err(XrpcError::Auth(AuthError::InvalidToken)), 126 128 _ => Err(XrpcError::Generic(generic)), ··· 133 135 // 401: always auth error 134 136 } else { 135 137 match serde_json::from_slice::<GenericXrpcError>(&self.buffer) { 136 - Ok(generic) => { 137 - match generic.error.as_str() { 138 - "ExpiredToken" => Err(XrpcError::Auth(AuthError::TokenExpired)), 139 - "InvalidToken" => Err(XrpcError::Auth(AuthError::InvalidToken)), 140 - _ => Err(XrpcError::Auth(AuthError::NotAuthenticated)), 141 - } 142 - } 138 + Ok(generic) => match generic.error.as_ref() { 139 + "ExpiredToken" => Err(XrpcError::Auth(AuthError::TokenExpired)), 140 + "InvalidToken" => Err(XrpcError::Auth(AuthError::InvalidToken)), 141 + _ => Err(XrpcError::Auth(AuthError::NotAuthenticated)), 142 + }, 143 143 Err(e) => Err(XrpcError::Decode(e)), 144 144 } 145 145 } ··· 151 151 } 152 152 } 153 153 154 - /// Generic XRPC error format (for InvalidRequest, etc.) 154 + /// Generic XRPC error format for untyped errors like InvalidRequest 155 + /// 156 + /// Used when the error doesn't match the endpoint's specific error enum 155 157 #[derive(Debug, Clone, Deserialize)] 156 158 pub struct GenericXrpcError { 157 - pub error: String, 158 - pub message: Option<String>, 159 + /// Error code (e.g., "InvalidRequest") 160 + pub error: SmolStr, 161 + /// Optional error message with details 162 + pub message: Option<SmolStr>, 159 163 } 160 164 161 165 impl std::fmt::Display for GenericXrpcError { ··· 170 174 171 175 impl std::error::Error for GenericXrpcError {} 172 176 177 + /// XRPC-specific errors returned from endpoints 178 + /// 179 + /// Represents errors returned in the response body 180 + /// Type parameter `E` is the endpoint's specific error enum type. 173 181 #[derive(Debug, thiserror::Error, miette::Diagnostic)] 174 182 pub enum XrpcError<E: std::error::Error + IntoStatic> { 175 - /// Typed XRPC error from the endpoint's error enum 183 + /// Typed XRPC error from the endpoint's specific error enum 176 184 #[error("XRPC error: {0}")] 177 185 Xrpc(E), 178 186 ··· 180 188 #[error("Authentication error: {0}")] 181 189 Auth(#[from] AuthError), 182 190 183 - /// Generic XRPC error (InvalidRequest, etc.) 191 + /// Generic XRPC error not in the endpoint's error enum (e.g., InvalidRequest) 184 192 #[error("XRPC error: {0}")] 185 193 Generic(GenericXrpcError), 186 194 187 - /// Failed to decode response 195 + /// Failed to decode the response body 188 196 #[error("Failed to decode response: {0}")] 189 197 Decode(#[from] serde_json::Error), 190 198 }
+7 -1
crates/jacquard/src/lib.rs
··· 1 + #![doc = include_str!("../../../README.md")] 2 + #![warn(missing_docs)] 3 + 4 + /// XRPC client traits and basic implementation 1 5 pub mod client; 2 6 3 - // Re-export common types 4 7 #[cfg(feature = "api")] 8 + /// If enabled, re-export the generated api crate 5 9 pub use jacquard_api as api; 10 + /// Re-export common types 6 11 pub use jacquard_common::*; 7 12 8 13 #[cfg(feature = "derive")] 14 + /// if enabled, reexport the attribute macros 9 15 pub use jacquard_derive::*;
+1 -1
crates/jacquard/src/main.rs
··· 27 27 28 28 // Create HTTP client 29 29 let http = reqwest::Client::new(); 30 - let mut client = AuthenticatedClient::new(http, CowStr::from(args.pds)); 30 + let mut client = AuthenticatedClient::new(http, args.pds); 31 31 32 32 // Create session 33 33 println!("logging in as {}...", args.username);