tangled
alpha
login
or
join now
alephcubed.com
/
jacquard
forked from
nonbinary.computer/jacquard
0
fork
atom
A better Rust ATProto crate
0
fork
atom
overview
issues
pulls
pipelines
reworked helper methods to be on a trait, readying 0.5 release
Orual
5 months ago
8c229615
1558344a
+534
-449
14 changed files
expand all
collapse all
unified
split
CHANGELOG.md
Cargo.lock
Cargo.toml
crates
jacquard
Cargo.toml
src
client
credential_session.rs
client.rs
jacquard-api
Cargo.toml
jacquard-axum
Cargo.toml
jacquard-common
Cargo.toml
jacquard-derive
Cargo.toml
jacquard-identity
Cargo.toml
jacquard-lexicon
Cargo.toml
jacquard-oauth
Cargo.toml
examples
update_preferences.rs
+84
CHANGELOG.md
···
1
1
# Changelog
2
2
3
3
+
## [0.5.0] - 2025-10-13
4
4
+
5
5
+
### Breaking Changes
6
6
+
7
7
+
**AgentSession trait** (`jacquard`)
8
8
+
- Removed `async fn` in favour of `impl Future` return types for better trait object compatibility
9
9
+
- Methods now return `impl Future` instead of being marked `async fn`
10
10
+
11
11
+
**XRPC improvements** (`jacquard-common`)
12
12
+
- Simplified response transmutation for typed record retrieval
13
13
+
- `Response::transmute()` added for zero-cost response type conversion
14
14
+
15
15
+
**jacquard-axum**
16
16
+
- Removed binary target (`main.rs`), now library-only
17
17
+
18
18
+
### Added
19
19
+
20
20
+
**Agent convenience methods** (`jacquard`)
21
21
+
- New `AgentSessionExt` trait automatically implemented for `AgentSession + IdentityResolver`
22
22
+
- **Basic CRUD**: `create_record()`, `get_record()`, `put_record()`, `delete_record()`
23
23
+
- **Update patterns**: `update_record()` (fetch-modify-put), `update_vec()`, `update_vec_item()`
24
24
+
- **Blob operations**: `upload_blob()`
25
25
+
- All methods auto-fill repo from session and collection from type's `Collection::NSID`
26
26
+
- Simplified bounds on `update_record` - no HRTB issues, works with all record types
27
27
+
28
28
+
**VecUpdate trait** (`jacquard`)
29
29
+
- `VecUpdate` trait for fetch-modify-put patterns on array-based endpoints
30
30
+
- `PreferencesUpdate` implementation for updating user preferences
31
31
+
- Enables type-safe updates to preferences, saved feeds, and other array endpoints
32
32
+
33
33
+
**Typed record retrieval** (`jacquard-api`, `jacquard-common`)
34
34
+
- Each collection generates `{Type}Record` marker struct implementing `XrpcResp`
35
35
+
- `Collection::Record` associated type points to the marker
36
36
+
- `get_record::<R>()` returns `Response<R::Record>` with zero-copy `.parse()`
37
37
+
- Response transmutation enables type-safe record operations
38
38
+
39
39
+
**Examples**
40
40
+
- `create_post.rs`: Creating posts with Agent convenience methods
41
41
+
- `update_profile.rs`: Updating profile with fetch-modify-put
42
42
+
- `post_with_image.rs`: Uploading images and creating posts with embeds
43
43
+
- `update_preferences.rs`: Using VecUpdate for preferences
44
44
+
- `create_whitewind_post.rs`, `read_whitewind_post.rs`: Third-party lexicons
45
45
+
- `read_tangled_repo.rs`: Reading git repo metadata from tangled.sh
46
46
+
- `resolve_did.rs`: Identity resolution examples
47
47
+
- `public_atproto_feed.rs`: Unauthenticated feed access
48
48
+
- `axum_server.rs`: Server-side XRPC handler
49
49
+
50
50
+
### Changed
51
51
+
52
52
+
**Code organization** (`jacquard-lexicon`)
53
53
+
- Refactored monolithic `codegen.rs` into focused modules:
54
54
+
- `codegen/structs.rs`: Record and object generation
55
55
+
- `codegen/xrpc.rs`: XRPC request/response generation
56
56
+
- `codegen/types.rs`: Type alias and union generation
57
57
+
- `codegen/names.rs`: Identifier sanitization and naming
58
58
+
- `codegen/lifetime.rs`: Lifetime propagation logic
59
59
+
- `codegen/output.rs`: Module and feature generation
60
60
+
- `codegen/utils.rs`: Shared utilities
61
61
+
- Improved code navigation and maintainability
62
62
+
63
63
+
**Documentation** (`jacquard`)
64
64
+
- Added comprehensive trait-level docs for `AgentSessionExt`
65
65
+
- Updated examples to use new convenience methods
66
66
+
67
67
+
### Fixed
68
68
+
69
69
+
- `update_record` now works with all record types without lifetime issues
70
70
+
- Proper `IdentityResolver` bounds on `AgentSessionExt`
71
71
+
72
72
+
## [0.4.1] - 2025-10-13
73
73
+
74
74
+
### Added
75
75
+
76
76
+
**Collection trait improvements** (`jacquard-api`)
77
77
+
- Generated `{Type}Record` marker structs for all record types
78
78
+
- Each implements `XrpcResp` with `Output<'de> = {Type}<'de>` and `Err<'de> = RecordError<'de>`
79
79
+
- Enables typed `get_record` returning `Response<R::Record>`
80
80
+
81
81
+
### Changed
82
82
+
83
83
+
- Minor improvements to derive macros (`jacquard-derive`)
84
84
+
- Identity resolution refinements (`jacquard-identity`)
85
85
+
- OAuth client improvements (`jacquard-oauth`)
86
86
+
3
87
## [0.4.0] - 2025-10-11
4
88
5
89
### Breaking Changes
+22
-110
Cargo.lock
···
1754
1754
1755
1755
[[package]]
1756
1756
name = "jacquard"
1757
1757
-
version = "0.4.0"
1757
1757
+
version = "0.5.0"
1758
1758
dependencies = [
1759
1759
"async-trait",
1760
1760
"bon",
1761
1761
"bytes",
1762
1762
"clap",
1763
1763
"http",
1764
1764
-
"jacquard-api 0.4.1",
1765
1765
-
"jacquard-common 0.4.0",
1766
1766
-
"jacquard-derive 0.4.0",
1767
1767
-
"jacquard-identity 0.4.0",
1764
1764
+
"jacquard-api",
1765
1765
+
"jacquard-common",
1766
1766
+
"jacquard-derive",
1767
1767
+
"jacquard-identity",
1768
1768
"jacquard-oauth",
1769
1769
"jose-jwk",
1770
1770
"miette",
···
1785
1785
1786
1786
[[package]]
1787
1787
name = "jacquard-api"
1788
1788
-
version = "0.4.0"
1789
1789
-
source = "git+https://tangled.org/@nonbinary.computer/jacquard#0cbdaf71e0721122b354892bb8ae49aa3ffcc9bc"
1790
1790
-
dependencies = [
1791
1791
-
"bon",
1792
1792
-
"bytes",
1793
1793
-
"jacquard-common 0.4.0 (git+https://tangled.org/@nonbinary.computer/jacquard)",
1794
1794
-
"jacquard-derive 0.4.0 (git+https://tangled.org/@nonbinary.computer/jacquard)",
1795
1795
-
"miette",
1796
1796
-
"serde",
1797
1797
-
"thiserror 2.0.17",
1798
1798
-
]
1799
1799
-
1800
1800
-
[[package]]
1801
1801
-
name = "jacquard-api"
1802
1788
version = "0.4.1"
1803
1789
dependencies = [
1804
1790
"bon",
1805
1791
"bytes",
1806
1806
-
"jacquard-common 0.4.0",
1807
1807
-
"jacquard-derive 0.4.0",
1792
1792
+
"jacquard-common",
1793
1793
+
"jacquard-derive",
1808
1794
"miette",
1809
1795
"serde",
1810
1796
"thiserror 2.0.17",
···
1812
1798
1813
1799
[[package]]
1814
1800
name = "jacquard-axum"
1815
1815
-
version = "0.4.0"
1801
1801
+
version = "0.5.0"
1816
1802
dependencies = [
1817
1803
"axum",
1818
1804
"axum-macros",
1819
1805
"axum-test",
1820
1806
"bytes",
1821
1807
"jacquard",
1822
1822
-
"jacquard-common 0.4.0",
1808
1808
+
"jacquard-common",
1823
1809
"miette",
1824
1810
"serde",
1825
1811
"serde_html_form",
···
1835
1821
1836
1822
[[package]]
1837
1823
name = "jacquard-common"
1838
1838
-
version = "0.4.0"
1824
1824
+
version = "0.5.0"
1839
1825
dependencies = [
1840
1826
"async-trait",
1841
1827
"base64 0.22.1",
···
1869
1855
]
1870
1856
1871
1857
[[package]]
1872
1872
-
name = "jacquard-common"
1873
1873
-
version = "0.4.0"
1874
1874
-
source = "git+https://tangled.org/@nonbinary.computer/jacquard#0cbdaf71e0721122b354892bb8ae49aa3ffcc9bc"
1875
1875
-
dependencies = [
1876
1876
-
"async-trait",
1877
1877
-
"base64 0.22.1",
1878
1878
-
"bon",
1879
1879
-
"bytes",
1880
1880
-
"chrono",
1881
1881
-
"cid",
1882
1882
-
"http",
1883
1883
-
"ipld-core",
1884
1884
-
"langtag",
1885
1885
-
"miette",
1886
1886
-
"multibase",
1887
1887
-
"multihash",
1888
1888
-
"num-traits",
1889
1889
-
"ouroboros",
1890
1890
-
"rand 0.9.2",
1891
1891
-
"regex",
1892
1892
-
"reqwest",
1893
1893
-
"serde",
1894
1894
-
"serde_html_form",
1895
1895
-
"serde_ipld_dagcbor",
1896
1896
-
"serde_json",
1897
1897
-
"serde_with",
1898
1898
-
"smol_str",
1899
1899
-
"thiserror 2.0.17",
1900
1900
-
"tokio",
1901
1901
-
"trait-variant",
1902
1902
-
"url",
1903
1903
-
]
1904
1904
-
1905
1905
-
[[package]]
1906
1858
name = "jacquard-derive"
1907
1907
-
version = "0.4.0"
1859
1859
+
version = "0.5.0"
1908
1860
dependencies = [
1909
1861
"heck 0.5.0",
1910
1862
"itertools",
1911
1911
-
"jacquard-common 0.4.0",
1912
1912
-
"prettyplease",
1913
1913
-
"proc-macro2",
1914
1914
-
"quote",
1915
1915
-
"serde",
1916
1916
-
"serde_json",
1917
1917
-
"serde_repr",
1918
1918
-
"serde_with",
1919
1919
-
"syn 2.0.106",
1920
1920
-
]
1921
1921
-
1922
1922
-
[[package]]
1923
1923
-
name = "jacquard-derive"
1924
1924
-
version = "0.4.0"
1925
1925
-
source = "git+https://tangled.org/@nonbinary.computer/jacquard#0cbdaf71e0721122b354892bb8ae49aa3ffcc9bc"
1926
1926
-
dependencies = [
1927
1927
-
"heck 0.5.0",
1928
1928
-
"itertools",
1863
1863
+
"jacquard-common",
1929
1864
"prettyplease",
1930
1865
"proc-macro2",
1931
1866
"quote",
···
1938
1873
1939
1874
[[package]]
1940
1875
name = "jacquard-identity"
1941
1941
-
version = "0.4.0"
1876
1876
+
version = "0.4.1"
1942
1877
dependencies = [
1943
1878
"async-trait",
1944
1879
"bon",
1945
1880
"bytes",
1946
1881
"hickory-resolver",
1947
1882
"http",
1948
1948
-
"jacquard-api 0.4.1",
1949
1949
-
"jacquard-common 0.4.0",
1950
1950
-
"miette",
1951
1951
-
"percent-encoding",
1952
1952
-
"reqwest",
1953
1953
-
"serde",
1954
1954
-
"serde_html_form",
1955
1955
-
"serde_json",
1956
1956
-
"thiserror 2.0.17",
1957
1957
-
"tokio",
1958
1958
-
"url",
1959
1959
-
"urlencoding",
1960
1960
-
]
1961
1961
-
1962
1962
-
[[package]]
1963
1963
-
name = "jacquard-identity"
1964
1964
-
version = "0.4.0"
1965
1965
-
source = "git+https://tangled.org/@nonbinary.computer/jacquard#0cbdaf71e0721122b354892bb8ae49aa3ffcc9bc"
1966
1966
-
dependencies = [
1967
1967
-
"async-trait",
1968
1968
-
"bon",
1969
1969
-
"bytes",
1970
1970
-
"http",
1971
1971
-
"jacquard-api 0.4.0",
1972
1972
-
"jacquard-common 0.4.0 (git+https://tangled.org/@nonbinary.computer/jacquard)",
1883
1883
+
"jacquard-api",
1884
1884
+
"jacquard-common",
1973
1885
"miette",
1974
1886
"percent-encoding",
1975
1887
"reqwest",
···
1984
1896
1985
1897
[[package]]
1986
1898
name = "jacquard-lexicon"
1987
1987
-
version = "0.4.0"
1899
1899
+
version = "0.5.0"
1988
1900
dependencies = [
1989
1901
"async-trait",
1990
1902
"clap",
1991
1903
"glob",
1992
1904
"heck 0.5.0",
1993
1905
"itertools",
1994
1994
-
"jacquard-api 0.4.0",
1995
1995
-
"jacquard-common 0.4.0 (git+https://tangled.org/@nonbinary.computer/jacquard)",
1996
1996
-
"jacquard-identity 0.4.0 (git+https://tangled.org/@nonbinary.computer/jacquard)",
1906
1906
+
"jacquard-api",
1907
1907
+
"jacquard-common",
1908
1908
+
"jacquard-identity",
1997
1909
"kdl",
1998
1910
"miette",
1999
1911
"prettyplease",
···
2013
1925
2014
1926
[[package]]
2015
1927
name = "jacquard-oauth"
2016
2016
-
version = "0.4.0"
1928
1928
+
version = "0.4.1"
2017
1929
dependencies = [
2018
1930
"async-trait",
2019
1931
"base64 0.22.1",
···
2022
1934
"dashmap",
2023
1935
"elliptic-curve",
2024
1936
"http",
2025
2025
-
"jacquard-common 0.4.0",
2026
2026
-
"jacquard-identity 0.4.0",
1937
1937
+
"jacquard-common",
1938
1938
+
"jacquard-identity",
2027
1939
"jose-jwa",
2028
1940
"jose-jwk",
2029
1941
"miette",
+1
-1
Cargo.toml
···
5
5
6
6
[workspace.package]
7
7
edition = "2024"
8
8
-
version = "0.4.0"
8
8
+
version = "0.5.0"
9
9
authors = ["Orual <orual@nonbinary.computer>"]
10
10
repository = "https://tangled.org/@nonbinary.computer/jacquard"
11
11
keywords = ["atproto", "at", "bluesky", "api", "client"]
+2
-2
crates/jacquard-api/Cargo.toml
···
17
17
[dependencies]
18
18
bon.workspace = true
19
19
bytes = { workspace = true, features = ["serde"] }
20
20
-
jacquard-common = { version = "0.4", path = "../jacquard-common" }
21
21
-
jacquard-derive = { version = "0.4", path = "../jacquard-derive" }
20
20
+
jacquard-common = { version = "0.5", path = "../jacquard-common" }
21
21
+
jacquard-derive = { version = "0.5", path = "../jacquard-derive" }
22
22
miette.workspace = true
23
23
serde.workspace = true
24
24
thiserror.workspace = true
+3
-3
crates/jacquard-axum/Cargo.toml
···
1
1
[package]
2
2
name = "jacquard-axum"
3
3
edition.workspace = true
4
4
-
version = "0.4.0"
4
4
+
version = "0.5.0"
5
5
authors.workspace = true
6
6
repository.workspace = true
7
7
keywords.workspace = true
···
24
24
axum = "0.8.6"
25
25
axum-macros = "0.5.0"
26
26
bytes.workspace = true
27
27
-
jacquard = { version = "0.4", path = "../jacquard" }
28
28
-
jacquard-common = { version = "0.4", path = "../jacquard-common", features = ["reqwest-client"] }
27
27
+
jacquard = { version = "0.5", path = "../jacquard" }
28
28
+
jacquard-common = { version = "0.5", path = "../jacquard-common", features = ["reqwest-client"] }
29
29
miette.workspace = true
30
30
serde.workspace = true
31
31
serde_html_form.workspace = true
+1
-1
crates/jacquard-common/Cargo.toml
···
2
2
name = "jacquard-common"
3
3
description = "Core AT Protocol types and utilities for Jacquard"
4
4
edition.workspace = true
5
5
-
version = "0.4.0"
5
5
+
version = "0.5.0"
6
6
authors.workspace = true
7
7
repository.workspace = true
8
8
keywords.workspace = true
+1
-1
crates/jacquard-derive/Cargo.toml
···
28
28
29
29
30
30
[dev-dependencies]
31
31
-
jacquard-common = { version = "0.4", path = "../jacquard-common" }
31
31
+
jacquard-common = { version = "0.5", path = "../jacquard-common" }
+3
-3
crates/jacquard-identity/Cargo.toml
···
1
1
[package]
2
2
name = "jacquard-identity"
3
3
edition.workspace = true
4
4
-
version = "0.4.0"
4
4
+
version = "0.4.1"
5
5
authors.workspace = true
6
6
repository.workspace = true
7
7
keywords.workspace = true
···
19
19
async-trait.workspace = true
20
20
bon.workspace = true
21
21
bytes.workspace = true
22
22
-
jacquard-common = { version = "0.4", path = "../jacquard-common", features = ["reqwest-client"] }
23
23
-
jacquard-api = { version = "0.4", path = "../jacquard-api" }
22
22
+
jacquard-common = { version = "0.5", path = "../jacquard-common", features = ["reqwest-client"] }
23
23
+
jacquard-api = { version = "0.4.1", path = "../jacquard-api" }
24
24
percent-encoding.workspace = true
25
25
reqwest.workspace = true
26
26
url.workspace = true
+3
-3
crates/jacquard-lexicon/Cargo.toml
···
25
25
glob = "0.3"
26
26
heck.workspace = true
27
27
itertools.workspace = true
28
28
-
jacquard-api = { version = "0.4", git = "https://tangled.org/@nonbinary.computer/jacquard" }
29
29
-
jacquard-common = { version = "0.4", git = "https://tangled.org/@nonbinary.computer/jacquard" }
30
30
-
jacquard-identity = { version = "0.4", git = "https://tangled.org/@nonbinary.computer/jacquard" }
28
28
+
jacquard-api = { version = "0.4.1", 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.4.1", git = "https://tangled.org/@nonbinary.computer/jacquard" }
31
31
kdl = "6"
32
32
miette = { workspace = true, features = ["fancy"] }
33
33
prettyplease.workspace = true
+3
-3
crates/jacquard-oauth/Cargo.toml
···
1
1
[package]
2
2
name = "jacquard-oauth"
3
3
-
version = "0.4.0"
3
3
+
version = "0.4.1"
4
4
edition.workspace = true
5
5
description = "AT Protocol OAuth 2.1 core types and helpers for Jacquard"
6
6
authors.workspace = true
···
12
12
license.workspace = true
13
13
14
14
[dependencies]
15
15
-
jacquard-common = { version = "0.4", path = "../jacquard-common", features = ["reqwest-client"] }
16
16
-
jacquard-identity = { version = "0.4", path = "../jacquard-identity" }
15
15
+
jacquard-common = { version = "0.5", path = "../jacquard-common", features = ["reqwest-client"] }
16
16
+
jacquard-identity = { version = "0.4.1", path = "../jacquard-identity" }
17
17
serde = { workspace = true, features = ["derive"] }
18
18
serde_json = { workspace = true }
19
19
url = { workspace = true }
+5
-5
crates/jacquard/Cargo.toml
···
79
79
required-features = ["fancy"]
80
80
81
81
[dependencies]
82
82
-
jacquard-api = { version = "0.4", path = "../jacquard-api" }
83
83
-
jacquard-common = { version = "0.4", path = "../jacquard-common", features = ["reqwest-client"] }
84
84
-
jacquard-oauth = { version = "0.4", path = "../jacquard-oauth" }
85
85
-
jacquard-derive = { version = "0.4", path = "../jacquard-derive", optional = true }
86
86
-
jacquard-identity = { version = "0.4", path = "../jacquard-identity" }
82
82
+
jacquard-api = { version = "0.4.1", path = "../jacquard-api" }
83
83
+
jacquard-common = { version = "0.5", path = "../jacquard-common", features = ["reqwest-client"] }
84
84
+
jacquard-oauth = { version = "0.4.1", path = "../jacquard-oauth" }
85
85
+
jacquard-derive = { version = "0.5", path = "../jacquard-derive", optional = true }
86
86
+
jacquard-identity = { version = "0.4.1", path = "../jacquard-identity" }
87
87
88
88
bon.workspace = true
89
89
async-trait.workspace = true
+402
-279
crates/jacquard/src/client.rs
···
27
27
28
28
use jacquard_api::com_atproto::repo::create_record::CreateRecordOutput;
29
29
use jacquard_api::com_atproto::repo::delete_record::DeleteRecordOutput;
30
30
+
use jacquard_api::com_atproto::repo::get_record::GetRecordResponse;
30
31
use jacquard_api::com_atproto::repo::put_record::PutRecordOutput;
32
32
+
use jacquard_api::com_atproto::repo::upload_blob::UploadBlobResponse;
31
33
use jacquard_api::com_atproto::server::create_session::CreateSessionOutput;
32
34
use jacquard_api::com_atproto::server::refresh_session::RefreshSessionOutput;
33
35
use jacquard_common::error::TransportError;
···
318
320
pub async fn refresh(&self) -> Result<AuthorizationToken<'static>, ClientError> {
319
321
self.inner.refresh().await
320
322
}
323
323
+
}
321
324
322
322
-
// Convenience methods for repository operations
323
323
-
325
325
+
/// Extension trait providing convenience methods for common repository operations.
326
326
+
///
327
327
+
/// This trait is automatically implemented for any type that implements both
328
328
+
/// [`AgentSession`] and [`IdentityResolver`]. It provides higher-level methods
329
329
+
/// that handle common patterns like fetch-modify-put, with automatic repo resolution
330
330
+
/// for at:// uris, and typed record operations.
331
331
+
///
332
332
+
/// # Available Operations
333
333
+
///
334
334
+
/// - **Basic CRUD**: [`create_record`](Self::create_record), [`get_record`](Self::get_record),
335
335
+
/// [`put_record`](Self::put_record), [`delete_record`](Self::delete_record)
336
336
+
/// - **Update patterns**: [`update_record`](Self::update_record) (fetch-modify-put for records),
337
337
+
/// [`update_vec`](Self::update_vec) and [`update_vec_item`](Self::update_vec_item) (for array endpoints)
338
338
+
/// - **Blob operations**: [`upload_blob`](Self::upload_blob)
339
339
+
///
340
340
+
/// # Example
341
341
+
///
342
342
+
/// ```no_run
343
343
+
/// # use jacquard::client::BasicClient;
344
344
+
/// # use jacquard_api::app_bsky::feed::post::Post;
345
345
+
/// # use jacquard_common::types::string::{AtUri, Datetime};
346
346
+
/// # use jacquard_common::CowStr;
347
347
+
/// use jacquard::client::AgentSessionExt;
348
348
+
/// # #[tokio::main]
349
349
+
/// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
350
350
+
/// # let agent: BasicClient = todo!();
351
351
+
/// // Create a post
352
352
+
/// let post = Post {
353
353
+
/// text: CowStr::from("Hello from Jacquard!"),
354
354
+
/// created_at: Datetime::now(),
355
355
+
/// # embed: None, entities: None, facets: None, labels: None,
356
356
+
/// # langs: None, reply: None, tags: None, extra_data: Default::default(),
357
357
+
/// };
358
358
+
/// let output = agent.create_record(post, None).await?;
359
359
+
///
360
360
+
/// // Read it back
361
361
+
/// let response = agent.get_record::<Post>(output.uri).await?;
362
362
+
/// let record = response.parse()?;
363
363
+
/// println!("Post: {}", record.value.text);
364
364
+
/// # Ok(())
365
365
+
/// # }
366
366
+
/// ```
367
367
+
pub trait AgentSessionExt: AgentSession + IdentityResolver {
324
368
/// Create a new record in the repository.
325
369
///
326
370
/// The collection is inferred from the record type's `Collection::NSID`.
···
333
377
/// # use jacquard_api::app_bsky::feed::post::Post;
334
378
/// # use jacquard_common::types::string::Datetime;
335
379
/// # use jacquard_common::CowStr;
380
380
+
/// use jacquard::client::AgentSessionExt;
336
381
/// # #[tokio::main]
337
382
/// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
338
383
/// # let agent: BasicClient = todo!();
···
353
398
/// # Ok(())
354
399
/// # }
355
400
/// ```
356
356
-
pub async fn create_record<R>(
401
401
+
fn create_record<R>(
357
402
&self,
358
403
record: R,
359
404
rkey: Option<RecordKey<Rkey<'_>>>,
360
360
-
) -> Result<CreateRecordOutput<'static>, AgentError>
405
405
+
) -> impl std::future::Future<Output = Result<CreateRecordOutput<'static>, AgentError>>
361
406
where
362
407
R: Collection + serde::Serialize,
363
408
{
364
364
-
use jacquard_api::com_atproto::repo::create_record::CreateRecord;
365
365
-
use jacquard_common::types::ident::AtIdentifier;
366
366
-
use jacquard_common::types::value::to_data;
409
409
+
async move {
410
410
+
use jacquard_api::com_atproto::repo::create_record::CreateRecord;
411
411
+
use jacquard_common::types::ident::AtIdentifier;
412
412
+
use jacquard_common::types::value::to_data;
413
413
+
414
414
+
let (did, _) = self.session_info().await.ok_or(AgentError::NoSession)?;
415
415
+
416
416
+
let data = to_data(&record).map_err(|e| AgentError::SubOperation {
417
417
+
step: "serialize record",
418
418
+
error: Box::new(e),
419
419
+
})?;
420
420
+
421
421
+
let request = CreateRecord::new()
422
422
+
.repo(AtIdentifier::Did(did))
423
423
+
.collection(R::nsid())
424
424
+
.record(data)
425
425
+
.maybe_rkey(rkey)
426
426
+
.build();
427
427
+
428
428
+
let response = self.send(request).await?;
429
429
+
response.into_output().map_err(|e| match e {
430
430
+
XrpcError::Auth(auth) => AgentError::Auth(auth),
431
431
+
XrpcError::Generic(g) => AgentError::Generic(g),
432
432
+
XrpcError::Decode(e) => AgentError::Decode(DecodeError::Json(e)),
433
433
+
XrpcError::Xrpc(typed) => AgentError::SubOperation {
434
434
+
step: "create record",
435
435
+
error: Box::new(typed),
436
436
+
},
437
437
+
})
438
438
+
}
439
439
+
}
440
440
+
441
441
+
/// Get a record from the repository using an at:// URI.
442
442
+
///
443
443
+
/// Returns a typed `Response` that deserializes directly to the record type.
444
444
+
/// Use `.parse()` to borrow from the response buffer, or `.into_output()` for owned data.
445
445
+
///
446
446
+
/// # Example
447
447
+
///
448
448
+
/// ```no_run
449
449
+
/// # use jacquard::client::BasicClient;
450
450
+
/// # use jacquard_api::app_bsky::feed::post::Post;
451
451
+
/// # use jacquard_common::types::string::AtUri;
452
452
+
/// # use jacquard_common::IntoStatic;
453
453
+
/// use jacquard::client::AgentSessionExt;
454
454
+
/// # #[tokio::main]
455
455
+
/// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
456
456
+
/// # let agent: BasicClient = todo!();
457
457
+
/// let uri = AtUri::new_static("at://did:plc:xyz/app.bsky.feed.post/3l5bqm7lepk2c").unwrap();
458
458
+
/// let response = agent.get_record::<Post>(uri).await?;
459
459
+
/// let output = response.parse()?; // PostGetRecordOutput<'_> borrowing from buffer
460
460
+
/// println!("Post text: {}", output.value.text);
461
461
+
///
462
462
+
/// // Or get owned data
463
463
+
/// let output_owned = response.into_output()?;
464
464
+
/// # Ok(())
465
465
+
/// # }
466
466
+
/// ```
467
467
+
fn get_record<R>(
468
468
+
&self,
469
469
+
uri: AtUri<'_>,
470
470
+
) -> impl std::future::Future<Output = Result<Response<R::Record>, ClientError>>
471
471
+
where
472
472
+
R: Collection,
473
473
+
{
474
474
+
async move {
475
475
+
// Validate that URI's collection matches the expected type
476
476
+
if let Some(uri_collection) = uri.collection() {
477
477
+
if uri_collection.as_str() != R::nsid().as_str() {
478
478
+
return Err(ClientError::Transport(TransportError::Other(
479
479
+
format!(
480
480
+
"Collection mismatch: URI contains '{}' but type parameter expects '{}'",
481
481
+
uri_collection,
482
482
+
R::nsid()
483
483
+
)
484
484
+
.into(),
485
485
+
)));
486
486
+
}
487
487
+
}
488
488
+
489
489
+
let rkey = uri.rkey().ok_or_else(|| {
490
490
+
ClientError::Transport(TransportError::Other("AtUri missing rkey".into()))
491
491
+
})?;
492
492
+
493
493
+
// Resolve authority (DID or handle) to get DID and PDS
494
494
+
use jacquard_common::types::ident::AtIdentifier;
495
495
+
let (repo_did, pds_url) = match uri.authority() {
496
496
+
AtIdentifier::Did(did) => {
497
497
+
let pds = self.pds_for_did(did).await.map_err(|e| {
498
498
+
ClientError::Transport(TransportError::Other(
499
499
+
format!("Failed to resolve PDS for {}: {}", did, e).into(),
500
500
+
))
501
501
+
})?;
502
502
+
(did.clone(), pds)
503
503
+
}
504
504
+
AtIdentifier::Handle(handle) => self.pds_for_handle(handle).await.map_err(|e| {
505
505
+
ClientError::Transport(TransportError::Other(
506
506
+
format!("Failed to resolve handle {}: {}", handle, e).into(),
507
507
+
))
508
508
+
})?,
509
509
+
};
510
510
+
511
511
+
// Make stateless XRPC call to that PDS (no auth required for public records)
512
512
+
use jacquard_api::com_atproto::repo::get_record::GetRecord;
513
513
+
let request = GetRecord::new()
514
514
+
.repo(AtIdentifier::Did(repo_did))
515
515
+
.collection(R::nsid())
516
516
+
.rkey(rkey.clone())
517
517
+
.build();
518
518
+
519
519
+
let response: Response<GetRecordResponse> = {
520
520
+
let http_request = xrpc::build_http_request(&pds_url, &request, &self.opts().await)
521
521
+
.map_err(|e| ClientError::Transport(TransportError::from(e)))?;
522
522
+
523
523
+
let http_response = self
524
524
+
.send_http(http_request)
525
525
+
.await
526
526
+
.map_err(|e| ClientError::Transport(TransportError::Other(Box::new(e))))?;
527
527
+
528
528
+
xrpc::process_response(http_response)
529
529
+
}?;
530
530
+
Ok(response.transmute())
531
531
+
}
532
532
+
}
533
533
+
534
534
+
/// Update a record in-place with a fetch-modify-put pattern.
535
535
+
///
536
536
+
/// This fetches the record using an at:// URI, converts it to owned data, applies
537
537
+
/// the modification function, and puts it back. The modification function receives
538
538
+
/// a mutable reference to the record data.
539
539
+
///
540
540
+
/// # Example
541
541
+
///
542
542
+
/// ```no_run
543
543
+
/// # use jacquard::client::BasicClient;
544
544
+
/// # use jacquard_api::app_bsky::actor::profile::Profile;
545
545
+
/// # use jacquard_common::CowStr;
546
546
+
/// # use jacquard_common::types::string::AtUri;
547
547
+
/// use jacquard::client::AgentSessionExt;
548
548
+
/// # #[tokio::main]
549
549
+
/// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
550
550
+
/// # let agent: BasicClient = todo!();
551
551
+
/// let uri = AtUri::new_static("at://did:plc:xyz/app.bsky.actor.profile/self").unwrap();
552
552
+
/// // Update profile record in-place
553
553
+
/// agent.update_record::<Profile>(uri, |profile| {
554
554
+
/// profile.display_name = Some(CowStr::from("New Name"));
555
555
+
/// profile.description = Some(CowStr::from("Updated bio"));
556
556
+
/// }).await?;
557
557
+
/// # Ok(())
558
558
+
/// # }
559
559
+
/// ```
560
560
+
fn update_record<R>(
561
561
+
&self,
562
562
+
uri: AtUri<'_>,
563
563
+
f: impl FnOnce(&mut R),
564
564
+
) -> impl std::future::Future<Output = Result<PutRecordOutput<'static>, AgentError>>
565
565
+
where
566
566
+
R: Collection + Serialize,
567
567
+
R: for<'a> From<<<R as Collection>::Record as XrpcResp>::Output<'a>>,
568
568
+
{
569
569
+
async move {
570
570
+
// Fetch the record - Response<R::Record> where R::Record::Output<'de> = R<'de>
571
571
+
let response = self.get_record::<R>(uri.clone()).await?;
367
572
368
368
-
let (did, _) = self.info().await.ok_or(AgentError::NoSession)?;
573
573
+
// Parse to get R<'_> borrowing from response buffer
574
574
+
let record = response.parse().map_err(|e| match e {
575
575
+
XrpcError::Auth(auth) => AgentError::Auth(auth),
576
576
+
XrpcError::Generic(g) => AgentError::Generic(g),
577
577
+
XrpcError::Decode(e) => AgentError::Decode(DecodeError::Json(e)),
578
578
+
XrpcError::Xrpc(typed) => AgentError::SubOperation {
579
579
+
step: "get record",
580
580
+
error: format!("{:?}", typed).into(),
581
581
+
},
582
582
+
})?;
369
583
370
370
-
let data = to_data(&record).map_err(|e| AgentError::SubOperation {
371
371
-
step: "serialize record",
372
372
-
error: Box::new(e),
373
373
-
})?;
584
584
+
// Convert to owned
585
585
+
let mut owned = R::from(record);
374
586
375
375
-
let request = CreateRecord::new()
376
376
-
.repo(AtIdentifier::Did(did))
377
377
-
.collection(R::nsid())
378
378
-
.record(data)
379
379
-
.maybe_rkey(rkey)
380
380
-
.build();
587
587
+
// Apply modification
588
588
+
f(&mut owned);
381
589
382
382
-
let response = self.send(request).await?;
383
383
-
response.into_output().map_err(|e| match e {
384
384
-
XrpcError::Auth(auth) => AgentError::Auth(auth),
385
385
-
XrpcError::Generic(g) => AgentError::Generic(g),
386
386
-
XrpcError::Decode(e) => AgentError::Decode(DecodeError::Json(e)),
387
387
-
XrpcError::Xrpc(typed) => AgentError::SubOperation {
388
388
-
step: "create record",
389
389
-
error: Box::new(typed),
390
390
-
},
391
391
-
})
590
590
+
// Put it back
591
591
+
let rkey = uri
592
592
+
.rkey()
593
593
+
.ok_or(AgentError::SubOperation {
594
594
+
step: "extract rkey",
595
595
+
error: "AtUri missing rkey".into(),
596
596
+
})?
597
597
+
.clone()
598
598
+
.into_static();
599
599
+
self.put_record::<R>(rkey, owned).await
600
600
+
}
392
601
}
393
602
394
603
/// Delete a record from the repository.
395
604
///
396
605
/// The collection is inferred from the type parameter.
397
606
/// The repo is automatically filled from the session info.
398
398
-
pub async fn delete_record<R>(
607
607
+
fn delete_record<R>(
399
608
&self,
400
609
rkey: RecordKey<Rkey<'_>>,
401
401
-
) -> Result<DeleteRecordOutput<'static>, AgentError>
610
610
+
) -> impl std::future::Future<Output = Result<DeleteRecordOutput<'static>, AgentError>>
402
611
where
403
612
R: Collection,
404
613
{
405
405
-
use jacquard_api::com_atproto::repo::delete_record::DeleteRecord;
406
406
-
use jacquard_common::types::ident::AtIdentifier;
614
614
+
async {
615
615
+
use jacquard_api::com_atproto::repo::delete_record::DeleteRecord;
616
616
+
use jacquard_common::types::ident::AtIdentifier;
407
617
408
408
-
let (did, _) = self.info().await.ok_or(AgentError::NoSession)?;
618
618
+
let (did, _) = self.session_info().await.ok_or(AgentError::NoSession)?;
409
619
410
410
-
let request = DeleteRecord::new()
411
411
-
.repo(AtIdentifier::Did(did))
412
412
-
.collection(R::nsid())
413
413
-
.rkey(rkey)
414
414
-
.build();
620
620
+
let request = DeleteRecord::new()
621
621
+
.repo(AtIdentifier::Did(did))
622
622
+
.collection(R::nsid())
623
623
+
.rkey(rkey)
624
624
+
.build();
415
625
416
416
-
let response = self.send(request).await?;
417
417
-
response.into_output().map_err(|e| match e {
418
418
-
XrpcError::Auth(auth) => AgentError::Auth(auth),
419
419
-
XrpcError::Generic(g) => AgentError::Generic(g),
420
420
-
XrpcError::Decode(e) => AgentError::Decode(DecodeError::Json(e)),
421
421
-
XrpcError::Xrpc(typed) => AgentError::SubOperation {
422
422
-
step: "delete record",
423
423
-
error: Box::new(typed),
424
424
-
},
425
425
-
})
626
626
+
let response = self.send(request).await?;
627
627
+
response.into_output().map_err(|e| match e {
628
628
+
XrpcError::Auth(auth) => AgentError::Auth(auth),
629
629
+
XrpcError::Generic(g) => AgentError::Generic(g),
630
630
+
XrpcError::Decode(e) => AgentError::Decode(DecodeError::Json(e)),
631
631
+
XrpcError::Xrpc(typed) => AgentError::SubOperation {
632
632
+
step: "delete record",
633
633
+
error: Box::new(typed),
634
634
+
},
635
635
+
})
636
636
+
}
426
637
}
427
638
428
639
/// Put (upsert) a record in the repository.
429
640
///
430
641
/// The collection is inferred from the record type's `Collection::NSID`.
431
642
/// The repo is automatically filled from the session info.
432
432
-
pub async fn put_record<R>(
643
643
+
fn put_record<R>(
433
644
&self,
434
645
rkey: RecordKey<Rkey<'static>>,
435
646
record: R,
436
436
-
) -> Result<PutRecordOutput<'static>, AgentError>
647
647
+
) -> impl std::future::Future<Output = Result<PutRecordOutput<'static>, AgentError>>
437
648
where
438
649
R: Collection + serde::Serialize,
439
650
{
440
440
-
use jacquard_api::com_atproto::repo::put_record::PutRecord;
441
441
-
use jacquard_common::types::ident::AtIdentifier;
442
442
-
use jacquard_common::types::value::to_data;
651
651
+
async move {
652
652
+
use jacquard_api::com_atproto::repo::put_record::PutRecord;
653
653
+
use jacquard_common::types::ident::AtIdentifier;
654
654
+
use jacquard_common::types::value::to_data;
443
655
444
444
-
let (did, _) = self.info().await.ok_or(AgentError::NoSession)?;
656
656
+
let (did, _) = self.session_info().await.ok_or(AgentError::NoSession)?;
445
657
446
446
-
let data = to_data(&record).map_err(|e| AgentError::SubOperation {
447
447
-
step: "serialize record",
448
448
-
error: Box::new(e),
449
449
-
})?;
658
658
+
let data = to_data(&record).map_err(|e| AgentError::SubOperation {
659
659
+
step: "serialize record",
660
660
+
error: Box::new(e),
661
661
+
})?;
450
662
451
451
-
let request = PutRecord::new()
452
452
-
.repo(AtIdentifier::Did(did))
453
453
-
.collection(R::nsid())
454
454
-
.rkey(rkey)
455
455
-
.record(data)
456
456
-
.build();
663
663
+
let request = PutRecord::new()
664
664
+
.repo(AtIdentifier::Did(did))
665
665
+
.collection(R::nsid())
666
666
+
.rkey(rkey)
667
667
+
.record(data)
668
668
+
.build();
457
669
458
458
-
let response = self.send(request).await?;
459
459
-
response.into_output().map_err(|e| match e {
460
460
-
XrpcError::Auth(auth) => AgentError::Auth(auth),
461
461
-
XrpcError::Generic(g) => AgentError::Generic(g),
462
462
-
XrpcError::Decode(e) => AgentError::Decode(DecodeError::Json(e)),
463
463
-
XrpcError::Xrpc(typed) => AgentError::SubOperation {
464
464
-
step: "put record",
465
465
-
error: Box::new(typed),
466
466
-
},
467
467
-
})
670
670
+
let response = self.send(request).await?;
671
671
+
response.into_output().map_err(|e| match e {
672
672
+
XrpcError::Auth(auth) => AgentError::Auth(auth),
673
673
+
XrpcError::Generic(g) => AgentError::Generic(g),
674
674
+
XrpcError::Decode(e) => AgentError::Decode(DecodeError::Json(e)),
675
675
+
XrpcError::Xrpc(typed) => AgentError::SubOperation {
676
676
+
step: "put record",
677
677
+
error: Box::new(typed),
678
678
+
},
679
679
+
})
680
680
+
}
468
681
}
469
682
470
683
/// Upload a blob to the repository.
···
477
690
/// ```no_run
478
691
/// # use jacquard::client::BasicClient;
479
692
/// # use jacquard_common::types::blob::MimeType;
693
693
+
/// use jacquard::client::AgentSessionExt;
480
694
/// # #[tokio::main]
481
695
/// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
482
696
/// # let agent: BasicClient = todo!();
···
486
700
/// # Ok(())
487
701
/// # }
488
702
/// ```
489
489
-
pub async fn upload_blob(
703
703
+
fn upload_blob(
490
704
&self,
491
705
data: impl Into<bytes::Bytes>,
492
706
mime_type: MimeType<'_>,
493
493
-
) -> Result<Blob<'static>, AgentError> {
494
494
-
use http::header::CONTENT_TYPE;
495
495
-
use jacquard_api::com_atproto::repo::upload_blob::UploadBlob;
707
707
+
) -> impl std::future::Future<Output = Result<Blob<'static>, AgentError>> {
708
708
+
async move {
709
709
+
use http::header::CONTENT_TYPE;
710
710
+
use jacquard_api::com_atproto::repo::upload_blob::UploadBlob;
711
711
+
712
712
+
let bytes = data.into();
713
713
+
let request = UploadBlob::new().body(bytes).build();
714
714
+
715
715
+
// Override Content-Type header with actual mime type instead of */*
716
716
+
let base = self.base_uri();
717
717
+
let mut opts = self.opts().await;
718
718
+
opts.extra_headers.push((
719
719
+
CONTENT_TYPE,
720
720
+
http::HeaderValue::from_str(mime_type.as_str()).map_err(|e| {
721
721
+
AgentError::SubOperation {
722
722
+
step: "set Content-Type header",
723
723
+
error: Box::new(e),
724
724
+
}
725
725
+
})?,
726
726
+
));
496
727
497
497
-
let bytes = data.into();
498
498
-
let request = UploadBlob::new().body(bytes).build();
728
728
+
let response: Response<UploadBlobResponse> = {
729
729
+
let http_request =
730
730
+
xrpc::build_http_request(&base, &request, &opts).map_err(|e| {
731
731
+
AgentError::Client(ClientError::Transport(TransportError::from(e)))
732
732
+
})?;
499
733
500
500
-
// Override Content-Type header with actual mime type instead of */*
501
501
-
let base = self.base_uri();
502
502
-
let mut opts = self.opts().await;
503
503
-
opts.extra_headers.push((
504
504
-
CONTENT_TYPE,
505
505
-
http::HeaderValue::from_str(mime_type.as_str()).map_err(|e| {
506
506
-
AgentError::SubOperation {
507
507
-
step: "set Content-Type header",
508
508
-
error: Box::new(e),
509
509
-
}
510
510
-
})?,
511
511
-
));
734
734
+
let http_response = self.send_http(http_request).await.map_err(|e| {
735
735
+
AgentError::Client(ClientError::Transport(TransportError::Other(Box::new(e))))
736
736
+
})?;
512
737
513
513
-
let response = self.xrpc(base).with_options(opts).send(&request).await?;
738
738
+
xrpc::process_response(http_response)
739
739
+
}?;
514
740
515
515
-
let output = response.into_output().map_err(|e| match e {
516
516
-
XrpcError::Auth(auth) => AgentError::Auth(auth),
517
517
-
XrpcError::Generic(g) => AgentError::Generic(g),
518
518
-
XrpcError::Decode(e) => AgentError::Decode(DecodeError::Json(e)),
519
519
-
XrpcError::Xrpc(typed) => AgentError::SubOperation {
520
520
-
step: "upload blob",
521
521
-
error: Box::new(typed),
522
522
-
},
523
523
-
})?;
524
524
-
Ok(output.blob.into_static())
741
741
+
let output = response.into_output().map_err(|e| match e {
742
742
+
XrpcError::Auth(auth) => AgentError::Auth(auth),
743
743
+
XrpcError::Generic(g) => AgentError::Generic(g),
744
744
+
XrpcError::Decode(e) => AgentError::Decode(DecodeError::Json(e)),
745
745
+
XrpcError::Xrpc(typed) => AgentError::SubOperation {
746
746
+
step: "upload blob",
747
747
+
error: Box::new(typed),
748
748
+
},
749
749
+
})?;
750
750
+
Ok(output.blob.into_static())
751
751
+
}
525
752
}
526
753
527
754
/// Update a vec-based data structure with a fetch-modify-put pattern.
···
537
764
/// prefs.retain(|p| !matches!(p, Preference::Hidden(_)));
538
765
/// }).await?;
539
766
/// ```
540
540
-
pub async fn update_vec<'s, U>(
767
767
+
fn update_vec<'s, U>(
541
768
&'s self,
542
769
modify: impl FnOnce(&mut Vec<<U as vec_update::VecUpdate>::Item>),
543
543
-
) -> Result<xrpc::Response<<U::PutRequest<'s> as XrpcRequest<'s>>::Response>, AgentError>
770
770
+
) -> impl std::future::Future<
771
771
+
Output = Result<
772
772
+
xrpc::Response<<U::PutRequest<'s> as XrpcRequest<'s>>::Response>,
773
773
+
AgentError,
774
774
+
>,
775
775
+
>
544
776
where
545
777
U: vec_update::VecUpdate + 's,
546
778
{
547
547
-
// Fetch current data
548
548
-
let get_request = U::build_get();
549
549
-
let response = self.send(get_request).await?;
550
550
-
let output = response.parse().map_err(|e| match e {
551
551
-
XrpcError::Auth(auth) => AgentError::Auth(auth),
552
552
-
XrpcError::Generic(g) => AgentError::Generic(g),
553
553
-
XrpcError::Decode(e) => AgentError::Decode(DecodeError::Json(e)),
554
554
-
XrpcError::Xrpc(_) => AgentError::SubOperation {
555
555
-
step: "get vec",
556
556
-
error: format!("{:?}", e).into(),
557
557
-
},
558
558
-
})?;
779
779
+
async {
780
780
+
// Fetch current data
781
781
+
let get_request = U::build_get();
782
782
+
let response = self.send(get_request).await?;
783
783
+
let output = response.parse().map_err(|e| match e {
784
784
+
XrpcError::Auth(auth) => AgentError::Auth(auth),
785
785
+
XrpcError::Generic(g) => AgentError::Generic(g),
786
786
+
XrpcError::Decode(e) => AgentError::Decode(DecodeError::Json(e)),
787
787
+
XrpcError::Xrpc(_) => AgentError::SubOperation {
788
788
+
step: "get vec",
789
789
+
error: format!("{:?}", e).into(),
790
790
+
},
791
791
+
})?;
559
792
560
560
-
// Extract vec (converts to owned via IntoStatic)
561
561
-
let mut items = U::extract_vec(output);
793
793
+
// Extract vec (converts to owned via IntoStatic)
794
794
+
let mut items = U::extract_vec(output);
562
795
563
563
-
// Apply modification
564
564
-
modify(&mut items);
796
796
+
// Apply modification
797
797
+
modify(&mut items);
565
798
566
566
-
// Build put request
567
567
-
let put_request = U::build_put(items);
799
799
+
// Build put request
800
800
+
let put_request = U::build_put(items);
568
801
569
569
-
// Send it
570
570
-
Ok(self.send(put_request).await?)
802
802
+
// Send it
803
803
+
Ok(self.send(put_request).await?)
804
804
+
}
571
805
}
572
806
573
807
/// Update a single item in a vec-based data structure.
···
581
815
/// let pref = AdultContentPref::new().enabled(true).build();
582
816
/// agent.update_vec_item::<PreferencesUpdate>(pref.into()).await?;
583
817
/// ```
584
584
-
pub async fn update_vec_item<'s, U>(
818
818
+
fn update_vec_item<'s, U>(
585
819
&'s self,
586
820
item: <U as vec_update::VecUpdate>::Item,
587
587
-
) -> Result<xrpc::Response<<U::PutRequest<'s> as XrpcRequest<'s>>::Response>, AgentError>
821
821
+
) -> impl std::future::Future<
822
822
+
Output = Result<
823
823
+
xrpc::Response<<U::PutRequest<'s> as XrpcRequest<'s>>::Response>,
824
824
+
AgentError,
825
825
+
>,
826
826
+
>
588
827
where
589
828
U: vec_update::VecUpdate + 's,
590
829
{
591
591
-
self.update_vec::<U>(|vec| {
592
592
-
if let Some(pos) = vec.iter().position(|i| U::matches(i, &item)) {
593
593
-
vec[pos] = item;
594
594
-
} else {
595
595
-
vec.push(item);
596
596
-
}
597
597
-
})
598
598
-
.await
599
599
-
}
600
600
-
}
601
601
-
602
602
-
impl<A: AgentSession + IdentityResolver> Agent<A> {
603
603
-
/// Get a record from the repository using an at:// URI.
604
604
-
///
605
605
-
/// Returns a typed `Response` that deserializes directly to the record type.
606
606
-
/// Use `.parse()` to borrow from the response buffer, or `.into_output()` for owned data.
607
607
-
///
608
608
-
/// # Example
609
609
-
///
610
610
-
/// ```no_run
611
611
-
/// # use jacquard::client::BasicClient;
612
612
-
/// # use jacquard_api::app_bsky::feed::post::Post;
613
613
-
/// # use jacquard_common::types::string::AtUri;
614
614
-
/// # use jacquard_common::IntoStatic;
615
615
-
/// # #[tokio::main]
616
616
-
/// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
617
617
-
/// # let agent: BasicClient = todo!();
618
618
-
/// let uri = AtUri::new_static("at://did:plc:xyz/app.bsky.feed.post/3l5bqm7lepk2c").unwrap();
619
619
-
/// let response = agent.get_record::<Post>(uri).await?;
620
620
-
/// let output = response.parse()?; // PostGetRecordOutput<'_> borrowing from buffer
621
621
-
/// println!("Post text: {}", output.value.text);
622
622
-
///
623
623
-
/// // Or get owned data
624
624
-
/// let output_owned = response.into_output()?;
625
625
-
/// # Ok(())
626
626
-
/// # }
627
627
-
/// ```
628
628
-
pub async fn get_record<R>(&self, uri: AtUri<'_>) -> Result<Response<R::Record>, ClientError>
629
629
-
where
630
630
-
R: Collection,
631
631
-
{
632
632
-
// Validate that URI's collection matches the expected type
633
633
-
if let Some(uri_collection) = uri.collection() {
634
634
-
if uri_collection.as_str() != R::nsid().as_str() {
635
635
-
return Err(ClientError::Transport(TransportError::Other(
636
636
-
format!(
637
637
-
"Collection mismatch: URI contains '{}' but type parameter expects '{}'",
638
638
-
uri_collection,
639
639
-
R::nsid()
640
640
-
)
641
641
-
.into(),
642
642
-
)));
643
643
-
}
830
830
+
async {
831
831
+
self.update_vec::<U>(|vec| {
832
832
+
if let Some(pos) = vec.iter().position(|i| U::matches(i, &item)) {
833
833
+
vec[pos] = item;
834
834
+
} else {
835
835
+
vec.push(item);
836
836
+
}
837
837
+
})
838
838
+
.await
644
839
}
645
645
-
646
646
-
let rkey = uri.rkey().ok_or_else(|| {
647
647
-
ClientError::Transport(TransportError::Other("AtUri missing rkey".into()))
648
648
-
})?;
649
649
-
650
650
-
// Resolve authority (DID or handle) to get DID and PDS
651
651
-
use jacquard_common::types::ident::AtIdentifier;
652
652
-
let (repo_did, pds_url) = match uri.authority() {
653
653
-
AtIdentifier::Did(did) => {
654
654
-
let pds = self.pds_for_did(did).await.map_err(|e| {
655
655
-
ClientError::Transport(TransportError::Other(
656
656
-
format!("Failed to resolve PDS for {}: {}", did, e).into(),
657
657
-
))
658
658
-
})?;
659
659
-
(did.clone(), pds)
660
660
-
}
661
661
-
AtIdentifier::Handle(handle) => self.pds_for_handle(handle).await.map_err(|e| {
662
662
-
ClientError::Transport(TransportError::Other(
663
663
-
format!("Failed to resolve handle {}: {}", handle, e).into(),
664
664
-
))
665
665
-
})?,
666
666
-
};
667
667
-
668
668
-
// Make stateless XRPC call to that PDS (no auth required for public records)
669
669
-
use jacquard_api::com_atproto::repo::get_record::GetRecord;
670
670
-
let request = GetRecord::new()
671
671
-
.repo(AtIdentifier::Did(repo_did))
672
672
-
.collection(R::nsid())
673
673
-
.rkey(rkey.clone())
674
674
-
.build();
675
675
-
676
676
-
let response = self.xrpc(pds_url).send(&request).await?;
677
677
-
Ok(response.transmute())
678
840
}
679
679
-
680
680
-
/// Update a record in-place with a fetch-modify-put pattern.
681
681
-
///
682
682
-
/// This fetches the record using an at:// URI, converts it to owned data, applies
683
683
-
/// the modification function, and puts it back. The modification function receives
684
684
-
/// a mutable reference to the record data.
685
685
-
///
686
686
-
/// # Example
687
687
-
///
688
688
-
/// ```no_run
689
689
-
/// # use jacquard::client::BasicClient;
690
690
-
/// # use jacquard_api::app_bsky::actor::profile::Profile;
691
691
-
/// # use jacquard_common::CowStr;
692
692
-
/// # use jacquard_common::types::string::AtUri;
693
693
-
/// # #[tokio::main]
694
694
-
/// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
695
695
-
/// # let agent: BasicClient = todo!();
696
696
-
/// let uri = AtUri::new_static("at://did:plc:xyz/app.bsky.actor.profile/self").unwrap();
697
697
-
/// // Update profile record in-place
698
698
-
/// agent.update_record::<Profile>(uri, |profile| {
699
699
-
/// profile.display_name = Some(CowStr::from("New Name"));
700
700
-
/// profile.description = Some(CowStr::from("Updated bio"));
701
701
-
/// }).await?;
702
702
-
/// # Ok(())
703
703
-
/// # }
704
704
-
/// ```
705
705
-
pub async fn update_record<R>(
706
706
-
&self,
707
707
-
uri: AtUri<'_>,
708
708
-
f: impl FnOnce(&mut R),
709
709
-
) -> Result<PutRecordOutput<'static>, AgentError>
710
710
-
where
711
711
-
R: Collection + Serialize,
712
712
-
R: for<'a> From<<<R as Collection>::Record as XrpcResp>::Output<'a>>,
713
713
-
{
714
714
-
// Fetch the record - Response<R::Record> where R::Record::Output<'de> = R<'de>
715
715
-
let response = self.get_record::<R>(uri.clone()).await?;
841
841
+
}
716
842
717
717
-
// Parse to get R<'_> borrowing from response buffer
718
718
-
let record = response.parse().map_err(|e| match e {
719
719
-
XrpcError::Auth(auth) => AgentError::Auth(auth),
720
720
-
XrpcError::Generic(g) => AgentError::Generic(g),
721
721
-
XrpcError::Decode(e) => AgentError::Decode(DecodeError::Json(e)),
722
722
-
XrpcError::Xrpc(typed) => AgentError::SubOperation {
723
723
-
step: "get record",
724
724
-
error: format!("{:?}", typed).into(),
725
725
-
},
726
726
-
})?;
727
727
-
728
728
-
// Convert to owned
729
729
-
let mut owned = R::from(record);
730
730
-
731
731
-
// Apply modification
732
732
-
f(&mut owned);
733
733
-
734
734
-
// Put it back
735
735
-
let rkey = uri
736
736
-
.rkey()
737
737
-
.ok_or(AgentError::SubOperation {
738
738
-
step: "extract rkey",
739
739
-
error: "AtUri missing rkey".into(),
740
740
-
})?
741
741
-
.clone()
742
742
-
.into_static();
743
743
-
self.put_record::<R>(rkey, owned).await
744
744
-
}
745
745
-
}
843
843
+
impl<T: AgentSession + IdentityResolver> AgentSessionExt for T {}
746
844
747
845
impl<A: AgentSession> HttpClient for Agent<A> {
748
846
type Error = <A as HttpClient>::Error;
···
800
898
}
801
899
}
802
900
901
901
+
impl<A: AgentSession> AgentSession for Agent<A> {
902
902
+
fn session_kind(&self) -> AgentKind {
903
903
+
self.kind()
904
904
+
}
905
905
+
906
906
+
fn session_info(
907
907
+
&self,
908
908
+
) -> impl Future<Output = Option<(Did<'static>, Option<CowStr<'static>>)>> {
909
909
+
async { self.info().await }
910
910
+
}
911
911
+
912
912
+
fn endpoint(&self) -> impl Future<Output = url::Url> {
913
913
+
async { self.endpoint().await }
914
914
+
}
915
915
+
916
916
+
fn set_options<'a>(&'a self, opts: CallOptions<'a>) -> impl Future<Output = ()> {
917
917
+
async { self.set_options(opts).await }
918
918
+
}
919
919
+
920
920
+
fn refresh(&self) -> impl Future<Output = Result<AuthorizationToken<'static>, ClientError>> {
921
921
+
async { self.refresh().await }
922
922
+
}
923
923
+
}
924
924
+
803
925
impl<A: AgentSession> From<A> for Agent<A> {
804
926
fn from(inner: A) -> Self {
805
927
Self::new(inner)
···
831
953
/// # use jacquard::client::BasicClient;
832
954
/// # use jacquard::types::string::AtUri;
833
955
/// # use jacquard_api::app_bsky::feed::post::Post;
956
956
+
/// use crate::jacquard::client::AgentSessionExt;
834
957
/// # #[tokio::main]
835
958
/// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
836
959
/// let client = BasicClient::unauthenticated();
+1
crates/jacquard/src/client/credential_session.rs
···
408
408
)
409
409
}
410
410
}
411
411
+
411
412
async fn send<'s, R>(
412
413
&self,
413
414
request: R,
+3
-38
examples/update_preferences.rs
···
1
1
use clap::Parser;
2
2
+
use jacquard::CowStr;
2
3
use jacquard::api::app_bsky::actor::{AdultContentPref, PreferencesItem};
3
3
-
use jacquard::client::vec_update::VecUpdate;
4
4
+
use jacquard::client::AgentSessionExt;
5
5
+
use jacquard::client::vec_update::PreferencesUpdate;
4
6
use jacquard::client::{Agent, FileAuthStore};
5
5
-
use jacquard::oauth::atproto::AtprotoClientMetadata;
6
7
use jacquard::oauth::client::OAuthClient;
7
8
use jacquard::oauth::loopback::LoopbackConfig;
8
8
-
use jacquard::{CowStr, IntoStatic};
9
9
10
10
#[derive(Parser, Debug)]
11
11
#[command(author, version, about = "Update Bluesky preferences")]
···
20
20
/// Path to auth store file (will be created if missing)
21
21
#[arg(long, default_value = "/tmp/jacquard-oauth-session.json")]
22
22
store: String,
23
23
-
}
24
24
-
25
25
-
/// Helper struct for the VecUpdate pattern on preferences
26
26
-
pub struct PreferencesUpdate;
27
27
-
28
28
-
impl VecUpdate for PreferencesUpdate {
29
29
-
type GetRequest<'de> = jacquard::api::app_bsky::actor::get_preferences::GetPreferences;
30
30
-
type PutRequest<'de> = jacquard::api::app_bsky::actor::put_preferences::PutPreferences<'de>;
31
31
-
type Item = PreferencesItem<'static>;
32
32
-
33
33
-
fn build_get<'s>() -> Self::GetRequest<'s> {
34
34
-
jacquard::api::app_bsky::actor::get_preferences::GetPreferences::new().build()
35
35
-
}
36
36
-
37
37
-
fn build_put<'s>(items: Vec<Self::Item>) -> Self::PutRequest<'s> {
38
38
-
jacquard::api::app_bsky::actor::put_preferences::PutPreferences {
39
39
-
preferences: items,
40
40
-
extra_data: Default::default(),
41
41
-
}
42
42
-
}
43
43
-
44
44
-
fn extract_vec(
45
45
-
output: jacquard::api::app_bsky::actor::get_preferences::GetPreferencesOutput<'_>,
46
46
-
) -> Vec<Self::Item> {
47
47
-
output
48
48
-
.preferences
49
49
-
.into_iter()
50
50
-
.map(|p| p.into_static())
51
51
-
.collect()
52
52
-
}
53
53
-
54
54
-
fn matches(a: &Self::Item, b: &Self::Item) -> bool {
55
55
-
// Match by enum variant discriminant
56
56
-
std::mem::discriminant(a) == std::mem::discriminant(b)
57
57
-
}
58
23
}
59
24
60
25
#[tokio::main]