Compare changes

Choose any two refs to compare.

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