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