+10
-4
crates/jacquard-common/src/cowstr.rs
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+1
crates/jacquard-common/src/types/xrpc.rs
+9
-4
crates/jacquard/Cargo.toml
+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
+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
+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
+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
+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
+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);