A better Rust ATProto crate

dependency tree cleanup, fixing examples, nicer justfile

Orual c27a050b d478b2fb

-59
Cargo.lock
··· 157 157 ] 158 158 159 159 [[package]] 160 - name = "async-stream" 161 - version = "0.3.6" 162 - source = "registry+https://github.com/rust-lang/crates.io-index" 163 - checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476" 164 - dependencies = [ 165 - "async-stream-impl", 166 - "futures-core", 167 - "pin-project-lite", 168 - ] 169 - 170 - [[package]] 171 - name = "async-stream-impl" 172 - version = "0.3.6" 173 - source = "registry+https://github.com/rust-lang/crates.io-index" 174 - checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" 175 - dependencies = [ 176 - "proc-macro2", 177 - "quote", 178 - "syn 2.0.106", 179 - ] 180 - 181 - [[package]] 182 160 name = "async-trait" 183 161 version = "0.1.89" 184 162 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1780 1758 "thiserror 2.0.17", 1781 1759 "tokio", 1782 1760 "url", 1783 - "urlencoding", 1784 1761 ] 1785 1762 1786 1763 [[package]] ··· 1835 1812 "serde_json", 1836 1813 "thiserror 2.0.17", 1837 1814 "tokio", 1838 - "tokio-test", 1839 1815 "tower", 1840 1816 "tower-http", 1841 1817 "tracing", 1842 1818 "tracing-subscriber", 1843 - "urlencoding", 1844 1819 ] 1845 1820 1846 1821 [[package]] ··· 1870 1845 "serde_html_form", 1871 1846 "serde_ipld_dagcbor", 1872 1847 "serde_json", 1873 - "serde_with", 1874 1848 "signature", 1875 1849 "smol_str", 1876 1850 "thiserror 2.0.17", 1877 1851 "tokio", 1878 - "trait-variant", 1879 1852 "url", 1880 1853 ] 1881 1854 ··· 1916 1889 name = "jacquard-derive" 1917 1890 version = "0.5.1" 1918 1891 dependencies = [ 1919 - "heck 0.5.0", 1920 - "itertools", 1921 1892 "jacquard-common 0.5.1", 1922 - "prettyplease", 1923 1893 "proc-macro2", 1924 1894 "quote", 1925 1895 "serde", 1926 1896 "serde_json", 1927 - "serde_repr", 1928 - "serde_with", 1929 1897 "syn 2.0.106", 1930 1898 ] 1931 1899 ··· 2000 1968 "clap", 2001 1969 "glob", 2002 1970 "heck 0.5.0", 2003 - "itertools", 2004 1971 "jacquard-api 0.5.1 (git+https://tangled.org/@nonbinary.computer/jacquard)", 2005 1972 "jacquard-common 0.5.1 (git+https://tangled.org/@nonbinary.computer/jacquard)", 2006 1973 "jacquard-identity 0.5.1 (git+https://tangled.org/@nonbinary.computer/jacquard)", ··· 2052 2019 "tokio", 2053 2020 "trait-variant", 2054 2021 "url", 2055 - "uuid", 2056 2022 "webbrowser", 2057 2023 ] 2058 2024 ··· 3850 3816 ] 3851 3817 3852 3818 [[package]] 3853 - name = "tokio-stream" 3854 - version = "0.1.17" 3855 - source = "registry+https://github.com/rust-lang/crates.io-index" 3856 - checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047" 3857 - dependencies = [ 3858 - "futures-core", 3859 - "pin-project-lite", 3860 - "tokio", 3861 - ] 3862 - 3863 - [[package]] 3864 - name = "tokio-test" 3865 - version = "0.4.4" 3866 - source = "registry+https://github.com/rust-lang/crates.io-index" 3867 - checksum = "2468baabc3311435b55dd935f702f42cd1b8abb7e754fb7dfb16bd36aa88f9f7" 3868 - dependencies = [ 3869 - "async-stream", 3870 - "bytes", 3871 - "futures-core", 3872 - "tokio", 3873 - "tokio-stream", 3874 - ] 3875 - 3876 - [[package]] 3877 3819 name = "tokio-util" 3878 3820 version = "0.7.16" 3879 3821 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 4136 4078 source = "registry+https://github.com/rust-lang/crates.io-index" 4137 4079 checksum = "2f87b8aa10b915a06587d0dec516c282ff295b475d94abf425d62b57710070a2" 4138 4080 dependencies = [ 4139 - "getrandom 0.3.3", 4140 4081 "js-sys", 4141 4082 "wasm-bindgen", 4142 4083 ]
+1 -1
README.md
··· 81 81 82 82 ``` 83 83 84 - If you have `just` installed, you can run the [examples](https://tangled.org/@nonbinary.computer/jacquard/tree/main/examples) using `just example-{example-name} {ARGS}` 84 + If you have `just` installed, you can run the [examples](https://tangled.org/@nonbinary.computer/jacquard/tree/main/examples) using `just example {example-name} {ARGS}` or `just examples` to see what's available. 85 85 86 86 ## Component crates 87 87
+4 -5
crates/jacquard-axum/Cargo.toml
··· 18 18 [[example]] 19 19 name = "axum_server" 20 20 path = "../../examples/axum_server.rs" 21 - required-features = ["jacquard/fancy"] 22 21 23 22 [dependencies] 24 23 axum = "0.8.6" 25 - axum-macros = "0.5.0" 26 24 bytes.workspace = true 27 25 jacquard = { version = "0.5", path = "../jacquard" } 28 26 jacquard-common = { version = "0.5", path = "../jacquard-common", features = ["reqwest-client"] } ··· 38 36 tokio.workspace = true 39 37 tower-http = { version = "0.6.6", features = ["trace", "tracing"] } 40 38 tracing = "0.1.41" 41 - tracing-subscriber = { version = "0.3.20", features = ["env-filter", "time"] } 42 - urlencoding.workspace = true 43 39 44 40 [features] 45 41 default = ["service-auth"] 46 42 service-auth = ["jacquard-common/service-auth", "dep:jacquard-identity", "dep:multibase"] 47 43 48 44 [dev-dependencies] 45 + axum-macros = "0.5.0" 49 46 axum-test = "18.1.0" 50 47 base64.workspace = true 51 48 chrono.workspace = true 52 49 k256 = { version = "0.13", features = ["ecdsa"] } 50 + miette = { workspace = true, features = ["fancy"] } 53 51 rand = "0.8" 54 52 reqwest.workspace = true 55 53 serde_json.workspace = true 56 - tokio-test = "0.4.4" 54 + #tokio-test = "0.4.4" 57 55 tower = { version = "0.5", features = ["util"] } 56 + tracing-subscriber = { version = "0.3.20", features = ["env-filter", "time"] }
-2
crates/jacquard-common/Cargo.toml
··· 29 29 serde.workspace = true 30 30 serde_html_form.workspace = true 31 31 serde_json.workspace = true 32 - serde_with.workspace = true 33 32 smol_str.workspace = true 34 33 thiserror.workspace = true 35 34 url.workspace = true ··· 38 37 tokio = { workspace = true, features = ["sync"] } 39 38 reqwest = { workspace = true, optional = true, features = ["charset", "http2", "json", "system-proxy", "gzip", "rustls-tls"] } 40 39 serde_ipld_dagcbor.workspace = true 41 - trait-variant.workspace = true 42 40 signature = { version = "2", optional = true } 43 41 44 42 [features]
+2 -8
crates/jacquard-derive/Cargo.toml
··· 15 15 proc-macro = true 16 16 17 17 [dependencies] 18 - heck.workspace = true 19 - itertools.workspace = true 20 - prettyplease.workspace = true 21 18 proc-macro2.workspace = true 22 19 quote.workspace = true 23 - serde.workspace = true 24 - serde_json.workspace = true 25 - serde_repr.workspace = true 26 - serde_with.workspace = true 27 20 syn.workspace = true 28 - 29 21 30 22 [dev-dependencies] 31 23 jacquard-common = { version = "0.5", path = "../jacquard-common" } 24 + serde.workspace = true 25 + serde_json.workspace = true
+1 -1
crates/jacquard-lexicon/Cargo.toml
··· 24 24 clap.workspace = true 25 25 glob = "0.3" 26 26 heck.workspace = true 27 - itertools.workspace = true 27 + #itertools.workspace = true 28 28 jacquard-api = { version = "0.5", git = "https://tangled.org/@nonbinary.computer/jacquard" } 29 29 jacquard-common = { version = "0.5", git = "https://tangled.org/@nonbinary.computer/jacquard" } 30 30 jacquard-identity = { version = "0.5", git = "https://tangled.org/@nonbinary.computer/jacquard" }
-1
crates/jacquard-oauth/Cargo.toml
··· 23 23 thiserror = { workspace = true } 24 24 serde_html_form = { workspace = true } 25 25 miette = { workspace = true } 26 - uuid = { version = "1", features = ["v4","std"] } 27 26 p256 = { workspace = true, features = ["ecdsa"] } 28 27 signature = "2" 29 28 rand_core.workspace = true
+5 -14
crates/jacquard/Cargo.toml
··· 23 23 # All captured generated lexicon API bindings 24 24 api_all = ["api_full", "jacquard-api/ufos"] 25 25 dns = ["jacquard-identity/dns"] 26 - # Pretty debug prints for examples 27 - fancy = ["miette/fancy"] 28 26 # Propagate loopback to oauth (server + browser helper) 29 27 loopback = ["jacquard-oauth/loopback", "jacquard-oauth/browser-open"] 30 28 ··· 32 30 [[example]] 33 31 name = "oauth_timeline" 34 32 path = "../../examples/oauth_timeline.rs" 35 - required-features = ["fancy"] 36 33 37 34 [[example]] 38 35 name = "create_post" 39 36 path = "../../examples/create_post.rs" 40 - required-features = ["fancy"] 41 37 42 38 [[example]] 43 39 name = "post_with_image" 44 40 path = "../../examples/post_with_image.rs" 45 - required-features = ["fancy"] 46 41 47 42 [[example]] 48 43 name = "update_profile" 49 44 path = "../../examples/update_profile.rs" 50 - required-features = ["fancy"] 51 45 52 46 [[example]] 53 47 name = "public_atproto_feed" ··· 56 50 [[example]] 57 51 name = "create_whitewind_post" 58 52 path = "../../examples/create_whitewind_post.rs" 59 - required-features = ["fancy", ] 60 53 61 54 [[example]] 62 55 name = "read_whitewind_post" 63 56 path = "../../examples/read_whitewind_post.rs" 64 - required-features = ["fancy"] 65 57 66 58 [[example]] 67 59 name = "read_tangled_repo" 68 60 path = "../../examples/read_tangled_repo.rs" 69 - required-features = ["fancy"] 70 61 71 62 [[example]] 72 63 name = "resolve_did" 73 64 path = "../../examples/resolve_did.rs" 74 - required-features = ["fancy"] 75 65 76 66 [[example]] 77 67 name = "update_preferences" 78 68 path = "../../examples/update_preferences.rs" 79 - required-features = ["fancy"] 80 69 81 70 [dependencies] 82 71 jacquard-api = { version = "0.5", path = "../jacquard-api" } ··· 88 77 bon.workspace = true 89 78 async-trait.workspace = true 90 79 bytes.workspace = true 91 - clap.workspace = true 92 80 http.workspace = true 93 81 miette = { workspace = true } 94 82 reqwest = { workspace = true, features = ["charset", "http2", "json", "system-proxy", "gzip", "rustls-tls"] } ··· 101 89 url.workspace = true 102 90 smol_str.workspace = true 103 91 percent-encoding.workspace = true 104 - urlencoding.workspace = true 105 92 jose-jwk = { workspace = true, features = ["p256"] } 106 93 p256 = { workspace = true, features = ["ecdsa"] } 107 94 rand_core.workspace = true 108 95 96 + [dev-dependencies] 97 + clap.workspace = true 98 + miette = { workspace = true, features = ["fancy"] } 99 + 109 100 [package.metadata.docs.rs] 110 - features = [ "api_all", "derive", "dns", "fancy", "loopback" ] 101 + features = [ "api_all", "derive", "dns", "loopback" ]
+1 -6
examples/axum_server.rs
··· 1 1 use std::sync::Arc; 2 2 3 - use axum::{ 4 - Json, Router, 5 - extract::State, 6 - http::{StatusCode, header}, 7 - response::IntoResponse, 8 - }; 3 + use axum::{Json, Router, extract::State, http::StatusCode, response::IntoResponse}; 9 4 use jacquard::{ 10 5 api::com_atproto::identity::resolve_did::{ResolveDidOutput, ResolveDidRequest}, 11 6 identity::{JacquardResolver, resolver::IdentityResolver},
+2 -5
examples/create_post.rs
··· 1 1 use clap::Parser; 2 + use jacquard::CowStr; 2 3 use jacquard::api::app_bsky::feed::post::Post; 3 - use jacquard::client::{Agent, FileAuthStore}; 4 - use jacquard::oauth::atproto::AtprotoClientMetadata; 4 + use jacquard::client::{Agent, AgentSessionExt, FileAuthStore}; 5 5 use jacquard::oauth::client::OAuthClient; 6 6 use jacquard::oauth::loopback::LoopbackConfig; 7 7 use jacquard::types::string::Datetime; 8 - use jacquard::xrpc::XrpcClient; 9 - use jacquard::CowStr; 10 - use miette::IntoDiagnostic; 11 8 12 9 #[derive(Parser, Debug)] 13 10 #[command(author, version, about = "Create a simple post")]
+3 -5
examples/create_whitewind_post.rs
··· 1 1 use clap::Parser; 2 2 use jacquard::CowStr; 3 3 use jacquard::api::com_whtwnd::blog::entry::Entry; 4 - use jacquard::client::{Agent, FileAuthStore}; 5 - use jacquard::oauth::atproto::AtprotoClientMetadata; 4 + use jacquard::client::{Agent, AgentSessionExt, FileAuthStore}; 6 5 use jacquard::oauth::client::OAuthClient; 7 6 use jacquard::oauth::loopback::LoopbackConfig; 8 7 use jacquard::types::string::Datetime; 9 - use jacquard::xrpc::XrpcClient; 10 8 use miette::IntoDiagnostic; 11 9 use url::Url; 12 10 ··· 59 57 extra_data: Default::default(), 60 58 }; 61 59 62 - let mut output = agent.create_record(entry, None).await?; 60 + let output = agent.create_record(entry, None).await?; 63 61 println!("Created WhiteWind blog post: {}", output.uri); 64 - let url = Url::parse(format!( 62 + let url = Url::parse(&format!( 65 63 "https://whtwnd.nat.vg/{}/{}", 66 64 output.uri.authority(), 67 65 output.uri.rkey().map(|r| r.as_ref()).unwrap_or("")
+1 -3
examples/post_with_image.rs
··· 2 2 use jacquard::CowStr; 3 3 use jacquard::api::app_bsky::embed::images::{Image, Images}; 4 4 use jacquard::api::app_bsky::feed::post::{Post, PostEmbed}; 5 - use jacquard::client::{Agent, FileAuthStore}; 6 - use jacquard::oauth::atproto::AtprotoClientMetadata; 5 + use jacquard::client::{Agent, AgentSessionExt, FileAuthStore}; 7 6 use jacquard::oauth::client::OAuthClient; 8 7 use jacquard::oauth::loopback::LoopbackConfig; 9 8 use jacquard::types::blob::MimeType; 10 9 use jacquard::types::string::Datetime; 11 - use jacquard::xrpc::XrpcClient; 12 10 use miette::IntoDiagnostic; 13 11 use std::path::PathBuf; 14 12
+1 -1
examples/read_tangled_repo.rs
··· 1 1 use clap::Parser; 2 2 use jacquard::api::sh_tangled::repo::Repo; 3 - use jacquard::client::BasicClient; 3 + use jacquard::client::{AgentSessionExt, BasicClient}; 4 4 use jacquard::types::string::AtUri; 5 5 6 6 #[derive(Parser, Debug)]
+5 -2
examples/read_whitewind_post.rs
··· 1 1 use clap::Parser; 2 2 use jacquard::api::com_whtwnd::blog::entry::Entry; 3 - use jacquard::client::BasicClient; 3 + use jacquard::client::{AgentSessionExt, BasicClient}; 4 4 use jacquard::types::string::AtUri; 5 5 6 6 #[derive(Parser, Debug)] ··· 27 27 28 28 println!("📚 WhiteWind Blog Entry\n"); 29 29 println!("URI: {}", output.uri); 30 - println!("Title: {}", output.value.title.as_deref().unwrap_or("[Untitled]")); 30 + println!( 31 + "Title: {}", 32 + output.value.title.as_deref().unwrap_or("[Untitled]") 33 + ); 31 34 if let Some(subtitle) = &output.value.subtitle { 32 35 println!("Subtitle: {}", subtitle); 33 36 }
+1 -4
examples/update_profile.rs
··· 1 1 use clap::Parser; 2 2 use jacquard::CowStr; 3 3 use jacquard::api::app_bsky::actor::profile::Profile; 4 - use jacquard::client::{Agent, FileAuthStore}; 5 - use jacquard::oauth::atproto::AtprotoClientMetadata; 4 + use jacquard::client::{Agent, AgentSessionExt, FileAuthStore}; 6 5 use jacquard::oauth::client::OAuthClient; 7 6 use jacquard::oauth::loopback::LoopbackConfig; 8 7 use jacquard::types::string::AtUri; 9 - use jacquard::xrpc::XrpcClient; 10 - use miette::IntoDiagnostic; 11 8 12 9 #[derive(Parser, Debug)] 13 10 #[command(author, version, about = "Update profile display name and description")]
+48 -37
justfile
··· 13 13 watch *ARGS: 14 14 bacon --job run -- -- {{ ARGS }} 15 15 16 - # Run the OAuth timeline example 17 - example-oauth *ARGS: 18 - cargo run -p jacquard --example oauth_timeline --features fancy -- {{ARGS}} 16 + update-api: 17 + cargo run -p jacquard-lexicon --bin lex-fetch -- -v 19 18 20 - # Create a simple post 21 - example-create-post *ARGS: 22 - cargo run -p jacquard --example create_post --features fancy -- {{ARGS}} 19 + generate-api: 20 + cargo run -p jacquard-lexicon --bin jacquard-codegen -- -i crates/jacquard-api/lexicons -o crates/jacquard-api/src -r crate 23 21 24 - # Create a post with an image 25 - example-post-image *ARGS: 26 - cargo run -p jacquard --example post_with_image --features fancy -- {{ARGS}} 22 + lex-gen *ARGS: 23 + cargo run -p jacquard-lexicon --bin lex-fetch -- {{ARGS}} 27 24 28 - # Update profile display name and description 29 - example-update-profile *ARGS: 30 - cargo run -p jacquard --example update_profile --features fancy -- {{ARGS}} 25 + lex-fetch *ARGS: 26 + cargo run -p jacquard-lexicon --bin lex-fetch -- --no-codegen {{ARGS}} 31 27 32 - # Fetch public AT Protocol feed (no auth) 33 - example-public-feed: 34 - cargo run -p jacquard --example public_atproto_feed 28 + codegen *ARGS: 29 + cargo run -p jacquard-lexicon --bin jacquard-codegen -- -r crate {{ARGS}} 35 30 36 - # Create a WhiteWind blog post 37 - example-whitewind-create *ARGS: 38 - cargo run -p jacquard --example create_whitewind_post --features fancy -- {{ARGS}} 31 + # List all available examples 32 + examples: 33 + #!/usr/bin/env bash 34 + echo "jacquard examples:" 35 + for file in "examples"/*.rs; do 36 + name=$(basename "$file" .rs) 37 + echo " - $name" 38 + done 39 + echo "" 40 + echo "jacquard-axum examples:" 41 + cargo metadata --format-version=1 --no-deps | \ 42 + jq -r '.packages[] | select(.name == "jacquard-axum") | .targets[] | select(.kind[] == "example") | .name' | \ 43 + sed 's/^/ - /' 44 + echo "" 45 + echo "Usage: just example <name> [ARGS...]" 39 46 40 - # Read a WhiteWind blog post 41 - example-whitewind-read *ARGS: 42 - cargo run -p jacquard --example read_whitewind_posts --features fancy -- {{ARGS}} 43 - 44 - # Read info about a Tangled git repository 45 - example-tangled-repo *ARGS: 46 - cargo run -p jacquard --example read_tangled_repo --features fancy -- {{ARGS}} 47 - 48 - # Resolve a handle to its DID document 49 - example-resolve-did *ARGS: 50 - cargo run -p jacquard --example resolve_did -- {{ARGS}} 51 - 52 - # Update Bluesky preferences 53 - example-update-preferences *ARGS: 54 - cargo run -p jacquard --example update_preferences --features fancy -- {{ARGS}} 55 - 56 - # Run the Axum server example 57 - example-axum: 58 - cargo run -p jacquard-axum --example axum_server --features jacquard/fancy 47 + # Run an example by name (auto-detects package) 48 + example NAME *ARGS: 49 + #!/usr/bin/env bash 50 + if [ -f "examples/{{NAME}}.rs" ]; then 51 + cargo run -p jacquard --example {{NAME}} -- {{ARGS}} 52 + elif cargo metadata --format-version=1 --no-deps | \ 53 + jq -e '.packages[] | select(.name == "jacquard-axum") | .targets[] | select(.kind[] == "example" and .name == "{{NAME}}")' > /dev/null; then 54 + cargo run -p jacquard-axum --example {{NAME}} -- {{ARGS}} 55 + else 56 + echo "Example '{{NAME}}' not found." 57 + echo "" 58 + echo "jacquard examples:" 59 + for file in "examples"/*.rs; do 60 + name=$(basename "$file" .rs) 61 + echo " - $name" 62 + done 63 + echo "" 64 + echo "jacquard-axum examples:" 65 + cargo metadata --format-version=1 --no-deps | \ 66 + jq -r '.packages[] | select(.name == "jacquard-axum") | .targets[] | select(.kind[] == "example") | .name' | \ 67 + sed 's/^/ - /' 68 + exit 1 69 + fi