-13
.claude/settings.local.json
-13
.claude/settings.local.json
-151
CLAUDE.md
-151
CLAUDE.md
···
1
-
# CLAUDE.md
2
-
3
-
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4
-
5
-
## Project Overview
6
-
7
-
Jacquard is a suite of Rust crates for the AT Protocol (atproto/Bluesky). The project emphasizes spec-compliant, validated, performant baseline types with minimal boilerplate. Key design goals:
8
-
9
-
- Validated AT Protocol types including typed at:// URIs
10
-
- Custom lexicon extension support
11
-
- Lexicon `Value` type for working with unknown atproto data (dag-cbor or json)
12
-
- Using as much or as little of the crates as needed
13
-
14
-
## Workspace Structure
15
-
16
-
This is a Cargo workspace with several crates:
17
-
18
-
- **jacquard**: Main library crate with XRPC client and public API surface (re-exports jacquard-api and jacquard-common)
19
-
- **jacquard-common**: Core AT Protocol types (DIDs, handles, at-URIs, NSIDs, TIDs, CIDs, etc.) and the `CowStr` type for efficient string handling
20
-
- **jacquard-lexicon**: Lexicon parsing and Rust code generation from lexicon schemas
21
-
- **jacquard-api**: Generated API bindings from lexicon schemas (implementation detail, not directly used by consumers)
22
-
- **jacquard-derive**: Attribute macros (`#[lexicon]`, `#[open_union]`) for lexicon structures
23
-
24
-
## Development Commands
25
-
26
-
### Using Nix (preferred)
27
-
```bash
28
-
# Enter dev shell
29
-
nix develop
30
-
31
-
# Build
32
-
nix build
33
-
34
-
# Run
35
-
nix develop -c cargo run
36
-
```
37
-
38
-
### Using Cargo/Just
39
-
```bash
40
-
# Build
41
-
cargo build
42
-
43
-
# Run tests
44
-
cargo test
45
-
46
-
# Run specific test
47
-
cargo test <test_name>
48
-
49
-
# Run specific package tests
50
-
cargo test -p <package_name>
51
-
52
-
# Run
53
-
cargo run
54
-
55
-
# Auto-recompile and run
56
-
just watch [ARGS]
57
-
58
-
# Format and lint all
59
-
just pre-commit-all
60
-
61
-
# Generate API bindings from lexicon schemas
62
-
cargo run -p jacquard-lexicon --bin jacquard-codegen -- -i <input_dir> -o <output_dir> [-r <root_module>]
63
-
# Example:
64
-
cargo run -p jacquard-lexicon --bin jacquard-codegen -- -i crates/jacquard-lexicon/tests/fixtures/lexicons/atproto/lexicons -o crates/jacquard-api/src -r crate
65
-
```
66
-
67
-
## String Type Pattern
68
-
69
-
The codebase uses a consistent pattern for validated string types. Each type should have:
70
-
71
-
### Constructors
72
-
- `new()`: Construct from a string slice with appropriate lifetime (borrows)
73
-
- `new_owned()`: Construct from `impl AsRef<str>`, taking ownership
74
-
- `new_static()`: Construct from `&'static str` using `SmolStr`/`CowStr`'s static constructor (no allocation)
75
-
- `raw()`: Same as `new()` but panics instead of returning `Result`
76
-
- `unchecked()`: Same as `new()` but doesn't validate (marked `unsafe`)
77
-
- `as_str()`: Return string slice
78
-
79
-
### Traits
80
-
All string types should implement:
81
-
- `Serialize` + `Deserialize` (custom impl for latter, sometimes for former)
82
-
- `FromStr`, `Display`
83
-
- `Debug`, `PartialEq`, `Eq`, `Hash`, `Clone`
84
-
- `From<T> for String`, `CowStr`, `SmolStr`
85
-
- `From<String>`, `From<CowStr>`, `From<SmolStr>`, or `TryFrom` if likely to fail
86
-
- `AsRef<str>`
87
-
- `Deref` with `Target = str` (usually)
88
-
89
-
### Implementation Details
90
-
- Use `#[repr(transparent)]` when possible (exception: at-uri type and components)
91
-
- Use `SmolStr` directly as inner type if most instances will be under 24 bytes
92
-
- Use `CowStr` for longer strings to allow borrowing from input
93
-
- Implement `IntoStatic` trait to take ownership of string types
94
-
95
-
## Code Style
96
-
97
-
- Avoid comments for self-documenting code
98
-
- Comments should not detail fixes when refactoring
99
-
- Professional writing within source code and comments only
100
-
- Prioritize long-term maintainability over implementation speed
101
-
102
-
## Testing
103
-
104
-
- Write test cases for all critical code
105
-
- Tests can be run per-package or workspace-wide
106
-
- Use `cargo test <name>` to run specific tests
107
-
- Current test coverage: 89 tests in jacquard-common
108
-
109
-
## Lexicon Code Generation
110
-
111
-
The `jacquard-codegen` binary generates Rust types from AT Protocol Lexicon schemas:
112
-
113
-
- Generates structs with `#[lexicon]` attribute for forward compatibility (captures unknown fields in `extra_data`)
114
-
- Generates enums with `#[open_union]` attribute for handling unknown variants (unless marked `closed` in lexicon)
115
-
- Resolves local refs (e.g., `#image` becomes `Image<'a>`)
116
-
- Extracts doc comments from lexicon `description` fields
117
-
- Adds header comments with `@generated` marker and lexicon NSID
118
-
- Handles XRPC queries, procedures, subscriptions, and errors
119
-
- Generates proper module tree with Rust 2018 style
120
-
- **XrpcRequest trait**: Implemented directly on params/input structs (not marker types), with GATs for Output<'de> and Err<'de>
121
-
- **IntoStatic trait**: All generated types implement `IntoStatic` to convert borrowed types to owned ('static) variants
122
-
- **Collection trait**: Implemented on record types directly, with const NSID
123
-
124
-
## Current State & Next Steps
125
-
126
-
### Completed
127
-
- โ
Comprehensive validation tests for all core string types (handle, DID, NSID, TID, record key, AT-URI, datetime, language, identifier)
128
-
- โ
Validated implementations against AT Protocol specs and TypeScript reference implementation
129
-
- โ
String type interface standardization (Language now has `new_static()`, Datetime has full conversion traits)
130
-
- โ
Data serialization: Full serialize/deserialize for `Data<'_>`, `Array`, `Object` with format-specific handling (JSON vs CBOR)
131
-
- โ
CidLink wrapper type with automatic `{"$link": "cid"}` serialization in JSON
132
-
- โ
Integration test with real Bluesky thread data validates round-trip correctness
133
-
- โ
Lexicon code generation with forward compatibility and proper lifetime handling
134
-
- โ
IntoStatic implementations for all generated types (structs, enums, unions)
135
-
- โ
XrpcRequest trait with GATs, implemented on params/input types directly
136
-
- โ
HttpClient and XrpcClient traits with generic send_xrpc implementation
137
-
- โ
Response wrapper with parse() (borrowed) and into_output() (owned) methods
138
-
- โ
Structured error types (ClientError, TransportError, EncodeError, DecodeError, HttpError, AuthError)
139
-
140
-
### Next Steps
141
-
1. **Concrete HttpClient Implementation**: Implement HttpClient for reqwest::Client and potentially other HTTP clients
142
-
2. **Error Handling Improvements**: Add XRPC error parsing, better HTTP status code handling, structured error responses
143
-
3. **Authentication**: Session management, token refresh, DPoP support
144
-
4. **Body Encoding**: Support for non-JSON encodings (CBOR, multipart, etc.) in procedures
145
-
5. **Lexicon Resolution**: Fetch lexicons from web sources (atproto authorities, git repositories) and parse into corpus
146
-
6. **Custom Lexicon Support**: Allow users to plug in their own generated lexicons alongside jacquard-api types in the client/server layer
147
-
7. **Public API**: Design the main API surface in `jacquard` that re-exports and wraps generated types
148
-
8. **DID Document Support**: Parsing, validation, and resolution of DID documents
149
-
9. **OAuth Implementation**: OAuth flow support for authentication
150
-
10. **Examples & Documentation**: Create examples and improve documentation
151
-
11. **Testing**: Comprehensive tests for generated code and round-trip serialization
+20
Cargo.lock
+20
Cargo.lock
···
618
618
]
619
619
620
620
[[package]]
621
+
name = "fastrand"
622
+
version = "2.3.0"
623
+
source = "registry+https://github.com/rust-lang/crates.io-index"
624
+
checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
625
+
626
+
[[package]]
621
627
name = "find-msvc-tools"
622
628
version = "0.1.2"
623
629
source = "registry+https://github.com/rust-lang/crates.io-index"
···
1225
1231
"serde_repr",
1226
1232
"serde_with",
1227
1233
"syn 2.0.106",
1234
+
"tempfile",
1228
1235
"thiserror 2.0.17",
1229
1236
]
1230
1237
···
2163
2170
dependencies = [
2164
2171
"core-foundation-sys",
2165
2172
"libc",
2173
+
]
2174
+
2175
+
[[package]]
2176
+
name = "tempfile"
2177
+
version = "3.23.0"
2178
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2179
+
checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16"
2180
+
dependencies = [
2181
+
"fastrand",
2182
+
"getrandom 0.3.3",
2183
+
"once_cell",
2184
+
"rustix",
2185
+
"windows-sys 0.60.2",
2166
2186
]
2167
2187
2168
2188
[[package]]
+34
-5
Cargo.toml
+34
-5
Cargo.toml
···
8
8
version = "0.1.0"
9
9
authors = ["Orual <orual@nonbinary.computer>"]
10
10
repository = "https://tangled.org/@nonbinary.computer/jacquard"
11
-
keywords = ["atproto", "at protocol", "bluesky", "api", "client"]
11
+
keywords = ["atproto", "at", "bluesky", "api", "client"]
12
12
categories = ["api-bindings", "web-programming::http-client"]
13
13
readme = "README.md"
14
-
documentation = "https://docs.rs/jacquard"
15
14
exclude = [".direnv"]
16
-
17
-
18
-
description = "A simple Rust project using Nix"
15
+
homepage = "https://tangled.org/@nonbinary.computer/jacquard"
16
+
license-file = "LICENSE"
19
17
18
+
description = "Simple and powerful AT Protocol client library for Rust"
20
19
21
20
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
22
21
23
22
[workspace.dependencies]
23
+
# CLI
24
24
clap = { version = "4.5", features = ["derive"] }
25
+
26
+
# Serialization
27
+
serde = { version = "1.0", features = ["derive"] }
28
+
serde_json = "1.0"
29
+
serde_with = "3.14"
30
+
serde_html_form = "0.2"
31
+
serde_ipld_dagcbor = "0.6"
32
+
serde_repr = "0.1"
33
+
34
+
# Error handling
35
+
miette = "7.6"
36
+
thiserror = "2.0"
37
+
38
+
# Data types
39
+
bytes = "1.10"
40
+
smol_str = { version = "0.3", features = ["serde"] }
41
+
url = "2.5"
42
+
43
+
# Proc macros
44
+
proc-macro2 = "1.0"
45
+
quote = "1.0"
46
+
syn = "2.0"
47
+
heck = "0.5"
48
+
itertools = "0.14"
49
+
prettyplease = "0.2"
50
+
51
+
# HTTP
52
+
http = "1.3"
53
+
reqwest = { version = "0.12", default-features = false }
+369
-17
LICENSE
+369
-17
LICENSE
···
1
-
MIT License
1
+
Mozilla Public License Version 2.0
2
+
==================================
3
+
4
+
1. Definitions
5
+
--------------
6
+
7
+
1.1. "Contributor"
8
+
means each individual or legal entity that creates, contributes to
9
+
the creation of, or owns Covered Software.
10
+
11
+
1.2. "Contributor Version"
12
+
means the combination of the Contributions of others (if any) used
13
+
by a Contributor and that particular Contributor's Contribution.
14
+
15
+
1.3. "Contribution"
16
+
means Covered Software of a particular Contributor.
17
+
18
+
1.4. "Covered Software"
19
+
means Source Code Form to which the initial Contributor has attached
20
+
the notice in Exhibit A, the Executable Form of such Source Code
21
+
Form, and Modifications of such Source Code Form, in each case
22
+
including portions thereof.
23
+
24
+
1.5. "Incompatible With Secondary Licenses"
25
+
means
26
+
27
+
(a) that the initial Contributor has attached the notice described
28
+
in Exhibit B to the Covered Software; or
29
+
30
+
(b) that the Covered Software was made available under the terms of
31
+
version 1.1 or earlier of the License, but not also under the
32
+
terms of a Secondary License.
33
+
34
+
1.6. "Executable Form"
35
+
means any form of the work other than Source Code Form.
36
+
37
+
1.7. "Larger Work"
38
+
means a work that combines Covered Software with other material, in
39
+
a separate file or files, that is not Covered Software.
40
+
41
+
1.8. "License"
42
+
means this document.
43
+
44
+
1.9. "Licensable"
45
+
means having the right to grant, to the maximum extent possible,
46
+
whether at the time of the initial grant or subsequently, any and
47
+
all of the rights conveyed by this License.
48
+
49
+
1.10. "Modifications"
50
+
means any of the following:
51
+
52
+
(a) any file in Source Code Form that results from an addition to,
53
+
deletion from, or modification of the contents of Covered
54
+
Software; or
55
+
56
+
(b) any new file in Source Code Form that contains any Covered
57
+
Software.
58
+
59
+
1.11. "Patent Claims" of a Contributor
60
+
means any patent claim(s), including without limitation, method,
61
+
process, and apparatus claims, in any patent Licensable by such
62
+
Contributor that would be infringed, but for the grant of the
63
+
License, by the making, using, selling, offering for sale, having
64
+
made, import, or transfer of either its Contributions or its
65
+
Contributor Version.
66
+
67
+
1.12. "Secondary License"
68
+
means either the GNU General Public License, Version 2.0, the GNU
69
+
Lesser General Public License, Version 2.1, the GNU Affero General
70
+
Public License, Version 3.0, or any later versions of those
71
+
licenses.
72
+
73
+
1.13. "Source Code Form"
74
+
means the form of the work preferred for making modifications.
75
+
76
+
1.14. "You" (or "Your")
77
+
means an individual or a legal entity exercising rights under this
78
+
License. For legal entities, "You" includes any entity that
79
+
controls, is controlled by, or is under common control with You. For
80
+
purposes of this definition, "control" means (a) the power, direct
81
+
or indirect, to cause the direction or management of such entity,
82
+
whether by contract or otherwise, or (b) ownership of more than
83
+
fifty percent (50%) of the outstanding shares or beneficial
84
+
ownership of such entity.
85
+
86
+
2. License Grants and Conditions
87
+
--------------------------------
88
+
89
+
2.1. Grants
90
+
91
+
Each Contributor hereby grants You a world-wide, royalty-free,
92
+
non-exclusive license:
93
+
94
+
(a) under intellectual property rights (other than patent or trademark)
95
+
Licensable by such Contributor to use, reproduce, make available,
96
+
modify, display, perform, distribute, and otherwise exploit its
97
+
Contributions, either on an unmodified basis, with Modifications, or
98
+
as part of a Larger Work; and
2
99
3
-
Copyright (c) 2023 Orual
100
+
(b) under Patent Claims of such Contributor to make, use, sell, offer
101
+
for sale, have made, import, and otherwise transfer either its
102
+
Contributions or its Contributor Version.
4
103
5
-
Permission is hereby granted, free of charge, to any person obtaining a copy
6
-
of this software and associated documentation files (the "Software"), to deal
7
-
in the Software without restriction, including without limitation the rights
8
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
-
copies of the Software, and to permit persons to whom the Software is
10
-
furnished to do so, subject to the following conditions:
104
+
2.2. Effective Date
11
105
12
-
The above copyright notice and this permission notice shall be included in all
13
-
copies or substantial portions of the Software.
106
+
The licenses granted in Section 2.1 with respect to any Contribution
107
+
become effective for each Contribution on the date the Contributor first
108
+
distributes such Contribution.
14
109
15
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
-
SOFTWARE.
110
+
2.3. Limitations on Grant Scope
111
+
112
+
The licenses granted in this Section 2 are the only rights granted under
113
+
this License. No additional rights or licenses will be implied from the
114
+
distribution or licensing of Covered Software under this License.
115
+
Notwithstanding Section 2.1(b) above, no patent license is granted by a
116
+
Contributor:
117
+
118
+
(a) for any code that a Contributor has removed from Covered Software;
119
+
or
120
+
121
+
(b) for infringements caused by: (i) Your and any other third party's
122
+
modifications of Covered Software, or (ii) the combination of its
123
+
Contributions with other software (except as part of its Contributor
124
+
Version); or
125
+
126
+
(c) under Patent Claims infringed by Covered Software in the absence of
127
+
its Contributions.
128
+
129
+
This License does not grant any rights in the trademarks, service marks,
130
+
or logos of any Contributor (except as may be necessary to comply with
131
+
the notice requirements in Section 3.4).
132
+
133
+
2.4. Subsequent Licenses
134
+
135
+
No Contributor makes additional grants as a result of Your choice to
136
+
distribute the Covered Software under a subsequent version of this
137
+
License (see Section 10.2) or under the terms of a Secondary License (if
138
+
permitted under the terms of Section 3.3).
139
+
140
+
2.5. Representation
141
+
142
+
Each Contributor represents that the Contributor believes its
143
+
Contributions are its original creation(s) or it has sufficient rights
144
+
to grant the rights to its Contributions conveyed by this License.
145
+
146
+
2.6. Fair Use
147
+
148
+
This License is not intended to limit any rights You have under
149
+
applicable copyright doctrines of fair use, fair dealing, or other
150
+
equivalents.
151
+
152
+
2.7. Conditions
153
+
154
+
Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
155
+
in Section 2.1.
156
+
157
+
3. Responsibilities
158
+
-------------------
159
+
160
+
3.1. Distribution of Source Form
161
+
162
+
All distribution of Covered Software in Source Code Form, including any
163
+
Modifications that You create or to which You contribute, must be under
164
+
the terms of this License. You must inform recipients that the Source
165
+
Code Form of the Covered Software is governed by the terms of this
166
+
License, and how they can obtain a copy of this License. You may not
167
+
attempt to alter or restrict the recipients' rights in the Source Code
168
+
Form.
169
+
170
+
3.2. Distribution of Executable Form
171
+
172
+
If You distribute Covered Software in Executable Form then:
173
+
174
+
(a) such Covered Software must also be made available in Source Code
175
+
Form, as described in Section 3.1, and You must inform recipients of
176
+
the Executable Form how they can obtain a copy of such Source Code
177
+
Form by reasonable means in a timely manner, at a charge no more
178
+
than the cost of distribution to the recipient; and
179
+
180
+
(b) You may distribute such Executable Form under the terms of this
181
+
License, or sublicense it under different terms, provided that the
182
+
license for the Executable Form does not attempt to limit or alter
183
+
the recipients' rights in the Source Code Form under this License.
184
+
185
+
3.3. Distribution of a Larger Work
186
+
187
+
You may create and distribute a Larger Work under terms of Your choice,
188
+
provided that You also comply with the requirements of this License for
189
+
the Covered Software. If the Larger Work is a combination of Covered
190
+
Software with a work governed by one or more Secondary Licenses, and the
191
+
Covered Software is not Incompatible With Secondary Licenses, this
192
+
License permits You to additionally distribute such Covered Software
193
+
under the terms of such Secondary License(s), so that the recipient of
194
+
the Larger Work may, at their option, further distribute the Covered
195
+
Software under the terms of either this License or such Secondary
196
+
License(s).
197
+
198
+
3.4. Notices
199
+
200
+
You may not remove or alter the substance of any license notices
201
+
(including copyright notices, patent notices, disclaimers of warranty,
202
+
or limitations of liability) contained within the Source Code Form of
203
+
the Covered Software, except that You may alter any license notices to
204
+
the extent required to remedy known factual inaccuracies.
205
+
206
+
3.5. Application of Additional Terms
207
+
208
+
You may choose to offer, and to charge a fee for, warranty, support,
209
+
indemnity or liability obligations to one or more recipients of Covered
210
+
Software. However, You may do so only on Your own behalf, and not on
211
+
behalf of any Contributor. You must make it absolutely clear that any
212
+
such warranty, support, indemnity, or liability obligation is offered by
213
+
You alone, and You hereby agree to indemnify every Contributor for any
214
+
liability incurred by such Contributor as a result of warranty, support,
215
+
indemnity or liability terms You offer. You may include additional
216
+
disclaimers of warranty and limitations of liability specific to any
217
+
jurisdiction.
218
+
219
+
4. Inability to Comply Due to Statute or Regulation
220
+
---------------------------------------------------
221
+
222
+
If it is impossible for You to comply with any of the terms of this
223
+
License with respect to some or all of the Covered Software due to
224
+
statute, judicial order, or regulation then You must: (a) comply with
225
+
the terms of this License to the maximum extent possible; and (b)
226
+
describe the limitations and the code they affect. Such description must
227
+
be placed in a text file included with all distributions of the Covered
228
+
Software under this License. Except to the extent prohibited by statute
229
+
or regulation, such description must be sufficiently detailed for a
230
+
recipient of ordinary skill to be able to understand it.
231
+
232
+
5. Termination
233
+
--------------
234
+
235
+
5.1. The rights granted under this License will terminate automatically
236
+
if You fail to comply with any of its terms. However, if You become
237
+
compliant, then the rights granted under this License from a particular
238
+
Contributor are reinstated (a) provisionally, unless and until such
239
+
Contributor explicitly and finally terminates Your grants, and (b) on an
240
+
ongoing basis, if such Contributor fails to notify You of the
241
+
non-compliance by some reasonable means prior to 60 days after You have
242
+
come back into compliance. Moreover, Your grants from a particular
243
+
Contributor are reinstated on an ongoing basis if such Contributor
244
+
notifies You of the non-compliance by some reasonable means, this is the
245
+
first time You have received notice of non-compliance with this License
246
+
from such Contributor, and You become compliant prior to 30 days after
247
+
Your receipt of the notice.
248
+
249
+
5.2. If You initiate litigation against any entity by asserting a patent
250
+
infringement claim (excluding declaratory judgment actions,
251
+
counter-claims, and cross-claims) alleging that a Contributor Version
252
+
directly or indirectly infringes any patent, then the rights granted to
253
+
You by any and all Contributors for the Covered Software under Section
254
+
2.1 of this License shall terminate.
255
+
256
+
5.3. In the event of termination under Sections 5.1 or 5.2 above, all
257
+
end user license agreements (excluding distributors and resellers) which
258
+
have been validly granted by You or Your distributors under this License
259
+
prior to termination shall survive termination.
260
+
261
+
************************************************************************
262
+
* *
263
+
* 6. Disclaimer of Warranty *
264
+
* ------------------------- *
265
+
* *
266
+
* Covered Software is provided under this License on an "as is" *
267
+
* basis, without warranty of any kind, either expressed, implied, or *
268
+
* statutory, including, without limitation, warranties that the *
269
+
* Covered Software is free of defects, merchantable, fit for a *
270
+
* particular purpose or non-infringing. The entire risk as to the *
271
+
* quality and performance of the Covered Software is with You. *
272
+
* Should any Covered Software prove defective in any respect, You *
273
+
* (not any Contributor) assume the cost of any necessary servicing, *
274
+
* repair, or correction. This disclaimer of warranty constitutes an *
275
+
* essential part of this License. No use of any Covered Software is *
276
+
* authorized under this License except under this disclaimer. *
277
+
* *
278
+
************************************************************************
279
+
280
+
************************************************************************
281
+
* *
282
+
* 7. Limitation of Liability *
283
+
* -------------------------- *
284
+
* *
285
+
* Under no circumstances and under no legal theory, whether tort *
286
+
* (including negligence), contract, or otherwise, shall any *
287
+
* Contributor, or anyone who distributes Covered Software as *
288
+
* permitted above, be liable to You for any direct, indirect, *
289
+
* special, incidental, or consequential damages of any character *
290
+
* including, without limitation, damages for lost profits, loss of *
291
+
* goodwill, work stoppage, computer failure or malfunction, or any *
292
+
* and all other commercial damages or losses, even if such party *
293
+
* shall have been informed of the possibility of such damages. This *
294
+
* limitation of liability shall not apply to liability for death or *
295
+
* personal injury resulting from such party's negligence to the *
296
+
* extent applicable law prohibits such limitation. Some *
297
+
* jurisdictions do not allow the exclusion or limitation of *
298
+
* incidental or consequential damages, so this exclusion and *
299
+
* limitation may not apply to You. *
300
+
* *
301
+
************************************************************************
302
+
303
+
8. Litigation
304
+
-------------
305
+
306
+
Any litigation relating to this License may be brought only in the
307
+
courts of a jurisdiction where the defendant maintains its principal
308
+
place of business and such litigation shall be governed by laws of that
309
+
jurisdiction, without reference to its conflict-of-law provisions.
310
+
Nothing in this Section shall prevent a party's ability to bring
311
+
cross-claims or counter-claims.
312
+
313
+
9. Miscellaneous
314
+
----------------
315
+
316
+
This License represents the complete agreement concerning the subject
317
+
matter hereof. If any provision of this License is held to be
318
+
unenforceable, such provision shall be reformed only to the extent
319
+
necessary to make it enforceable. Any law or regulation which provides
320
+
that the language of a contract shall be construed against the drafter
321
+
shall not be used to construe this License against a Contributor.
322
+
323
+
10. Versions of the License
324
+
---------------------------
325
+
326
+
10.1. New Versions
327
+
328
+
Mozilla Foundation is the license steward. Except as provided in Section
329
+
10.3, no one other than the license steward has the right to modify or
330
+
publish new versions of this License. Each version will be given a
331
+
distinguishing version number.
332
+
333
+
10.2. Effect of New Versions
334
+
335
+
You may distribute the Covered Software under the terms of the version
336
+
of the License under which You originally received the Covered Software,
337
+
or under the terms of any subsequent version published by the license
338
+
steward.
339
+
340
+
10.3. Modified Versions
341
+
342
+
If you create software not governed by this License, and you want to
343
+
create a new license for such software, you may create and use a
344
+
modified version of this License if you rename the license and remove
345
+
any references to the name of the license steward (except to note that
346
+
such modified license differs from this License).
347
+
348
+
10.4. Distributing Source Code Form that is Incompatible With Secondary
349
+
Licenses
350
+
351
+
If You choose to distribute Source Code Form that is Incompatible With
352
+
Secondary Licenses under the terms of this version of the License, the
353
+
notice described in Exhibit B of this License must be attached.
354
+
355
+
Exhibit A - Source Code Form License Notice
356
+
-------------------------------------------
357
+
358
+
This Source Code Form is subject to the terms of the Mozilla Public
359
+
License, v. 2.0. If a copy of the MPL was not distributed with this
360
+
file, You can obtain one at http://mozilla.org/MPL/2.0/.
361
+
362
+
If it is not possible or desirable to put the notice in a particular
363
+
file, then You may include the notice in a location (such as a LICENSE
364
+
file in a relevant directory) where a recipient would be likely to look
365
+
for such a notice.
366
+
367
+
You may add additional accurate notices of copyright ownership.
368
+
369
+
Exhibit B - "Incompatible With Secondary Licenses" Notice
370
+
---------------------------------------------------------
371
+
372
+
This Source Code Form is "Incompatible With Secondary Licenses", as
373
+
defined by the Mozilla Public License, v. 2.0.
+74
-26
README.md
+74
-26
README.md
···
2
2
3
3
A suite of Rust crates for the AT Protocol.
4
4
5
+
[](https://crates.io/crates/jacquard) [](https://docs.rs/jacquard) [](./LICENSE)
6
+
5
7
## Goals
6
8
7
9
- Validated, spec-compliant, easy to work with, and performant baseline types (including typed at:// uris)
···
13
15
- didDoc type with helper methods for getting handles, multikey, and PDS endpoint
14
16
- use as much or as little from the crates as you need
15
17
18
+
19
+
## Example
20
+
21
+
Dead simple api client. Logs in, prints the latest 5 posts from your timeline.
22
+
23
+
```rust
24
+
use clap::Parser;
25
+
use jacquard::CowStr;
26
+
use jacquard::api::app_bsky::feed::get_timeline::GetTimeline;
27
+
use jacquard::api::com_atproto::server::create_session::CreateSession;
28
+
use jacquard::client::{AuthenticatedClient, Session, XrpcClient};
29
+
use miette::IntoDiagnostic;
30
+
31
+
#[derive(Parser, Debug)]
32
+
#[command(author, version, about = "Jacquard - AT Protocol client demo")]
33
+
struct Args {
34
+
/// Username/handle (e.g., alice.mosphere.at)
35
+
#[arg(short, long)]
36
+
username: CowStr<'static>,
37
+
38
+
/// PDS URL (e.g., https://bsky.social)
39
+
#[arg(long, default_value = "https://bsky.social")]
40
+
pds: CowStr<'static>,
41
+
42
+
/// App password
43
+
#[arg(short, long)]
44
+
password: CowStr<'static>,
45
+
}
46
+
47
+
#[tokio::main]
48
+
async fn main() -> miette::Result<()> {
49
+
let args = Args::parse();
50
+
51
+
// Create HTTP client
52
+
let mut client = AuthenticatedClient::new(reqwest::Client::new(), args.pds);
53
+
54
+
// Create session
55
+
let session = Session::from(
56
+
client
57
+
.send(
58
+
CreateSession::new()
59
+
.identifier(args.username)
60
+
.password(args.password)
61
+
.build(),
62
+
)
63
+
.await?
64
+
.into_output()?,
65
+
);
66
+
67
+
println!("logged in as {} ({})", session.handle, session.did);
68
+
client.set_session(session);
69
+
70
+
// Fetch timeline
71
+
println!("\nfetching timeline...");
72
+
let timeline = client
73
+
.send(GetTimeline::new().limit(5).build())
74
+
.await?
75
+
.into_output()?;
76
+
77
+
println!("\ntimeline ({} posts):", timeline.feed.len());
78
+
for (i, post) in timeline.feed.iter().enumerate() {
79
+
println!("\n{}. by {}", i + 1, post.post.author.handle);
80
+
println!(
81
+
" {}",
82
+
serde_json::to_string_pretty(&post.post.record).into_diagnostic()?
83
+
);
84
+
}
85
+
86
+
Ok(())
87
+
}
88
+
```
89
+
16
90
## Development
17
91
18
92
This repo uses [Flakes](https://nixos.asia/en/flakes) from the get-go.
···
29
103
```
30
104
31
105
There's also a [`justfile`](https://just.systems/) for Makefile-esque commands to be run inside of the devShell, and you can generally `cargo ...` or `just ...` whatever just fine if you don't want to use Nix and have the prerequisites installed.
32
-
33
-
34
-
35
-
### String types
36
-
Something of a note to self. Developing a pattern with the string types (may macro-ify at some point). Each needs:
37
-
- new(): constructing from a string slice with the right lifetime that borrows
38
-
- new_owned(): constructing from an impl AsRef<str>, taking ownership
39
-
- new_static(): construction from a &'static str, using SmolStr's/CowStr's new_static() constructor to not allocate
40
-
- raw(): same as new() but panics instead of erroring
41
-
- unchecked(): same as new() but doesn't validate. marked unsafe.
42
-
- as_str(): does what it says on the tin
43
-
#### Traits:
44
-
- Serialize + Deserialize (custom impl for latter, sometimes for former)
45
-
- FromStr
46
-
- Display
47
-
- Debug, PartialEq, Eq, Hash, Clone
48
-
- From<T> for String, CowStr, SmolStr,
49
-
- From<String>, From<CowStr>, From<SmolStr>, or TryFrom if likely enough to fail in practice to make panics common
50
-
- AsRef<str>
51
-
- Deref with Target = str (usually)
52
-
53
-
Use `#[repr(transparent)]` as much as possible. Main exception is at-uri type and components.
54
-
Use SmolStr directly as the inner type if most or all of the instances will be under 24 bytes, save lifetime headaches.
55
-
Use CowStr for longer to allow for borrowing from input.
56
-
57
-
TODO: impl IntoStatic trait to take ownership of string types
+21
-16
crates/jacquard/Cargo.toml
+21
-16
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.workspace = true
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
+
exclude.workspace = true
12
+
license-file.workspace = true
8
13
9
14
[features]
10
15
default = ["api_all"]
···
21
26
path = "src/main.rs"
22
27
23
28
[dependencies]
24
-
bytes = "1.10"
25
-
clap = { workspace = true }
26
-
http = "1.3.1"
29
+
bytes.workspace = true
30
+
clap.workspace = true
31
+
http.workspace = true
27
32
jacquard-api = { version = "0.1.0", path = "../jacquard-api" }
28
-
jacquard-common = { path = "../jacquard-common" }
29
-
jacquard-derive = { path = "../jacquard-derive", optional = true }
30
-
miette = "7.6.0"
31
-
reqwest = { version = "0.12.23", default-features = false, features = ["charset", "http2", "json", "system-proxy", "gzip", "rustls-tls"] }
32
-
serde = { version = "1.0", features = ["derive"] }
33
-
serde_html_form = "0.2"
34
-
serde_ipld_dagcbor = "0.6.4"
35
-
serde_json = "1.0"
36
-
thiserror = "2.0"
33
+
jacquard-common = { version = "0.1.0", path = "../jacquard-common" }
34
+
jacquard-derive = { version = "0.1.0", path = "../jacquard-derive", optional = true }
35
+
miette.workspace = true
36
+
reqwest = { workspace = true, features = ["charset", "http2", "json", "system-proxy", "gzip", "rustls-tls"] }
37
+
serde.workspace = true
38
+
serde_html_form.workspace = true
39
+
serde_ipld_dagcbor.workspace = true
40
+
serde_json.workspace = true
41
+
thiserror.workspace = true
37
42
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }
+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
}
+54
-13
crates/jacquard/src/client.rs
+54
-13
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
···
5
10
use std::future::Future;
6
11
7
12
use bytes::Bytes;
8
-
pub use error::{ClientError, Result};
13
+
pub use error::{ClientError, Result, TransportError};
9
14
use http::{
10
15
HeaderName, HeaderValue, Request,
11
16
header::{AUTHORIZATION, CONTENT_TYPE, InvalidHeaderValue},
···
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
···
134
152
R: XrpcRequest,
135
153
C: XrpcClient + ?Sized,
136
154
{
137
-
// Build URI: base_uri + /xrpc/ + NSID
138
-
let mut uri = format!("{}/xrpc/{}", client.base_uri(), R::NSID);
155
+
// Configure the XRPC endpoint
156
+
let mut url: reqwest::Url = client
157
+
.base_uri()
158
+
.parse()
159
+
.map_err(|e| error::EncodeError::Other(format!("failed to parse base_uri: {e}").into()))?;
160
+
url.set_path(&format!("/xrpc/{}", R::NSID));
139
161
140
162
// Add query parameters for Query methods
141
163
if let XrpcMethod::Query = R::METHOD {
142
164
let qs = serde_html_form::to_string(&request).map_err(error::EncodeError::from)?;
143
165
if !qs.is_empty() {
144
-
uri.push('?');
145
-
uri.push_str(&qs);
166
+
url.set_query(Some(&qs))
146
167
}
147
168
}
148
169
···
152
173
XrpcMethod::Procedure(_) => http::Method::POST,
153
174
};
154
175
155
-
let mut builder = Request::builder().method(method).uri(&uri);
176
+
let mut builder = Request::builder().method(method).uri(&url.to_string());
156
177
157
178
// Add Content-Type for procedures
158
179
if let XrpcMethod::Procedure(encoding) = R::METHOD {
···
210
231
Ok(Response::new(buffer, status))
211
232
}
212
233
213
-
/// Session information from createSession
234
+
/// Session information from `com.atproto.server.createSession`
235
+
///
236
+
/// Contains the access and refresh tokens along with user identity information.
214
237
#[derive(Debug, Clone)]
215
238
pub struct Session {
239
+
/// Access token (JWT) used for authenticated requests
216
240
pub access_jwt: CowStr<'static>,
241
+
/// Refresh token (JWT) used to obtain new access tokens
217
242
pub refresh_jwt: CowStr<'static>,
243
+
/// User's DID (Decentralized Identifier)
218
244
pub did: Did<'static>,
245
+
/// User's handle (e.g., "alice.bsky.social")
219
246
pub handle: Handle<'static>,
220
247
}
221
248
···
232
259
}
233
260
}
234
261
235
-
/// Authenticated XRPC client that includes session tokens
262
+
/// Authenticated XRPC client wrapper that manages session tokens
263
+
///
264
+
/// Wraps an HTTP client and adds automatic Bearer token authentication for XRPC requests.
265
+
/// Handles both access tokens for regular requests and refresh tokens for session refresh.
236
266
pub struct AuthenticatedClient<C> {
237
267
client: C,
238
268
base_uri: CowStr<'static>,
···
241
271
242
272
impl<C> AuthenticatedClient<C> {
243
273
/// Create a new authenticated client with a base URI
274
+
///
275
+
/// # Example
276
+
/// ```ignore
277
+
/// let client = AuthenticatedClient::new(
278
+
/// reqwest::Client::new(),
279
+
/// CowStr::from("https://bsky.social")
280
+
/// );
281
+
/// ```
244
282
pub fn new(client: C, base_uri: CowStr<'static>) -> Self {
245
283
Self {
246
284
client,
···
249
287
}
250
288
}
251
289
252
-
/// Set the session
290
+
/// Set the session obtained from `createSession` or `refreshSession`
253
291
pub fn set_session(&mut self, session: Session) {
254
292
self.session = Some(session);
255
293
}
256
294
257
-
/// Get the current session
295
+
/// Get the current session if one exists
258
296
pub fn session(&self) -> Option<&Session> {
259
297
self.session.as_ref()
260
298
}
261
299
262
-
/// Clear the session
300
+
/// Clear the current session locally
301
+
///
302
+
/// Note: This only clears the local session state. To properly revoke the session
303
+
/// server-side, use `com.atproto.server.deleteSession` before calling this.
263
304
pub fn clear_session(&mut self) {
264
305
self.session = None;
265
306
}
+94
-1
crates/jacquard/src/lib.rs
+94
-1
crates/jacquard/src/lib.rs
···
1
+
//! # Jacquard
2
+
//!
3
+
//! A suite of Rust crates for the AT Protocol.
4
+
//!
5
+
//!
6
+
//! ## Goals
7
+
//!
8
+
//! - Validated, spec-compliant, easy to work with, and performant baseline types (including typed at:// uris)
9
+
//! - Batteries-included, but easily replaceable batteries.
10
+
//! - Easy to extend with custom lexicons
11
+
//! - lexicon Value type for working with unknown atproto data (dag-cbor or json)
12
+
//! - order of magnitude less boilerplate than some existing crates
13
+
//! - either the codegen produces code that's easy to work with, or there are good handwritten wrappers
14
+
//! - didDoc type with helper methods for getting handles, multikey, and PDS endpoint
15
+
//! - use as much or as little from the crates as you need
16
+
//!
17
+
//!
18
+
//! ## Example
19
+
//!
20
+
//! Dead simple api client. Logs in, prints the latest 5 posts from your timeline.
21
+
//!
22
+
//! ```rust
23
+
//! # use clap::Parser;
24
+
//! # use jacquard::CowStr;
25
+
//! use jacquard::api::app_bsky::feed::get_timeline::GetTimeline;
26
+
//! use jacquard::api::com_atproto::server::create_session::CreateSession;
27
+
//! use jacquard::client::{AuthenticatedClient, Session, XrpcClient};
28
+
//! # use miette::IntoDiagnostic;
29
+
//!
30
+
//! # #[derive(Parser, Debug)]
31
+
//! # #[command(author, version, about = "Jacquard - AT Protocol client demo")]
32
+
//! # struct Args {
33
+
//! # /// Username/handle (e.g., alice.mosphere.at)
34
+
//! # #[arg(short, long)]
35
+
//! # username: CowStr<'static>,
36
+
//! #
37
+
//! # /// PDS URL (e.g., https://bsky.social)
38
+
//! # #[arg(long, default_value = "https://bsky.social")]
39
+
//! # pds: CowStr<'static>,
40
+
//! #
41
+
//! # /// App password
42
+
//! # #[arg(short, long)]
43
+
//! # password: CowStr<'static>,
44
+
//! # }
45
+
//!
46
+
//! #[tokio::main]
47
+
//! async fn main() -> miette::Result<()> {
48
+
//! let args = Args::parse();
49
+
//!
50
+
//! // Create HTTP client
51
+
//! let mut client = AuthenticatedClient::new(reqwest::Client::new(), args.pds);
52
+
//!
53
+
//! // Create session
54
+
//! let session = Session::from(
55
+
//! client
56
+
//! .send(
57
+
//! CreateSession::new()
58
+
//! .identifier(args.username)
59
+
//! .password(args.password)
60
+
//! .build(),
61
+
//! )
62
+
//! .await?
63
+
//! .into_output()?,
64
+
//! );
65
+
//!
66
+
//! println!("logged in as {} ({})", session.handle, session.did);
67
+
//! client.set_session(session);
68
+
//!
69
+
//! // Fetch timeline
70
+
//! println!("\nfetching timeline...");
71
+
//! let timeline = client
72
+
//! .send(GetTimeline::new().limit(5).build())
73
+
//! .await?
74
+
//! .into_output()?;
75
+
//!
76
+
//! println!("\ntimeline ({} posts):", timeline.feed.len());
77
+
//! for (i, post) in timeline.feed.iter().enumerate() {
78
+
//! println!("\n{}. by {}", i + 1, post.post.author.handle);
79
+
//! println!(
80
+
//! " {}",
81
+
//! serde_json::to_string_pretty(&post.post.record).into_diagnostic()?
82
+
//! );
83
+
//! }
84
+
//!
85
+
//! Ok(())
86
+
//! }
87
+
//! ```
88
+
//!
89
+
90
+
#![warn(missing_docs)]
91
+
92
+
/// XRPC client traits and basic implementation
1
93
pub mod client;
2
94
3
-
// Re-export common types
4
95
#[cfg(feature = "api")]
96
+
/// If enabled, re-export the generated api crate
5
97
pub use jacquard_api as api;
6
98
pub use jacquard_common::*;
7
99
8
100
#[cfg(feature = "derive")]
101
+
/// if enabled, reexport the attribute macros
9
102
pub use jacquard_derive::*;
+19
-17
crates/jacquard/src/main.rs
+19
-17
crates/jacquard/src/main.rs
···
1
1
use clap::Parser;
2
+
use jacquard::CowStr;
3
+
use jacquard::api::app_bsky::feed::get_timeline::GetTimeline;
4
+
use jacquard::api::com_atproto::server::create_session::CreateSession;
2
5
use jacquard::client::{AuthenticatedClient, Session, XrpcClient};
3
-
use jacquard_api::app_bsky::feed::get_timeline::GetTimeline;
4
-
use jacquard_api::com_atproto::server::create_session::CreateSession;
5
-
use jacquard_common::CowStr;
6
6
use miette::IntoDiagnostic;
7
7
8
8
#[derive(Parser, Debug)]
···
20
20
#[arg(short, long)]
21
21
password: CowStr<'static>,
22
22
}
23
-
24
23
#[tokio::main]
25
24
async fn main() -> miette::Result<()> {
26
25
let args = Args::parse();
27
26
28
27
// Create HTTP client
29
-
let http = reqwest::Client::new();
30
-
let mut client = AuthenticatedClient::new(http, CowStr::from(args.pds));
28
+
let mut client = AuthenticatedClient::new(reqwest::Client::new(), args.pds);
31
29
32
30
// Create session
33
-
println!("logging in as {}...", args.username);
34
-
let create_session = CreateSession::new()
35
-
.identifier(args.username)
36
-
.password(args.password)
37
-
.build();
38
-
39
-
let session_output = client.send(create_session).await?.into_output()?;
40
-
let session = Session::from(session_output);
31
+
let session = Session::from(
32
+
client
33
+
.send(
34
+
CreateSession::new()
35
+
.identifier(args.username)
36
+
.password(args.password)
37
+
.build(),
38
+
)
39
+
.await?
40
+
.into_output()?,
41
+
);
41
42
42
43
println!("logged in as {} ({})", session.handle, session.did);
43
44
client.set_session(session);
44
45
45
46
// Fetch timeline
46
47
println!("\nfetching timeline...");
47
-
let timeline_req = GetTimeline::new().limit(5).build();
48
-
49
-
let timeline = client.send(timeline_req).await?.into_output()?;
48
+
let timeline = client
49
+
.send(GetTimeline::new().limit(5).build())
50
+
.await?
51
+
.into_output()?;
50
52
51
53
println!("\ntimeline ({} posts):", timeline.feed.len());
52
54
for (i, post) in timeline.feed.iter().enumerate() {
+6
-6
crates/jacquard-api/Cargo.toml
+6
-6
crates/jacquard-api/Cargo.toml
···
1
1
[package]
2
2
name = "jacquard-api"
3
+
description = "Generated AT Protocol API bindings for Jacquard"
3
4
edition.workspace = true
4
5
version.workspace = true
5
6
authors.workspace = true
···
7
8
keywords.workspace = true
8
9
categories.workspace = true
9
10
readme.workspace = true
10
-
documentation.workspace = true
11
11
exclude.workspace = true
12
-
description.workspace = true
12
+
license-file.workspace = true
13
13
14
14
[features]
15
15
default = [ "com_atproto"]
···
20
20
21
21
[dependencies]
22
22
bon = "3"
23
-
bytes = { version = "1.10.1", features = ["serde"] }
23
+
bytes = { workspace = true, features = ["serde"] }
24
24
jacquard-common = { version = "0.1.0", path = "../jacquard-common" }
25
25
jacquard-derive = { version = "0.1.0", path = "../jacquard-derive" }
26
-
miette = "7.6.0"
27
-
serde = { version = "1.0.228", features = ["derive"] }
28
-
thiserror = "2.0.17"
26
+
miette.workspace = true
27
+
serde.workspace = true
28
+
thiserror.workspace = true
+11
-12
crates/jacquard-common/Cargo.toml
+11
-12
crates/jacquard-common/Cargo.toml
···
1
1
[package]
2
2
name = "jacquard-common"
3
+
description = "Core AT Protocol types and utilities for Jacquard"
3
4
edition.workspace = true
4
5
version.workspace = true
5
6
authors.workspace = true
···
7
8
keywords.workspace = true
8
9
categories.workspace = true
9
10
readme.workspace = true
10
-
documentation.workspace = true
11
11
exclude.workspace = true
12
-
description.workspace = true
13
-
12
+
license-file.workspace = true
14
13
15
14
16
15
[dependencies]
17
16
base64 = "0.22.1"
18
-
bytes = "1.10.1"
17
+
bytes.workspace = true
19
18
chrono = "0.4.42"
20
19
cid = { version = "0.11.1", features = ["serde", "std"] }
21
20
enum_dispatch = "0.3.13"
22
21
ipld-core = { version = "0.4.2", features = ["serde"] }
23
22
langtag = { version = "0.4.0", features = ["serde"] }
24
-
miette = "7.6.0"
23
+
miette.workspace = true
25
24
multibase = "0.9.1"
26
25
multihash = "0.19.3"
27
26
num-traits = "0.2.19"
28
27
ouroboros = "0.18.5"
29
28
rand = "0.9.2"
30
29
regex = "1.11.3"
31
-
serde = { version = "1.0.227", features = ["derive"] }
32
-
serde_html_form = "0.2.8"
33
-
serde_json = "1.0.145"
34
-
serde_with = "3.14.1"
35
-
smol_str = { version = "0.3.2", features = ["serde"] }
36
-
thiserror = "2.0.16"
37
-
url = "2.5.7"
30
+
serde.workspace = true
31
+
serde_html_form.workspace = true
32
+
serde_json.workspace = true
33
+
serde_with.workspace = true
34
+
smol_str.workspace = true
35
+
thiserror.workspace = true
36
+
url.workspace = true
+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
}
+12
-5
crates/jacquard-common/src/lib.rs
+12
-5
crates/jacquard-common/src/lib.rs
···
1
+
//! Common types for the jacquard implementation of atproto
2
+
3
+
#![warn(missing_docs)]
4
+
pub use cowstr::CowStr;
5
+
pub use into_static::IntoStatic;
6
+
pub use smol_str;
7
+
pub use url;
8
+
9
+
/// A copy-on-write immutable string type that uses [`SmolStr`] for
10
+
/// the "owned" variant.
1
11
#[macro_use]
2
12
pub mod cowstr;
3
13
#[macro_use]
14
+
/// Trait for taking ownership of most borrowed types in jacquard.
4
15
pub mod into_static;
5
16
pub mod macros;
17
+
/// Baseline fundamental AT Protocol data types.
6
18
pub mod types;
7
-
8
-
pub use cowstr::CowStr;
9
-
pub use into_static::IntoStatic;
10
-
pub use smol_str;
11
-
pub use url;
+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(),
+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) {
+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
}
+1
crates/jacquard-common/src/types/xrpc.rs
+1
crates/jacquard-common/src/types/xrpc.rs
+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
}
+12
-12
crates/jacquard-derive/Cargo.toml
+12
-12
crates/jacquard-derive/Cargo.toml
···
1
1
[package]
2
2
name = "jacquard-derive"
3
+
description = "Procedural macros for Jacquard lexicon types"
3
4
edition.workspace = true
4
5
version.workspace = true
5
6
authors.workspace = true
···
7
8
keywords.workspace = true
8
9
categories.workspace = true
9
10
readme.workspace = true
10
-
documentation.workspace = true
11
11
exclude.workspace = true
12
-
description.workspace = true
12
+
license-file.workspace = true
13
13
14
14
[lib]
15
15
proc-macro = true
16
16
17
17
[dependencies]
18
-
heck = "0.5.0"
19
-
itertools = "0.14.0"
20
-
prettyplease = "0.2.37"
21
-
proc-macro2 = "1.0.101"
22
-
quote = "1.0.41"
23
-
serde = { version = "1.0.228", features = ["derive"] }
24
-
serde_json = "1.0.145"
25
-
serde_repr = "0.1.20"
26
-
serde_with = "3.14.1"
27
-
syn = "2.0.106"
18
+
heck.workspace = true
19
+
itertools.workspace = true
20
+
prettyplease.workspace = true
21
+
proc-macro2.workspace = true
22
+
quote.workspace = true
23
+
serde.workspace = true
24
+
serde_json.workspace = true
25
+
serde_repr.workspace = true
26
+
serde_with.workspace = true
27
+
syn.workspace = true
28
28
29
29
30
30
[dev-dependencies]
+18
-15
crates/jacquard-lexicon/Cargo.toml
+18
-15
crates/jacquard-lexicon/Cargo.toml
···
1
1
[package]
2
2
name = "jacquard-lexicon"
3
+
description = "Lexicon schema parsing and code generation for Jacquard"
3
4
edition.workspace = true
4
5
version.workspace = true
5
6
authors.workspace = true
···
7
8
keywords.workspace = true
8
9
categories.workspace = true
9
10
readme.workspace = true
10
-
documentation.workspace = true
11
11
exclude.workspace = true
12
-
description.workspace = true
12
+
license-file.workspace = true
13
13
14
14
[[bin]]
15
15
name = "jacquard-codegen"
16
16
path = "src/bin/codegen.rs"
17
17
18
18
[dependencies]
19
-
clap = { workspace = true }
20
-
heck = "0.5.0"
21
-
itertools = "0.14.0"
19
+
clap.workspace = true
20
+
heck.workspace = true
21
+
itertools.workspace = true
22
22
jacquard-common = { version = "0.1.0", path = "../jacquard-common" }
23
-
miette = { version = "7.6.0", features = ["fancy"] }
24
-
prettyplease = "0.2.37"
25
-
proc-macro2 = "1.0.101"
26
-
quote = "1.0.41"
27
-
serde = { version = "1.0.228", features = ["derive"] }
28
-
serde_json = "1.0.145"
29
-
serde_repr = "0.1.20"
30
-
serde_with = "3.14.1"
31
-
syn = "2.0.106"
32
-
thiserror = "2.0.17"
23
+
miette = { workspace = true, features = ["fancy"] }
24
+
prettyplease.workspace = true
25
+
proc-macro2.workspace = true
26
+
quote.workspace = true
27
+
serde.workspace = true
28
+
serde_json.workspace = true
29
+
serde_repr.workspace = true
30
+
serde_with.workspace = true
31
+
syn.workspace = true
32
+
thiserror.workspace = true
33
+
34
+
[dev-dependencies]
35
+
tempfile = { version = "3.23.0" }
+21
-35
crates/jacquard-lexicon/src/codegen.rs
+21
-35
crates/jacquard-lexicon/src/codegen.rs
···
264
264
let mut fields = Vec::new();
265
265
for (field_name, field_type) in &obj.properties {
266
266
let is_required = required.contains(field_name);
267
-
let field_tokens =
268
-
self.generate_field(nsid, parent_type_name, field_name, field_type, is_required, is_builder)?;
267
+
let field_tokens = self.generate_field(
268
+
nsid,
269
+
parent_type_name,
270
+
field_name,
271
+
field_type,
272
+
is_required,
273
+
is_builder,
274
+
)?;
269
275
fields.push(field_tokens);
270
276
}
271
277
···
1667
1673
LexXrpcParametersProperty::Integer(_) => (quote! { i64 }, false, false),
1668
1674
LexXrpcParametersProperty::String(s) => {
1669
1675
let is_cowstr = s.format.is_none(); // CowStr for plain strings
1670
-
(self.string_to_rust_type(s), self.string_needs_lifetime(s), is_cowstr)
1671
-
}
1672
-
LexXrpcParametersProperty::Unknown(_) => {
1673
-
(quote! { jacquard_common::types::value::Data<'a> }, true, false)
1676
+
(
1677
+
self.string_to_rust_type(s),
1678
+
self.string_needs_lifetime(s),
1679
+
is_cowstr,
1680
+
)
1674
1681
}
1682
+
LexXrpcParametersProperty::Unknown(_) => (
1683
+
quote! { jacquard_common::types::value::Data<'a> },
1684
+
true,
1685
+
false,
1686
+
),
1675
1687
LexXrpcParametersProperty::Array(arr) => {
1676
1688
let needs_lifetime = match &arr.items {
1677
1689
crate::lexicon::LexPrimitiveArrayItem::Boolean(_)
···
2472
2484
LexiconCorpus::load_from_dir("tests/fixtures/test_lexicons").expect("load corpus");
2473
2485
let codegen = CodeGenerator::new(&corpus, "test_generated");
2474
2486
2475
-
let output_dir = std::path::PathBuf::from("target/test_codegen_output");
2487
+
let tmp_dir =
2488
+
tempfile::tempdir().expect("should be able to create temp directory for output");
2489
+
let output_dir = std::path::PathBuf::from(tmp_dir.path());
2476
2490
2477
2491
// Clean up any previous test output
2478
2492
let _ = std::fs::remove_dir_all(&output_dir);
···
2494
2508
.expect("read post.rs");
2495
2509
assert!(post_content.contains("pub struct Post"));
2496
2510
assert!(post_content.contains("jacquard_common"));
2497
-
}
2498
-
2499
-
#[test]
2500
-
#[ignore] // run manually: cargo test test_generate_full_atproto -- --ignored
2501
-
fn test_generate_full_atproto() {
2502
-
let corpus = LexiconCorpus::load_from_dir("tests/fixtures/lexicons/atproto/lexicons")
2503
-
.expect("load atproto corpus");
2504
-
let codegen = CodeGenerator::new(&corpus, "crate");
2505
-
2506
-
let output_dir = std::path::PathBuf::from("../jacquard-api/src");
2507
-
2508
-
// Clean up existing generated code
2509
-
if output_dir.exists() {
2510
-
for entry in std::fs::read_dir(&output_dir).expect("read output dir") {
2511
-
let entry = entry.expect("dir entry");
2512
-
let path = entry.path();
2513
-
if path.is_dir() {
2514
-
std::fs::remove_dir_all(&path).ok();
2515
-
} else if path.extension().map_or(false, |e| e == "rs") {
2516
-
std::fs::remove_file(&path).ok();
2517
-
}
2518
-
}
2519
-
}
2520
-
2521
-
// Generate and write
2522
-
codegen.write_to_disk(&output_dir).expect("write to disk");
2523
-
2524
-
println!("\nโจ Generated full atproto API to {:?}", output_dir);
2525
2511
}
2526
2512
}
-99
crates/jacquard-lexicon/target/test_codegen_output/app_bsky/embed/external.rs
-99
crates/jacquard-lexicon/target/test_codegen_output/app_bsky/embed/external.rs
···
1
-
// @generated by jacquard-lexicon. DO NOT EDIT.
2
-
//
3
-
// Lexicon: app.bsky.embed.external
4
-
//
5
-
// This file was automatically generated from Lexicon schemas.
6
-
// Any manual changes will be overwritten on the next regeneration.
7
-
8
-
#[jacquard_derive::lexicon]
9
-
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq)]
10
-
#[serde(rename_all = "camelCase")]
11
-
pub struct External<'a> {
12
-
#[serde(borrow)]
13
-
pub description: jacquard_common::CowStr<'a>,
14
-
#[serde(skip_serializing_if = "std::option::Option::is_none")]
15
-
#[serde(borrow)]
16
-
pub thumb: std::option::Option<jacquard_common::types::blob::Blob<'a>>,
17
-
#[serde(borrow)]
18
-
pub title: jacquard_common::CowStr<'a>,
19
-
#[serde(borrow)]
20
-
pub uri: jacquard_common::types::string::Uri<'a>,
21
-
}
22
-
23
-
impl jacquard_common::IntoStatic for External<'_> {
24
-
type Output = External<'static>;
25
-
fn into_static(self) -> Self::Output {
26
-
External {
27
-
description: self.description.into_static(),
28
-
thumb: self.thumb.into_static(),
29
-
title: self.title.into_static(),
30
-
uri: self.uri.into_static(),
31
-
extra_data: self.extra_data.into_static(),
32
-
}
33
-
}
34
-
}
35
-
36
-
///A representation of some externally linked content (eg, a URL and 'card'), embedded in a Bluesky record (eg, a post).
37
-
#[jacquard_derive::lexicon]
38
-
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq)]
39
-
#[serde(rename_all = "camelCase")]
40
-
pub struct ExternalRecord<'a> {
41
-
#[serde(borrow)]
42
-
pub external: test_generated::app_bsky::embed::external::External<'a>,
43
-
}
44
-
45
-
impl jacquard_common::IntoStatic for ExternalRecord<'_> {
46
-
type Output = ExternalRecord<'static>;
47
-
fn into_static(self) -> Self::Output {
48
-
ExternalRecord {
49
-
external: self.external.into_static(),
50
-
extra_data: self.extra_data.into_static(),
51
-
}
52
-
}
53
-
}
54
-
55
-
#[jacquard_derive::lexicon]
56
-
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq)]
57
-
#[serde(rename_all = "camelCase")]
58
-
pub struct View<'a> {
59
-
#[serde(borrow)]
60
-
pub external: test_generated::app_bsky::embed::external::ViewExternal<'a>,
61
-
}
62
-
63
-
impl jacquard_common::IntoStatic for View<'_> {
64
-
type Output = View<'static>;
65
-
fn into_static(self) -> Self::Output {
66
-
View {
67
-
external: self.external.into_static(),
68
-
extra_data: self.extra_data.into_static(),
69
-
}
70
-
}
71
-
}
72
-
73
-
#[jacquard_derive::lexicon]
74
-
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq)]
75
-
#[serde(rename_all = "camelCase")]
76
-
pub struct ViewExternal<'a> {
77
-
#[serde(borrow)]
78
-
pub description: jacquard_common::CowStr<'a>,
79
-
#[serde(skip_serializing_if = "std::option::Option::is_none")]
80
-
#[serde(borrow)]
81
-
pub thumb: std::option::Option<jacquard_common::types::string::Uri<'a>>,
82
-
#[serde(borrow)]
83
-
pub title: jacquard_common::CowStr<'a>,
84
-
#[serde(borrow)]
85
-
pub uri: jacquard_common::types::string::Uri<'a>,
86
-
}
87
-
88
-
impl jacquard_common::IntoStatic for ViewExternal<'_> {
89
-
type Output = ViewExternal<'static>;
90
-
fn into_static(self) -> Self::Output {
91
-
ViewExternal {
92
-
description: self.description.into_static(),
93
-
thumb: self.thumb.into_static(),
94
-
title: self.title.into_static(),
95
-
uri: self.uri.into_static(),
96
-
extra_data: self.extra_data.into_static(),
97
-
}
98
-
}
99
-
}
-99
crates/jacquard-lexicon/target/test_codegen_output/app_bsky/embed/images.rs
-99
crates/jacquard-lexicon/target/test_codegen_output/app_bsky/embed/images.rs
···
1
-
// @generated by jacquard-lexicon. DO NOT EDIT.
2
-
//
3
-
// Lexicon: app.bsky.embed.images
4
-
//
5
-
// This file was automatically generated from Lexicon schemas.
6
-
// Any manual changes will be overwritten on the next regeneration.
7
-
8
-
#[jacquard_derive::lexicon]
9
-
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq)]
10
-
#[serde(rename_all = "camelCase")]
11
-
pub struct Image<'a> {
12
-
///Alt text description of the image, for accessibility.
13
-
#[serde(borrow)]
14
-
pub alt: jacquard_common::CowStr<'a>,
15
-
#[serde(skip_serializing_if = "std::option::Option::is_none")]
16
-
#[serde(borrow)]
17
-
pub aspect_ratio: std::option::Option<jacquard_common::types::value::Data<'a>>,
18
-
#[serde(borrow)]
19
-
pub image: jacquard_common::types::blob::Blob<'a>,
20
-
}
21
-
22
-
impl jacquard_common::IntoStatic for Image<'_> {
23
-
type Output = Image<'static>;
24
-
fn into_static(self) -> Self::Output {
25
-
Image {
26
-
alt: self.alt.into_static(),
27
-
aspect_ratio: self.aspect_ratio.into_static(),
28
-
image: self.image.into_static(),
29
-
extra_data: self.extra_data.into_static(),
30
-
}
31
-
}
32
-
}
33
-
34
-
#[jacquard_derive::lexicon]
35
-
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq)]
36
-
#[serde(rename_all = "camelCase")]
37
-
pub struct Images<'a> {
38
-
#[serde(borrow)]
39
-
pub images: Vec<test_generated::app_bsky::embed::images::Image<'a>>,
40
-
}
41
-
42
-
impl jacquard_common::IntoStatic for Images<'_> {
43
-
type Output = Images<'static>;
44
-
fn into_static(self) -> Self::Output {
45
-
Images {
46
-
images: self.images.into_static(),
47
-
extra_data: self.extra_data.into_static(),
48
-
}
49
-
}
50
-
}
51
-
52
-
#[jacquard_derive::lexicon]
53
-
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq)]
54
-
#[serde(rename_all = "camelCase")]
55
-
pub struct View<'a> {
56
-
#[serde(borrow)]
57
-
pub images: Vec<test_generated::app_bsky::embed::images::ViewImage<'a>>,
58
-
}
59
-
60
-
impl jacquard_common::IntoStatic for View<'_> {
61
-
type Output = View<'static>;
62
-
fn into_static(self) -> Self::Output {
63
-
View {
64
-
images: self.images.into_static(),
65
-
extra_data: self.extra_data.into_static(),
66
-
}
67
-
}
68
-
}
69
-
70
-
#[jacquard_derive::lexicon]
71
-
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq)]
72
-
#[serde(rename_all = "camelCase")]
73
-
pub struct ViewImage<'a> {
74
-
///Alt text description of the image, for accessibility.
75
-
#[serde(borrow)]
76
-
pub alt: jacquard_common::CowStr<'a>,
77
-
#[serde(skip_serializing_if = "std::option::Option::is_none")]
78
-
#[serde(borrow)]
79
-
pub aspect_ratio: std::option::Option<jacquard_common::types::value::Data<'a>>,
80
-
///Fully-qualified URL where a large version of the image can be fetched. May or may not be the exact original blob. For example, CDN location provided by the App View.
81
-
#[serde(borrow)]
82
-
pub fullsize: jacquard_common::types::string::Uri<'a>,
83
-
///Fully-qualified URL where a thumbnail of the image can be fetched. For example, CDN location provided by the App View.
84
-
#[serde(borrow)]
85
-
pub thumb: jacquard_common::types::string::Uri<'a>,
86
-
}
87
-
88
-
impl jacquard_common::IntoStatic for ViewImage<'_> {
89
-
type Output = ViewImage<'static>;
90
-
fn into_static(self) -> Self::Output {
91
-
ViewImage {
92
-
alt: self.alt.into_static(),
93
-
aspect_ratio: self.aspect_ratio.into_static(),
94
-
fullsize: self.fullsize.into_static(),
95
-
thumb: self.thumb.into_static(),
96
-
extra_data: self.extra_data.into_static(),
97
-
}
98
-
}
99
-
}
-169
crates/jacquard-lexicon/target/test_codegen_output/app_bsky/embed/record.rs
-169
crates/jacquard-lexicon/target/test_codegen_output/app_bsky/embed/record.rs
···
1
-
// @generated by jacquard-lexicon. DO NOT EDIT.
2
-
//
3
-
// Lexicon: app.bsky.embed.record
4
-
//
5
-
// This file was automatically generated from Lexicon schemas.
6
-
// Any manual changes will be overwritten on the next regeneration.
7
-
8
-
#[jacquard_derive::lexicon]
9
-
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq)]
10
-
#[serde(rename_all = "camelCase")]
11
-
pub struct Record<'a> {
12
-
#[serde(borrow)]
13
-
pub record: test_generated::com_atproto::repo::strong_ref::StrongRef<'a>,
14
-
}
15
-
16
-
impl jacquard_common::IntoStatic for Record<'_> {
17
-
type Output = Record<'static>;
18
-
fn into_static(self) -> Self::Output {
19
-
Record {
20
-
record: self.record.into_static(),
21
-
extra_data: self.extra_data.into_static(),
22
-
}
23
-
}
24
-
}
25
-
26
-
#[jacquard_derive::lexicon]
27
-
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq)]
28
-
#[serde(rename_all = "camelCase")]
29
-
pub struct View<'a> {
30
-
#[serde(borrow)]
31
-
pub record: ViewRecordRecord<'a>,
32
-
}
33
-
34
-
#[jacquard_derive::open_union]
35
-
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq)]
36
-
#[serde(tag = "$type")]
37
-
#[serde(bound(deserialize = "'de: 'a"))]
38
-
pub enum ViewRecordRecord<'a> {}
39
-
impl jacquard_common::IntoStatic for ViewRecordRecord<'_> {
40
-
type Output = ViewRecordRecord<'static>;
41
-
fn into_static(self) -> Self::Output {
42
-
match self {
43
-
ViewRecordRecord::Unknown(v) => ViewRecordRecord::Unknown(v.into_static()),
44
-
}
45
-
}
46
-
}
47
-
48
-
impl jacquard_common::IntoStatic for View<'_> {
49
-
type Output = View<'static>;
50
-
fn into_static(self) -> Self::Output {
51
-
View {
52
-
record: self.record.into_static(),
53
-
extra_data: self.extra_data.into_static(),
54
-
}
55
-
}
56
-
}
57
-
58
-
#[jacquard_derive::lexicon]
59
-
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq)]
60
-
#[serde(rename_all = "camelCase")]
61
-
pub struct ViewBlocked<'a> {
62
-
#[serde(borrow)]
63
-
pub author: jacquard_common::types::value::Data<'a>,
64
-
pub blocked: bool,
65
-
#[serde(borrow)]
66
-
pub uri: jacquard_common::types::string::AtUri<'a>,
67
-
}
68
-
69
-
impl jacquard_common::IntoStatic for ViewBlocked<'_> {
70
-
type Output = ViewBlocked<'static>;
71
-
fn into_static(self) -> Self::Output {
72
-
ViewBlocked {
73
-
author: self.author.into_static(),
74
-
blocked: self.blocked.into_static(),
75
-
uri: self.uri.into_static(),
76
-
extra_data: self.extra_data.into_static(),
77
-
}
78
-
}
79
-
}
80
-
81
-
#[jacquard_derive::lexicon]
82
-
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq)]
83
-
#[serde(rename_all = "camelCase")]
84
-
pub struct ViewDetached<'a> {
85
-
pub detached: bool,
86
-
#[serde(borrow)]
87
-
pub uri: jacquard_common::types::string::AtUri<'a>,
88
-
}
89
-
90
-
impl jacquard_common::IntoStatic for ViewDetached<'_> {
91
-
type Output = ViewDetached<'static>;
92
-
fn into_static(self) -> Self::Output {
93
-
ViewDetached {
94
-
detached: self.detached.into_static(),
95
-
uri: self.uri.into_static(),
96
-
extra_data: self.extra_data.into_static(),
97
-
}
98
-
}
99
-
}
100
-
101
-
#[jacquard_derive::lexicon]
102
-
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq)]
103
-
#[serde(rename_all = "camelCase")]
104
-
pub struct ViewNotFound<'a> {
105
-
pub not_found: bool,
106
-
#[serde(borrow)]
107
-
pub uri: jacquard_common::types::string::AtUri<'a>,
108
-
}
109
-
110
-
impl jacquard_common::IntoStatic for ViewNotFound<'_> {
111
-
type Output = ViewNotFound<'static>;
112
-
fn into_static(self) -> Self::Output {
113
-
ViewNotFound {
114
-
not_found: self.not_found.into_static(),
115
-
uri: self.uri.into_static(),
116
-
extra_data: self.extra_data.into_static(),
117
-
}
118
-
}
119
-
}
120
-
121
-
#[jacquard_derive::lexicon]
122
-
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq)]
123
-
#[serde(rename_all = "camelCase")]
124
-
pub struct ViewRecord<'a> {
125
-
#[serde(borrow)]
126
-
pub author: jacquard_common::types::value::Data<'a>,
127
-
#[serde(borrow)]
128
-
pub cid: jacquard_common::types::string::Cid<'a>,
129
-
#[serde(skip_serializing_if = "std::option::Option::is_none")]
130
-
#[serde(borrow)]
131
-
pub embeds: std::option::Option<Vec<jacquard_common::types::value::Data<'a>>>,
132
-
pub indexed_at: jacquard_common::types::string::Datetime,
133
-
#[serde(skip_serializing_if = "std::option::Option::is_none")]
134
-
#[serde(borrow)]
135
-
pub labels: std::option::Option<Vec<test_generated::com_atproto::label::Label<'a>>>,
136
-
#[serde(skip_serializing_if = "std::option::Option::is_none")]
137
-
pub like_count: std::option::Option<i64>,
138
-
#[serde(skip_serializing_if = "std::option::Option::is_none")]
139
-
pub quote_count: std::option::Option<i64>,
140
-
#[serde(skip_serializing_if = "std::option::Option::is_none")]
141
-
pub reply_count: std::option::Option<i64>,
142
-
#[serde(skip_serializing_if = "std::option::Option::is_none")]
143
-
pub repost_count: std::option::Option<i64>,
144
-
#[serde(borrow)]
145
-
pub uri: jacquard_common::types::string::AtUri<'a>,
146
-
///The record data itself.
147
-
#[serde(borrow)]
148
-
pub value: jacquard_common::types::value::Data<'a>,
149
-
}
150
-
151
-
impl jacquard_common::IntoStatic for ViewRecord<'_> {
152
-
type Output = ViewRecord<'static>;
153
-
fn into_static(self) -> Self::Output {
154
-
ViewRecord {
155
-
author: self.author.into_static(),
156
-
cid: self.cid.into_static(),
157
-
embeds: self.embeds.into_static(),
158
-
indexed_at: self.indexed_at.into_static(),
159
-
labels: self.labels.into_static(),
160
-
like_count: self.like_count.into_static(),
161
-
quote_count: self.quote_count.into_static(),
162
-
reply_count: self.reply_count.into_static(),
163
-
repost_count: self.repost_count.into_static(),
164
-
uri: self.uri.into_static(),
165
-
value: self.value.into_static(),
166
-
extra_data: self.extra_data.into_static(),
167
-
}
168
-
}
169
-
}
-110
crates/jacquard-lexicon/target/test_codegen_output/app_bsky/embed/record_with_media.rs
-110
crates/jacquard-lexicon/target/test_codegen_output/app_bsky/embed/record_with_media.rs
···
1
-
// @generated by jacquard-lexicon. DO NOT EDIT.
2
-
//
3
-
// Lexicon: app.bsky.embed.recordWithMedia
4
-
//
5
-
// This file was automatically generated from Lexicon schemas.
6
-
// Any manual changes will be overwritten on the next regeneration.
7
-
8
-
#[jacquard_derive::lexicon]
9
-
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq)]
10
-
#[serde(rename_all = "camelCase")]
11
-
pub struct RecordWithMedia<'a> {
12
-
#[serde(borrow)]
13
-
pub media: RecordWithMediaRecordMedia<'a>,
14
-
#[serde(borrow)]
15
-
pub record: test_generated::app_bsky::embed::record::Record<'a>,
16
-
}
17
-
18
-
#[jacquard_derive::open_union]
19
-
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq)]
20
-
#[serde(tag = "$type")]
21
-
#[serde(bound(deserialize = "'de: 'a"))]
22
-
pub enum RecordWithMediaRecordMedia<'a> {
23
-
#[serde(rename = "app.bsky.embed.images")]
24
-
Images(Box<test_generated::app_bsky::embed::images::Images<'a>>),
25
-
#[serde(rename = "app.bsky.embed.video")]
26
-
Video(Box<test_generated::app_bsky::embed::video::Video<'a>>),
27
-
#[serde(rename = "app.bsky.embed.external")]
28
-
External(Box<test_generated::app_bsky::embed::external::ExternalRecord<'a>>),
29
-
}
30
-
31
-
impl jacquard_common::IntoStatic for RecordWithMediaRecordMedia<'_> {
32
-
type Output = RecordWithMediaRecordMedia<'static>;
33
-
fn into_static(self) -> Self::Output {
34
-
match self {
35
-
RecordWithMediaRecordMedia::Images(v) => {
36
-
RecordWithMediaRecordMedia::Images(v.into_static())
37
-
}
38
-
RecordWithMediaRecordMedia::Video(v) => {
39
-
RecordWithMediaRecordMedia::Video(v.into_static())
40
-
}
41
-
RecordWithMediaRecordMedia::External(v) => {
42
-
RecordWithMediaRecordMedia::External(v.into_static())
43
-
}
44
-
RecordWithMediaRecordMedia::Unknown(v) => {
45
-
RecordWithMediaRecordMedia::Unknown(v.into_static())
46
-
}
47
-
}
48
-
}
49
-
}
50
-
51
-
impl jacquard_common::IntoStatic for RecordWithMedia<'_> {
52
-
type Output = RecordWithMedia<'static>;
53
-
fn into_static(self) -> Self::Output {
54
-
RecordWithMedia {
55
-
media: self.media.into_static(),
56
-
record: self.record.into_static(),
57
-
extra_data: self.extra_data.into_static(),
58
-
}
59
-
}
60
-
}
61
-
62
-
#[jacquard_derive::lexicon]
63
-
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq)]
64
-
#[serde(rename_all = "camelCase")]
65
-
pub struct View<'a> {
66
-
#[serde(borrow)]
67
-
pub media: ViewRecordMedia<'a>,
68
-
#[serde(borrow)]
69
-
pub record: test_generated::app_bsky::embed::record::View<'a>,
70
-
}
71
-
72
-
#[jacquard_derive::open_union]
73
-
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq)]
74
-
#[serde(tag = "$type")]
75
-
#[serde(bound(deserialize = "'de: 'a"))]
76
-
pub enum ViewRecordMedia<'a> {
77
-
#[serde(rename = "app.bsky.embed.images#view")]
78
-
ImagesView(Box<test_generated::app_bsky::embed::images::View<'a>>),
79
-
#[serde(rename = "app.bsky.embed.video#view")]
80
-
VideoView(Box<test_generated::app_bsky::embed::video::View<'a>>),
81
-
#[serde(rename = "app.bsky.embed.external#view")]
82
-
ExternalView(Box<test_generated::app_bsky::embed::external::View<'a>>),
83
-
}
84
-
85
-
impl jacquard_common::IntoStatic for ViewRecordMedia<'_> {
86
-
type Output = ViewRecordMedia<'static>;
87
-
fn into_static(self) -> Self::Output {
88
-
match self {
89
-
ViewRecordMedia::ImagesView(v) => {
90
-
ViewRecordMedia::ImagesView(v.into_static())
91
-
}
92
-
ViewRecordMedia::VideoView(v) => ViewRecordMedia::VideoView(v.into_static()),
93
-
ViewRecordMedia::ExternalView(v) => {
94
-
ViewRecordMedia::ExternalView(v.into_static())
95
-
}
96
-
ViewRecordMedia::Unknown(v) => ViewRecordMedia::Unknown(v.into_static()),
97
-
}
98
-
}
99
-
}
100
-
101
-
impl jacquard_common::IntoStatic for View<'_> {
102
-
type Output = View<'static>;
103
-
fn into_static(self) -> Self::Output {
104
-
View {
105
-
media: self.media.into_static(),
106
-
record: self.record.into_static(),
107
-
extra_data: self.extra_data.into_static(),
108
-
}
109
-
}
110
-
}
-93
crates/jacquard-lexicon/target/test_codegen_output/app_bsky/embed/video.rs
-93
crates/jacquard-lexicon/target/test_codegen_output/app_bsky/embed/video.rs
···
1
-
// @generated by jacquard-lexicon. DO NOT EDIT.
2
-
//
3
-
// Lexicon: app.bsky.embed.video
4
-
//
5
-
// This file was automatically generated from Lexicon schemas.
6
-
// Any manual changes will be overwritten on the next regeneration.
7
-
8
-
#[jacquard_derive::lexicon]
9
-
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq)]
10
-
#[serde(rename_all = "camelCase")]
11
-
pub struct Caption<'a> {
12
-
#[serde(borrow)]
13
-
pub file: jacquard_common::types::blob::Blob<'a>,
14
-
pub lang: jacquard_common::types::string::Language,
15
-
}
16
-
17
-
impl jacquard_common::IntoStatic for Caption<'_> {
18
-
type Output = Caption<'static>;
19
-
fn into_static(self) -> Self::Output {
20
-
Caption {
21
-
file: self.file.into_static(),
22
-
lang: self.lang.into_static(),
23
-
extra_data: self.extra_data.into_static(),
24
-
}
25
-
}
26
-
}
27
-
28
-
#[jacquard_derive::lexicon]
29
-
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq)]
30
-
#[serde(rename_all = "camelCase")]
31
-
pub struct Video<'a> {
32
-
///Alt text description of the video, for accessibility.
33
-
#[serde(skip_serializing_if = "std::option::Option::is_none")]
34
-
#[serde(borrow)]
35
-
pub alt: std::option::Option<jacquard_common::CowStr<'a>>,
36
-
#[serde(skip_serializing_if = "std::option::Option::is_none")]
37
-
#[serde(borrow)]
38
-
pub aspect_ratio: std::option::Option<jacquard_common::types::value::Data<'a>>,
39
-
#[serde(skip_serializing_if = "std::option::Option::is_none")]
40
-
#[serde(borrow)]
41
-
pub captions: std::option::Option<
42
-
Vec<test_generated::app_bsky::embed::video::Caption<'a>>,
43
-
>,
44
-
///The mp4 video file. May be up to 100mb, formerly limited to 50mb.
45
-
#[serde(borrow)]
46
-
pub video: jacquard_common::types::blob::Blob<'a>,
47
-
}
48
-
49
-
impl jacquard_common::IntoStatic for Video<'_> {
50
-
type Output = Video<'static>;
51
-
fn into_static(self) -> Self::Output {
52
-
Video {
53
-
alt: self.alt.into_static(),
54
-
aspect_ratio: self.aspect_ratio.into_static(),
55
-
captions: self.captions.into_static(),
56
-
video: self.video.into_static(),
57
-
extra_data: self.extra_data.into_static(),
58
-
}
59
-
}
60
-
}
61
-
62
-
#[jacquard_derive::lexicon]
63
-
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq)]
64
-
#[serde(rename_all = "camelCase")]
65
-
pub struct View<'a> {
66
-
#[serde(skip_serializing_if = "std::option::Option::is_none")]
67
-
#[serde(borrow)]
68
-
pub alt: std::option::Option<jacquard_common::CowStr<'a>>,
69
-
#[serde(skip_serializing_if = "std::option::Option::is_none")]
70
-
#[serde(borrow)]
71
-
pub aspect_ratio: std::option::Option<jacquard_common::types::value::Data<'a>>,
72
-
#[serde(borrow)]
73
-
pub cid: jacquard_common::types::string::Cid<'a>,
74
-
#[serde(borrow)]
75
-
pub playlist: jacquard_common::types::string::Uri<'a>,
76
-
#[serde(skip_serializing_if = "std::option::Option::is_none")]
77
-
#[serde(borrow)]
78
-
pub thumbnail: std::option::Option<jacquard_common::types::string::Uri<'a>>,
79
-
}
80
-
81
-
impl jacquard_common::IntoStatic for View<'_> {
82
-
type Output = View<'static>;
83
-
fn into_static(self) -> Self::Output {
84
-
View {
85
-
alt: self.alt.into_static(),
86
-
aspect_ratio: self.aspect_ratio.into_static(),
87
-
cid: self.cid.into_static(),
88
-
playlist: self.playlist.into_static(),
89
-
thumbnail: self.thumbnail.into_static(),
90
-
extra_data: self.extra_data.into_static(),
91
-
}
92
-
}
93
-
}
-10
crates/jacquard-lexicon/target/test_codegen_output/app_bsky/embed.rs
-10
crates/jacquard-lexicon/target/test_codegen_output/app_bsky/embed.rs
···
1
-
// @generated by jacquard-lexicon. DO NOT EDIT.
2
-
//
3
-
// This file was automatically generated from Lexicon schemas.
4
-
// Any manual changes will be overwritten on the next regeneration.
5
-
6
-
pub mod external;
7
-
pub mod images;
8
-
pub mod record;
9
-
pub mod record_with_media;
10
-
pub mod video;
-192
crates/jacquard-lexicon/target/test_codegen_output/app_bsky/feed/post.rs
-192
crates/jacquard-lexicon/target/test_codegen_output/app_bsky/feed/post.rs
···
1
-
// @generated by jacquard-lexicon. DO NOT EDIT.
2
-
//
3
-
// Lexicon: app.bsky.feed.post
4
-
//
5
-
// This file was automatically generated from Lexicon schemas.
6
-
// Any manual changes will be overwritten on the next regeneration.
7
-
8
-
///Deprecated: use facets instead.
9
-
#[jacquard_derive::lexicon]
10
-
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq)]
11
-
#[serde(rename_all = "camelCase")]
12
-
pub struct Entity<'a> {
13
-
#[serde(borrow)]
14
-
pub index: test_generated::app_bsky::feed::post::TextSlice<'a>,
15
-
///Expected values are 'mention' and 'link'.
16
-
#[serde(borrow)]
17
-
pub r#type: jacquard_common::CowStr<'a>,
18
-
#[serde(borrow)]
19
-
pub value: jacquard_common::CowStr<'a>,
20
-
}
21
-
22
-
impl jacquard_common::IntoStatic for Entity<'_> {
23
-
type Output = Entity<'static>;
24
-
fn into_static(self) -> Self::Output {
25
-
Entity {
26
-
index: self.index.into_static(),
27
-
r#type: self.r#type.into_static(),
28
-
value: self.value.into_static(),
29
-
extra_data: self.extra_data.into_static(),
30
-
}
31
-
}
32
-
}
33
-
34
-
///Record containing a Bluesky post.
35
-
#[jacquard_derive::lexicon]
36
-
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq)]
37
-
#[serde(rename_all = "camelCase")]
38
-
pub struct Post<'a> {
39
-
///Client-declared timestamp when this post was originally created.
40
-
pub created_at: jacquard_common::types::string::Datetime,
41
-
#[serde(skip_serializing_if = "std::option::Option::is_none")]
42
-
#[serde(borrow)]
43
-
pub embed: std::option::Option<PostRecordEmbed<'a>>,
44
-
///DEPRECATED: replaced by app.bsky.richtext.facet.
45
-
#[serde(skip_serializing_if = "std::option::Option::is_none")]
46
-
#[serde(borrow)]
47
-
pub entities: std::option::Option<
48
-
Vec<test_generated::app_bsky::feed::post::Entity<'a>>,
49
-
>,
50
-
///Annotations of text (mentions, URLs, hashtags, etc)
51
-
#[serde(skip_serializing_if = "std::option::Option::is_none")]
52
-
#[serde(borrow)]
53
-
pub facets: std::option::Option<
54
-
Vec<test_generated::app_bsky::richtext::facet::Facet<'a>>,
55
-
>,
56
-
///Self-label values for this post. Effectively content warnings.
57
-
#[serde(skip_serializing_if = "std::option::Option::is_none")]
58
-
#[serde(borrow)]
59
-
pub labels: std::option::Option<PostRecordLabels<'a>>,
60
-
///Indicates human language of post primary text content.
61
-
#[serde(skip_serializing_if = "std::option::Option::is_none")]
62
-
pub langs: std::option::Option<Vec<jacquard_common::types::string::Language>>,
63
-
#[serde(skip_serializing_if = "std::option::Option::is_none")]
64
-
#[serde(borrow)]
65
-
pub reply: std::option::Option<test_generated::app_bsky::feed::post::ReplyRef<'a>>,
66
-
///Additional hashtags, in addition to any included in post text and facets.
67
-
#[serde(skip_serializing_if = "std::option::Option::is_none")]
68
-
#[serde(borrow)]
69
-
pub tags: std::option::Option<Vec<jacquard_common::CowStr<'a>>>,
70
-
///The primary post content. May be an empty string, if there are embeds.
71
-
#[serde(borrow)]
72
-
pub text: jacquard_common::CowStr<'a>,
73
-
}
74
-
75
-
#[jacquard_derive::open_union]
76
-
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq)]
77
-
#[serde(tag = "$type")]
78
-
#[serde(bound(deserialize = "'de: 'a"))]
79
-
pub enum PostRecordEmbed<'a> {
80
-
#[serde(rename = "app.bsky.embed.images")]
81
-
Images(Box<test_generated::app_bsky::embed::images::Images<'a>>),
82
-
#[serde(rename = "app.bsky.embed.video")]
83
-
Video(Box<test_generated::app_bsky::embed::video::Video<'a>>),
84
-
#[serde(rename = "app.bsky.embed.external")]
85
-
External(Box<test_generated::app_bsky::embed::external::ExternalRecord<'a>>),
86
-
#[serde(rename = "app.bsky.embed.record")]
87
-
Record(Box<test_generated::app_bsky::embed::record::Record<'a>>),
88
-
#[serde(rename = "app.bsky.embed.recordWithMedia")]
89
-
RecordWithMedia(
90
-
Box<test_generated::app_bsky::embed::record_with_media::RecordWithMedia<'a>>,
91
-
),
92
-
}
93
-
94
-
impl jacquard_common::IntoStatic for PostRecordEmbed<'_> {
95
-
type Output = PostRecordEmbed<'static>;
96
-
fn into_static(self) -> Self::Output {
97
-
match self {
98
-
PostRecordEmbed::Images(v) => PostRecordEmbed::Images(v.into_static()),
99
-
PostRecordEmbed::Video(v) => PostRecordEmbed::Video(v.into_static()),
100
-
PostRecordEmbed::External(v) => PostRecordEmbed::External(v.into_static()),
101
-
PostRecordEmbed::Record(v) => PostRecordEmbed::Record(v.into_static()),
102
-
PostRecordEmbed::RecordWithMedia(v) => {
103
-
PostRecordEmbed::RecordWithMedia(v.into_static())
104
-
}
105
-
PostRecordEmbed::Unknown(v) => PostRecordEmbed::Unknown(v.into_static()),
106
-
}
107
-
}
108
-
}
109
-
110
-
#[jacquard_derive::open_union]
111
-
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq)]
112
-
#[serde(tag = "$type")]
113
-
#[serde(bound(deserialize = "'de: 'a"))]
114
-
pub enum PostRecordLabels<'a> {
115
-
#[serde(rename = "com.atproto.label.defs#selfLabels")]
116
-
DefsSelfLabels(Box<test_generated::com_atproto::label::SelfLabels<'a>>),
117
-
}
118
-
119
-
impl jacquard_common::IntoStatic for PostRecordLabels<'_> {
120
-
type Output = PostRecordLabels<'static>;
121
-
fn into_static(self) -> Self::Output {
122
-
match self {
123
-
PostRecordLabels::DefsSelfLabels(v) => {
124
-
PostRecordLabels::DefsSelfLabels(v.into_static())
125
-
}
126
-
PostRecordLabels::Unknown(v) => PostRecordLabels::Unknown(v.into_static()),
127
-
}
128
-
}
129
-
}
130
-
131
-
impl jacquard_common::types::collection::Collection for Post<'_> {
132
-
const NSID: &'static str = "app.bsky.feed.post";
133
-
}
134
-
135
-
impl jacquard_common::IntoStatic for Post<'_> {
136
-
type Output = Post<'static>;
137
-
fn into_static(self) -> Self::Output {
138
-
Post {
139
-
created_at: self.created_at.into_static(),
140
-
embed: self.embed.into_static(),
141
-
entities: self.entities.into_static(),
142
-
facets: self.facets.into_static(),
143
-
labels: self.labels.into_static(),
144
-
langs: self.langs.into_static(),
145
-
reply: self.reply.into_static(),
146
-
tags: self.tags.into_static(),
147
-
text: self.text.into_static(),
148
-
extra_data: self.extra_data.into_static(),
149
-
}
150
-
}
151
-
}
152
-
153
-
#[jacquard_derive::lexicon]
154
-
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq)]
155
-
#[serde(rename_all = "camelCase")]
156
-
pub struct ReplyRef<'a> {
157
-
#[serde(borrow)]
158
-
pub parent: test_generated::com_atproto::repo::strong_ref::StrongRef<'a>,
159
-
#[serde(borrow)]
160
-
pub root: test_generated::com_atproto::repo::strong_ref::StrongRef<'a>,
161
-
}
162
-
163
-
impl jacquard_common::IntoStatic for ReplyRef<'_> {
164
-
type Output = ReplyRef<'static>;
165
-
fn into_static(self) -> Self::Output {
166
-
ReplyRef {
167
-
parent: self.parent.into_static(),
168
-
root: self.root.into_static(),
169
-
extra_data: self.extra_data.into_static(),
170
-
}
171
-
}
172
-
}
173
-
174
-
///Deprecated. Use app.bsky.richtext instead -- A text segment. Start is inclusive, end is exclusive. Indices are for utf16-encoded strings.
175
-
#[jacquard_derive::lexicon]
176
-
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq)]
177
-
#[serde(rename_all = "camelCase")]
178
-
pub struct TextSlice<'a> {
179
-
pub end: i64,
180
-
pub start: i64,
181
-
}
182
-
183
-
impl jacquard_common::IntoStatic for TextSlice<'_> {
184
-
type Output = TextSlice<'static>;
185
-
fn into_static(self) -> Self::Output {
186
-
TextSlice {
187
-
end: self.end.into_static(),
188
-
start: self.start.into_static(),
189
-
extra_data: self.extra_data.into_static(),
190
-
}
191
-
}
192
-
}
-7
crates/jacquard-lexicon/target/test_codegen_output/app_bsky/feed.rs
-7
crates/jacquard-lexicon/target/test_codegen_output/app_bsky/feed.rs
-105
crates/jacquard-lexicon/target/test_codegen_output/app_bsky/richtext/facet.rs
-105
crates/jacquard-lexicon/target/test_codegen_output/app_bsky/richtext/facet.rs
···
1
-
// @generated by jacquard-lexicon. DO NOT EDIT.
2
-
//
3
-
// Lexicon: app.bsky.richtext.facet
4
-
//
5
-
// This file was automatically generated from Lexicon schemas.
6
-
// Any manual changes will be overwritten on the next regeneration.
7
-
8
-
///Specifies the sub-string range a facet feature applies to. Start index is inclusive, end index is exclusive. Indices are zero-indexed, counting bytes of the UTF-8 encoded text. NOTE: some languages, like Javascript, use UTF-16 or Unicode codepoints for string slice indexing; in these languages, convert to byte arrays before working with facets.
9
-
#[jacquard_derive::lexicon]
10
-
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq)]
11
-
#[serde(rename_all = "camelCase")]
12
-
pub struct ByteSlice<'a> {
13
-
pub byte_end: i64,
14
-
pub byte_start: i64,
15
-
}
16
-
17
-
impl jacquard_common::IntoStatic for ByteSlice<'_> {
18
-
type Output = ByteSlice<'static>;
19
-
fn into_static(self) -> Self::Output {
20
-
ByteSlice {
21
-
byte_end: self.byte_end.into_static(),
22
-
byte_start: self.byte_start.into_static(),
23
-
extra_data: self.extra_data.into_static(),
24
-
}
25
-
}
26
-
}
27
-
28
-
///Facet feature for a URL. The text URL may have been simplified or truncated, but the facet reference should be a complete URL.
29
-
#[jacquard_derive::lexicon]
30
-
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq)]
31
-
#[serde(rename_all = "camelCase")]
32
-
pub struct Link<'a> {
33
-
#[serde(borrow)]
34
-
pub uri: jacquard_common::types::string::Uri<'a>,
35
-
}
36
-
37
-
impl jacquard_common::IntoStatic for Link<'_> {
38
-
type Output = Link<'static>;
39
-
fn into_static(self) -> Self::Output {
40
-
Link {
41
-
uri: self.uri.into_static(),
42
-
extra_data: self.extra_data.into_static(),
43
-
}
44
-
}
45
-
}
46
-
47
-
///Annotation of a sub-string within rich text.
48
-
#[jacquard_derive::lexicon]
49
-
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq)]
50
-
#[serde(rename_all = "camelCase")]
51
-
pub struct Facet<'a> {
52
-
#[serde(borrow)]
53
-
pub features: Vec<jacquard_common::types::value::Data<'a>>,
54
-
#[serde(borrow)]
55
-
pub index: test_generated::app_bsky::richtext::facet::ByteSlice<'a>,
56
-
}
57
-
58
-
impl jacquard_common::IntoStatic for Facet<'_> {
59
-
type Output = Facet<'static>;
60
-
fn into_static(self) -> Self::Output {
61
-
Facet {
62
-
features: self.features.into_static(),
63
-
index: self.index.into_static(),
64
-
extra_data: self.extra_data.into_static(),
65
-
}
66
-
}
67
-
}
68
-
69
-
///Facet feature for mention of another account. The text is usually a handle, including a '@' prefix, but the facet reference is a DID.
70
-
#[jacquard_derive::lexicon]
71
-
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq)]
72
-
#[serde(rename_all = "camelCase")]
73
-
pub struct Mention<'a> {
74
-
#[serde(borrow)]
75
-
pub did: jacquard_common::types::string::Did<'a>,
76
-
}
77
-
78
-
impl jacquard_common::IntoStatic for Mention<'_> {
79
-
type Output = Mention<'static>;
80
-
fn into_static(self) -> Self::Output {
81
-
Mention {
82
-
did: self.did.into_static(),
83
-
extra_data: self.extra_data.into_static(),
84
-
}
85
-
}
86
-
}
87
-
88
-
///Facet feature for a hashtag. The text usually includes a '#' prefix, but the facet reference should not (except in the case of 'double hash tags').
89
-
#[jacquard_derive::lexicon]
90
-
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq)]
91
-
#[serde(rename_all = "camelCase")]
92
-
pub struct Tag<'a> {
93
-
#[serde(borrow)]
94
-
pub tag: jacquard_common::CowStr<'a>,
95
-
}
96
-
97
-
impl jacquard_common::IntoStatic for Tag<'_> {
98
-
type Output = Tag<'static>;
99
-
fn into_static(self) -> Self::Output {
100
-
Tag {
101
-
tag: self.tag.into_static(),
102
-
extra_data: self.extra_data.into_static(),
103
-
}
104
-
}
105
-
}
-6
crates/jacquard-lexicon/target/test_codegen_output/app_bsky/richtext.rs
-6
crates/jacquard-lexicon/target/test_codegen_output/app_bsky/richtext.rs
-8
crates/jacquard-lexicon/target/test_codegen_output/app_bsky.rs
-8
crates/jacquard-lexicon/target/test_codegen_output/app_bsky.rs
-287
crates/jacquard-lexicon/target/test_codegen_output/com_atproto/label.rs
-287
crates/jacquard-lexicon/target/test_codegen_output/com_atproto/label.rs
···
1
-
// @generated by jacquard-lexicon. DO NOT EDIT.
2
-
//
3
-
// Lexicon: com.atproto.label.defs
4
-
//
5
-
// This file was automatically generated from Lexicon schemas.
6
-
// Any manual changes will be overwritten on the next regeneration.
7
-
8
-
///Metadata tag on an atproto resource (eg, repo or record).
9
-
#[jacquard_derive::lexicon]
10
-
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq)]
11
-
#[serde(rename_all = "camelCase")]
12
-
pub struct Label<'a> {
13
-
///Optionally, CID specifying the specific version of 'uri' resource this label applies to.
14
-
#[serde(skip_serializing_if = "std::option::Option::is_none")]
15
-
#[serde(borrow)]
16
-
pub cid: std::option::Option<jacquard_common::types::string::Cid<'a>>,
17
-
///Timestamp when this label was created.
18
-
pub cts: jacquard_common::types::string::Datetime,
19
-
///Timestamp at which this label expires (no longer applies).
20
-
#[serde(skip_serializing_if = "std::option::Option::is_none")]
21
-
pub exp: std::option::Option<jacquard_common::types::string::Datetime>,
22
-
///If true, this is a negation label, overwriting a previous label.
23
-
#[serde(skip_serializing_if = "std::option::Option::is_none")]
24
-
pub neg: std::option::Option<bool>,
25
-
///Signature of dag-cbor encoded label.
26
-
#[serde(skip_serializing_if = "std::option::Option::is_none")]
27
-
pub sig: std::option::Option<bytes::Bytes>,
28
-
///DID of the actor who created this label.
29
-
#[serde(borrow)]
30
-
pub src: jacquard_common::types::string::Did<'a>,
31
-
///AT URI of the record, repository (account), or other resource that this label applies to.
32
-
#[serde(borrow)]
33
-
pub uri: jacquard_common::types::string::Uri<'a>,
34
-
///The short string name of the value or type of this label.
35
-
#[serde(borrow)]
36
-
pub val: jacquard_common::CowStr<'a>,
37
-
///The AT Protocol version of the label object.
38
-
#[serde(skip_serializing_if = "std::option::Option::is_none")]
39
-
pub ver: std::option::Option<i64>,
40
-
}
41
-
42
-
impl jacquard_common::IntoStatic for Label<'_> {
43
-
type Output = Label<'static>;
44
-
fn into_static(self) -> Self::Output {
45
-
Label {
46
-
cid: self.cid.into_static(),
47
-
cts: self.cts.into_static(),
48
-
exp: self.exp.into_static(),
49
-
neg: self.neg.into_static(),
50
-
sig: self.sig.into_static(),
51
-
src: self.src.into_static(),
52
-
uri: self.uri.into_static(),
53
-
val: self.val.into_static(),
54
-
ver: self.ver.into_static(),
55
-
extra_data: self.extra_data.into_static(),
56
-
}
57
-
}
58
-
}
59
-
60
-
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
61
-
pub enum LabelValue<'a> {
62
-
Hide,
63
-
NoPromote,
64
-
Warn,
65
-
NoUnauthenticated,
66
-
DmcaViolation,
67
-
Doxxing,
68
-
Porn,
69
-
Sexual,
70
-
Nudity,
71
-
Nsfl,
72
-
Gore,
73
-
Other(jacquard_common::CowStr<'a>),
74
-
}
75
-
76
-
impl<'a> LabelValue<'a> {
77
-
pub fn as_str(&self) -> &str {
78
-
match self {
79
-
Self::Hide => "!hide",
80
-
Self::NoPromote => "!no-promote",
81
-
Self::Warn => "!warn",
82
-
Self::NoUnauthenticated => "!no-unauthenticated",
83
-
Self::DmcaViolation => "dmca-violation",
84
-
Self::Doxxing => "doxxing",
85
-
Self::Porn => "porn",
86
-
Self::Sexual => "sexual",
87
-
Self::Nudity => "nudity",
88
-
Self::Nsfl => "nsfl",
89
-
Self::Gore => "gore",
90
-
Self::Other(s) => s.as_ref(),
91
-
}
92
-
}
93
-
}
94
-
95
-
impl<'a> From<&'a str> for LabelValue<'a> {
96
-
fn from(s: &'a str) -> Self {
97
-
match s {
98
-
"!hide" => Self::Hide,
99
-
"!no-promote" => Self::NoPromote,
100
-
"!warn" => Self::Warn,
101
-
"!no-unauthenticated" => Self::NoUnauthenticated,
102
-
"dmca-violation" => Self::DmcaViolation,
103
-
"doxxing" => Self::Doxxing,
104
-
"porn" => Self::Porn,
105
-
"sexual" => Self::Sexual,
106
-
"nudity" => Self::Nudity,
107
-
"nsfl" => Self::Nsfl,
108
-
"gore" => Self::Gore,
109
-
_ => Self::Other(jacquard_common::CowStr::from(s)),
110
-
}
111
-
}
112
-
}
113
-
114
-
impl<'a> From<String> for LabelValue<'a> {
115
-
fn from(s: String) -> Self {
116
-
match s.as_str() {
117
-
"!hide" => Self::Hide,
118
-
"!no-promote" => Self::NoPromote,
119
-
"!warn" => Self::Warn,
120
-
"!no-unauthenticated" => Self::NoUnauthenticated,
121
-
"dmca-violation" => Self::DmcaViolation,
122
-
"doxxing" => Self::Doxxing,
123
-
"porn" => Self::Porn,
124
-
"sexual" => Self::Sexual,
125
-
"nudity" => Self::Nudity,
126
-
"nsfl" => Self::Nsfl,
127
-
"gore" => Self::Gore,
128
-
_ => Self::Other(jacquard_common::CowStr::from(s)),
129
-
}
130
-
}
131
-
}
132
-
133
-
impl<'a> AsRef<str> for LabelValue<'a> {
134
-
fn as_ref(&self) -> &str {
135
-
self.as_str()
136
-
}
137
-
}
138
-
139
-
impl<'a> serde::Serialize for LabelValue<'a> {
140
-
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
141
-
where
142
-
S: serde::Serializer,
143
-
{
144
-
serializer.serialize_str(self.as_str())
145
-
}
146
-
}
147
-
148
-
impl<'de, 'a> serde::Deserialize<'de> for LabelValue<'a>
149
-
where
150
-
'de: 'a,
151
-
{
152
-
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
153
-
where
154
-
D: serde::Deserializer<'de>,
155
-
{
156
-
let s = <&'de str>::deserialize(deserializer)?;
157
-
Ok(Self::from(s))
158
-
}
159
-
}
160
-
161
-
impl jacquard_common::IntoStatic for LabelValue<'_> {
162
-
type Output = LabelValue<'static>;
163
-
fn into_static(self) -> Self::Output {
164
-
match self {
165
-
LabelValue::Hide => LabelValue::Hide,
166
-
LabelValue::NoPromote => LabelValue::NoPromote,
167
-
LabelValue::Warn => LabelValue::Warn,
168
-
LabelValue::NoUnauthenticated => LabelValue::NoUnauthenticated,
169
-
LabelValue::DmcaViolation => LabelValue::DmcaViolation,
170
-
LabelValue::Doxxing => LabelValue::Doxxing,
171
-
LabelValue::Porn => LabelValue::Porn,
172
-
LabelValue::Sexual => LabelValue::Sexual,
173
-
LabelValue::Nudity => LabelValue::Nudity,
174
-
LabelValue::Nsfl => LabelValue::Nsfl,
175
-
LabelValue::Gore => LabelValue::Gore,
176
-
LabelValue::Other(v) => LabelValue::Other(v.into_static()),
177
-
}
178
-
}
179
-
}
180
-
181
-
///Declares a label value and its expected interpretations and behaviors.
182
-
#[jacquard_derive::lexicon]
183
-
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq)]
184
-
#[serde(rename_all = "camelCase")]
185
-
pub struct LabelValueDefinition<'a> {
186
-
///Does the user need to have adult content enabled in order to configure this label?
187
-
#[serde(skip_serializing_if = "std::option::Option::is_none")]
188
-
pub adult_only: std::option::Option<bool>,
189
-
///What should this label hide in the UI, if applied? 'content' hides all of the target; 'media' hides the images/video/audio; 'none' hides nothing.
190
-
#[serde(borrow)]
191
-
pub blurs: jacquard_common::CowStr<'a>,
192
-
///The default setting for this label.
193
-
#[serde(skip_serializing_if = "std::option::Option::is_none")]
194
-
#[serde(borrow)]
195
-
pub default_setting: std::option::Option<jacquard_common::CowStr<'a>>,
196
-
///The value of the label being defined. Must only include lowercase ascii and the '-' character ([a-z-]+).
197
-
#[serde(borrow)]
198
-
pub identifier: jacquard_common::CowStr<'a>,
199
-
#[serde(borrow)]
200
-
pub locales: Vec<
201
-
test_generated::com_atproto::label::LabelValueDefinitionStrings<'a>,
202
-
>,
203
-
///How should a client visually convey this label? 'inform' means neutral and informational; 'alert' means negative and warning; 'none' means show nothing.
204
-
#[serde(borrow)]
205
-
pub severity: jacquard_common::CowStr<'a>,
206
-
}
207
-
208
-
impl jacquard_common::IntoStatic for LabelValueDefinition<'_> {
209
-
type Output = LabelValueDefinition<'static>;
210
-
fn into_static(self) -> Self::Output {
211
-
LabelValueDefinition {
212
-
adult_only: self.adult_only.into_static(),
213
-
blurs: self.blurs.into_static(),
214
-
default_setting: self.default_setting.into_static(),
215
-
identifier: self.identifier.into_static(),
216
-
locales: self.locales.into_static(),
217
-
severity: self.severity.into_static(),
218
-
extra_data: self.extra_data.into_static(),
219
-
}
220
-
}
221
-
}
222
-
223
-
///Strings which describe the label in the UI, localized into a specific language.
224
-
#[jacquard_derive::lexicon]
225
-
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq)]
226
-
#[serde(rename_all = "camelCase")]
227
-
pub struct LabelValueDefinitionStrings<'a> {
228
-
///A longer description of what the label means and why it might be applied.
229
-
#[serde(borrow)]
230
-
pub description: jacquard_common::CowStr<'a>,
231
-
///The code of the language these strings are written in.
232
-
pub lang: jacquard_common::types::string::Language,
233
-
///A short human-readable name for the label.
234
-
#[serde(borrow)]
235
-
pub name: jacquard_common::CowStr<'a>,
236
-
}
237
-
238
-
impl jacquard_common::IntoStatic for LabelValueDefinitionStrings<'_> {
239
-
type Output = LabelValueDefinitionStrings<'static>;
240
-
fn into_static(self) -> Self::Output {
241
-
LabelValueDefinitionStrings {
242
-
description: self.description.into_static(),
243
-
lang: self.lang.into_static(),
244
-
name: self.name.into_static(),
245
-
extra_data: self.extra_data.into_static(),
246
-
}
247
-
}
248
-
}
249
-
250
-
///Metadata tag on an atproto record, published by the author within the record. Note that schemas should use #selfLabels, not #selfLabel.
251
-
#[jacquard_derive::lexicon]
252
-
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq)]
253
-
#[serde(rename_all = "camelCase")]
254
-
pub struct SelfLabel<'a> {
255
-
///The short string name of the value or type of this label.
256
-
#[serde(borrow)]
257
-
pub val: jacquard_common::CowStr<'a>,
258
-
}
259
-
260
-
impl jacquard_common::IntoStatic for SelfLabel<'_> {
261
-
type Output = SelfLabel<'static>;
262
-
fn into_static(self) -> Self::Output {
263
-
SelfLabel {
264
-
val: self.val.into_static(),
265
-
extra_data: self.extra_data.into_static(),
266
-
}
267
-
}
268
-
}
269
-
270
-
///Metadata tags on an atproto record, published by the author within the record.
271
-
#[jacquard_derive::lexicon]
272
-
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq)]
273
-
#[serde(rename_all = "camelCase")]
274
-
pub struct SelfLabels<'a> {
275
-
#[serde(borrow)]
276
-
pub values: Vec<test_generated::com_atproto::label::SelfLabel<'a>>,
277
-
}
278
-
279
-
impl jacquard_common::IntoStatic for SelfLabels<'_> {
280
-
type Output = SelfLabels<'static>;
281
-
fn into_static(self) -> Self::Output {
282
-
SelfLabels {
283
-
values: self.values.into_static(),
284
-
extra_data: self.extra_data.into_static(),
285
-
}
286
-
}
287
-
}
-27
crates/jacquard-lexicon/target/test_codegen_output/com_atproto/repo/strong_ref.rs
-27
crates/jacquard-lexicon/target/test_codegen_output/com_atproto/repo/strong_ref.rs
···
1
-
// @generated by jacquard-lexicon. DO NOT EDIT.
2
-
//
3
-
// Lexicon: com.atproto.repo.strongRef
4
-
//
5
-
// This file was automatically generated from Lexicon schemas.
6
-
// Any manual changes will be overwritten on the next regeneration.
7
-
8
-
#[jacquard_derive::lexicon]
9
-
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq)]
10
-
#[serde(rename_all = "camelCase")]
11
-
pub struct StrongRef<'a> {
12
-
#[serde(borrow)]
13
-
pub cid: jacquard_common::types::string::Cid<'a>,
14
-
#[serde(borrow)]
15
-
pub uri: jacquard_common::types::string::AtUri<'a>,
16
-
}
17
-
18
-
impl jacquard_common::IntoStatic for StrongRef<'_> {
19
-
type Output = StrongRef<'static>;
20
-
fn into_static(self) -> Self::Output {
21
-
StrongRef {
22
-
cid: self.cid.into_static(),
23
-
uri: self.uri.into_static(),
24
-
extra_data: self.extra_data.into_static(),
25
-
}
26
-
}
27
-
}
-6
crates/jacquard-lexicon/target/test_codegen_output/com_atproto/repo.rs
-6
crates/jacquard-lexicon/target/test_codegen_output/com_atproto/repo.rs
-7
crates/jacquard-lexicon/target/test_codegen_output/com_atproto.rs
-7
crates/jacquard-lexicon/target/test_codegen_output/com_atproto.rs
-7
crates/jacquard-lexicon/target/test_codegen_output/lib.rs
-7
crates/jacquard-lexicon/target/test_codegen_output/lib.rs
-15
crates/jacquard-lexicon/tests/regen_api.rs
-15
crates/jacquard-lexicon/tests/regen_api.rs
···
1
-
use jacquard_lexicon::codegen::CodeGenerator;
2
-
use jacquard_lexicon::corpus::LexiconCorpus;
3
-
4
-
#[test]
5
-
#[ignore] // Run with: cargo test --test regen_api -- --ignored
6
-
fn regenerate_api() {
7
-
let corpus = LexiconCorpus::load_from_dir("tests/fixtures/lexicons/atproto/lexicons").expect("load corpus");
8
-
let codegen = CodeGenerator::new(&corpus, "crate");
9
-
10
-
codegen
11
-
.write_to_disk(std::path::Path::new("../jacquard-api/src"))
12
-
.expect("write to disk");
13
-
14
-
println!("Generated {} lexicons", corpus.len());
15
-
}
+1
nix/modules/devshell.nix
+1
nix/modules/devshell.nix
-120
regen.rs
-120
regen.rs
···
1
-
use jacquard_lexicon::codegen::CodeGenerator;
2
-
use jacquard_lexicon::corpus::LexiconCorpus;
3
-
use prettyplease;
4
-
use std::collections::BTreeMap;
5
-
use std::fs;
6
-
use std::path::Path;
7
-
8
-
fn main() -> Result<(), Box<dyn std::error::Error>> {
9
-
let lexicons_path = "lexicons/atproto";
10
-
let output_path = "crates/jacquard-api/src";
11
-
let root_module = "crate";
12
-
13
-
println!("Loading lexicons from {}...", lexicons_path);
14
-
let corpus = LexiconCorpus::load_from_dir(lexicons_path)?;
15
-
println!("Loaded {} lexicons", corpus.len());
16
-
17
-
println!("Generating code...");
18
-
let generator = CodeGenerator::new(&corpus, root_module);
19
-
20
-
// Group by module
21
-
let mut modules: BTreeMap<String, Vec<(String, String)>> = BTreeMap::new();
22
-
23
-
for (nsid, doc) in corpus.iter() {
24
-
let nsid_str = nsid.as_str();
25
-
26
-
// Get module path: app.bsky.feed.post -> app_bsky/feed
27
-
let parts: Vec<&str> = nsid_str.split('.').collect();
28
-
let module_path = if parts.len() >= 3 {
29
-
let first_two = format!("{}_{}", parts[0], parts[1]);
30
-
if parts.len() > 3 {
31
-
let middle: Vec<&str> = parts[2..parts.len() - 1].iter().copied().collect();
32
-
format!("{}/{}", first_two, middle.join("/"))
33
-
} else {
34
-
first_two
35
-
}
36
-
} else {
37
-
parts.join("_")
38
-
};
39
-
40
-
let file_name = parts.last().unwrap().to_string();
41
-
42
-
for (def_name, def) in &doc.defs {
43
-
match generator.generate_def(nsid_str, def_name, def) {
44
-
Ok(tokens) => {
45
-
let code = prettyplease::unparse(&syn::parse_file(&tokens.to_string())?);
46
-
modules
47
-
.entry(format!("{}/{}.rs", module_path, file_name))
48
-
.or_default()
49
-
.push((def_name.to_string(), code));
50
-
}
51
-
Err(e) => {
52
-
eprintln!("Error generating {}.{}: {:?}", nsid_str, def_name, e);
53
-
}
54
-
}
55
-
}
56
-
}
57
-
58
-
// Write files
59
-
for (file_path, defs) in modules {
60
-
let full_path = Path::new(output_path).join(&file_path);
61
-
62
-
// Create parent directory
63
-
if let Some(parent) = full_path.parent() {
64
-
fs::create_dir_all(parent)?;
65
-
}
66
-
67
-
let content = defs.iter().map(|(_, code)| code.as_str()).collect::<Vec<_>>().join("\n");
68
-
fs::write(&full_path, content)?;
69
-
println!("Wrote {}", file_path);
70
-
}
71
-
72
-
// Generate mod.rs files
73
-
println!("Generating mod.rs files...");
74
-
generate_mod_files(Path::new(output_path))?;
75
-
76
-
println!("Done!");
77
-
Ok(())
78
-
}
79
-
80
-
fn generate_mod_files(root: &Path) -> Result<(), Box<dyn std::error::Error>> {
81
-
// Find all directories
82
-
for entry in fs::read_dir(root)? {
83
-
let entry = entry?;
84
-
let path = entry.path();
85
-
86
-
if path.is_dir() {
87
-
let dir_name = path.file_name().unwrap().to_str().unwrap();
88
-
89
-
// Recursively generate for subdirectories
90
-
generate_mod_files(&path)?;
91
-
92
-
// Generate mod.rs for this directory
93
-
let mut mods = Vec::new();
94
-
for sub_entry in fs::read_dir(&path)? {
95
-
let sub_entry = sub_entry?;
96
-
let sub_path = sub_entry.path();
97
-
98
-
if sub_path.is_file() {
99
-
if let Some(name) = sub_path.file_stem() {
100
-
let name_str = name.to_str().unwrap();
101
-
if name_str != "mod" {
102
-
mods.push(format!("pub mod {};", name_str));
103
-
}
104
-
}
105
-
} else if sub_path.is_dir() {
106
-
if let Some(name) = sub_path.file_name() {
107
-
mods.push(format!("pub mod {};", name.to_str().unwrap()));
108
-
}
109
-
}
110
-
}
111
-
112
-
if !mods.is_empty() {
113
-
let mod_content = mods.join("\n") + "\n";
114
-
fs::write(path.join("mod.rs"), mod_content)?;
115
-
}
116
-
}
117
-
}
118
-
119
-
Ok(())
120
-
}