+31
-2
Cargo.toml
+31
-2
Cargo.toml
···
15
15
exclude = [".direnv"]
16
16
17
17
18
-
description = "A simple Rust project using Nix"
19
-
18
+
description = "Simple and powerful AT Protocol client library for Rust"
20
19
21
20
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
22
21
23
22
[workspace.dependencies]
23
+
# CLI
24
24
clap = { version = "4.5", features = ["derive"] }
25
+
26
+
# Serialization
27
+
serde = { version = "1.0", features = ["derive"] }
28
+
serde_json = "1.0"
29
+
serde_with = "3.14"
30
+
serde_html_form = "0.2"
31
+
serde_ipld_dagcbor = "0.6"
32
+
serde_repr = "0.1"
33
+
34
+
# Error handling
35
+
miette = "7.6"
36
+
thiserror = "2.0"
37
+
38
+
# Data types
39
+
bytes = "1.10"
40
+
smol_str = { version = "0.3", features = ["serde"] }
41
+
url = "2.5"
42
+
43
+
# Proc macros
44
+
proc-macro2 = "1.0"
45
+
quote = "1.0"
46
+
syn = "2.0"
47
+
heck = "0.5"
48
+
itertools = "0.14"
49
+
prettyplease = "0.2"
50
+
51
+
# HTTP
52
+
http = "1.3"
53
+
reqwest = { version = "0.12", default-features = false }
+71
-26
README.md
+71
-26
README.md
···
2
2
3
3
A suite of Rust crates for the AT Protocol.
4
4
5
+
## Example
6
+
7
+
Dead simple api client. Logs in, prints the latest 5 posts from your timeline.
8
+
9
+
```rust
10
+
use clap::Parser;
11
+
use jacquard::CowStr;
12
+
use jacquard::api::app_bsky::feed::get_timeline::GetTimeline;
13
+
use jacquard::api::com_atproto::server::create_session::CreateSession;
14
+
use jacquard::client::{AuthenticatedClient, Session, XrpcClient};
15
+
use miette::IntoDiagnostic;
16
+
17
+
#[derive(Parser, Debug)]
18
+
#[command(author, version, about = "Jacquard - AT Protocol client demo")]
19
+
struct Args {
20
+
/// Username/handle (e.g., alice.mosphere.at)
21
+
#[arg(short, long)]
22
+
username: CowStr<'static>,
23
+
24
+
/// PDS URL (e.g., https://bsky.social)
25
+
#[arg(long, default_value = "https://bsky.social")]
26
+
pds: CowStr<'static>,
27
+
28
+
/// App password
29
+
#[arg(short, long)]
30
+
password: CowStr<'static>,
31
+
}
32
+
33
+
#[tokio::main]
34
+
async fn main() -> miette::Result<()> {
35
+
let args = Args::parse();
36
+
37
+
// Create HTTP client
38
+
let mut client = AuthenticatedClient::new(reqwest::Client::new(), args.pds);
39
+
40
+
// Create session
41
+
let session = Session::from(
42
+
client
43
+
.send(
44
+
CreateSession::new()
45
+
.identifier(args.username)
46
+
.password(args.password)
47
+
.build(),
48
+
)
49
+
.await?
50
+
.into_output()?,
51
+
);
52
+
53
+
println!("logged in as {} ({})", session.handle, session.did);
54
+
client.set_session(session);
55
+
56
+
// Fetch timeline
57
+
println!("\nfetching timeline...");
58
+
let timeline = client
59
+
.send(GetTimeline::new().limit(5).build())
60
+
.await?
61
+
.into_output()?;
62
+
63
+
println!("\ntimeline ({} posts):", timeline.feed.len());
64
+
for (i, post) in timeline.feed.iter().enumerate() {
65
+
println!("\n{}. by {}", i + 1, post.post.author.handle);
66
+
println!(
67
+
" {}",
68
+
serde_json::to_string_pretty(&post.post.record).into_diagnostic()?
69
+
);
70
+
}
71
+
72
+
Ok(())
73
+
}
74
+
```
75
+
5
76
## Goals
6
77
7
78
- Validated, spec-compliant, easy to work with, and performant baseline types (including typed at:// uris)
···
29
100
```
30
101
31
102
There's also a [`justfile`](https://just.systems/) for Makefile-esque commands to be run inside of the devShell, and you can generally `cargo ...` or `just ...` whatever just fine if you don't want to use Nix and have the prerequisites installed.
32
-
33
-
34
-
35
-
### String types
36
-
Something of a note to self. Developing a pattern with the string types (may macro-ify at some point). Each needs:
37
-
- new(): constructing from a string slice with the right lifetime that borrows
38
-
- new_owned(): constructing from an impl AsRef<str>, taking ownership
39
-
- new_static(): construction from a &'static str, using SmolStr's/CowStr's new_static() constructor to not allocate
40
-
- raw(): same as new() but panics instead of erroring
41
-
- unchecked(): same as new() but doesn't validate. marked unsafe.
42
-
- as_str(): does what it says on the tin
43
-
#### Traits:
44
-
- Serialize + Deserialize (custom impl for latter, sometimes for former)
45
-
- FromStr
46
-
- Display
47
-
- Debug, PartialEq, Eq, Hash, Clone
48
-
- From<T> for String, CowStr, SmolStr,
49
-
- From<String>, From<CowStr>, From<SmolStr>, or TryFrom if likely enough to fail in practice to make panics common
50
-
- AsRef<str>
51
-
- Deref with Target = str (usually)
52
-
53
-
Use `#[repr(transparent)]` as much as possible. Main exception is at-uri type and components.
54
-
Use SmolStr directly as the inner type if most or all of the instances will be under 24 bytes, save lifetime headaches.
55
-
Use CowStr for longer to allow for borrowing from input.
56
-
57
-
TODO: impl IntoStatic trait to take ownership of string types
+8
-8
crates/jacquard-api/Cargo.toml
+8
-8
crates/jacquard-api/Cargo.toml
···
1
1
[package]
2
2
name = "jacquard-api"
3
+
description = "Generated AT Protocol API bindings for Jacquard"
3
4
edition.workspace = true
4
5
version.workspace = true
5
6
authors.workspace = true
···
7
8
keywords.workspace = true
8
9
categories.workspace = true
9
10
readme.workspace = true
10
-
documentation.workspace = true
11
+
documentation = "https://docs.rs/jacquard-api"
11
12
exclude.workspace = true
12
-
description.workspace = true
13
13
14
14
[features]
15
15
default = [ "com_atproto"]
···
20
20
21
21
[dependencies]
22
22
bon = "3"
23
-
bytes = { version = "1.10.1", features = ["serde"] }
24
-
jacquard-common = { version = "0.1.0", path = "../jacquard-common" }
25
-
jacquard-derive = { version = "0.1.0", path = "../jacquard-derive" }
26
-
miette = "7.6.0"
27
-
serde = { version = "1.0.228", features = ["derive"] }
28
-
thiserror = "2.0.17"
23
+
bytes = { workspace = true, features = ["serde"] }
24
+
jacquard-common = { path = "../jacquard-common" }
25
+
jacquard-derive = { path = "../jacquard-derive" }
26
+
miette.workspace = true
27
+
serde.workspace = true
28
+
thiserror.workspace = true
+11
-11
crates/jacquard-common/Cargo.toml
+11
-11
crates/jacquard-common/Cargo.toml
···
1
1
[package]
2
2
name = "jacquard-common"
3
+
description = "Core AT Protocol types and utilities for Jacquard"
3
4
edition.workspace = true
4
5
version.workspace = true
5
6
authors.workspace = true
···
7
8
keywords.workspace = true
8
9
categories.workspace = true
9
10
readme.workspace = true
10
-
documentation.workspace = true
11
+
documentation = "https://docs.rs/jacquard-common"
11
12
exclude.workspace = true
12
-
description.workspace = true
13
13
14
14
15
15
16
16
[dependencies]
17
17
base64 = "0.22.1"
18
-
bytes = "1.10.1"
18
+
bytes.workspace = true
19
19
chrono = "0.4.42"
20
20
cid = { version = "0.11.1", features = ["serde", "std"] }
21
21
enum_dispatch = "0.3.13"
22
22
ipld-core = { version = "0.4.2", features = ["serde"] }
23
23
langtag = { version = "0.4.0", features = ["serde"] }
24
-
miette = "7.6.0"
24
+
miette.workspace = true
25
25
multibase = "0.9.1"
26
26
multihash = "0.19.3"
27
27
num-traits = "0.2.19"
28
28
ouroboros = "0.18.5"
29
29
rand = "0.9.2"
30
30
regex = "1.11.3"
31
-
serde = { version = "1.0.227", features = ["derive"] }
32
-
serde_html_form = "0.2.8"
33
-
serde_json = "1.0.145"
34
-
serde_with = "3.14.1"
35
-
smol_str = { version = "0.3.2", features = ["serde"] }
36
-
thiserror = "2.0.16"
37
-
url = "2.5.7"
31
+
serde.workspace = true
32
+
serde_html_form.workspace = true
33
+
serde_json.workspace = true
34
+
serde_with.workspace = true
35
+
smol_str.workspace = true
36
+
thiserror.workspace = true
37
+
url.workspace = true
+13
-13
crates/jacquard-derive/Cargo.toml
+13
-13
crates/jacquard-derive/Cargo.toml
···
1
1
[package]
2
2
name = "jacquard-derive"
3
+
description = "Procedural macros for Jacquard lexicon types"
3
4
edition.workspace = true
4
5
version.workspace = true
5
6
authors.workspace = true
···
7
8
keywords.workspace = true
8
9
categories.workspace = true
9
10
readme.workspace = true
10
-
documentation.workspace = true
11
+
documentation = "https://docs.rs/jacquard-derive"
11
12
exclude.workspace = true
12
-
description.workspace = true
13
13
14
14
[lib]
15
15
proc-macro = true
16
16
17
17
[dependencies]
18
-
heck = "0.5.0"
19
-
itertools = "0.14.0"
20
-
prettyplease = "0.2.37"
21
-
proc-macro2 = "1.0.101"
22
-
quote = "1.0.41"
23
-
serde = { version = "1.0.228", features = ["derive"] }
24
-
serde_json = "1.0.145"
25
-
serde_repr = "0.1.20"
26
-
serde_with = "3.14.1"
27
-
syn = "2.0.106"
18
+
heck.workspace = true
19
+
itertools.workspace = true
20
+
prettyplease.workspace = true
21
+
proc-macro2.workspace = true
22
+
quote.workspace = true
23
+
serde.workspace = true
24
+
serde_json.workspace = true
25
+
serde_repr.workspace = true
26
+
serde_with.workspace = true
27
+
syn.workspace = true
28
28
29
29
30
30
[dev-dependencies]
31
-
jacquard-common = { version = "0.1.0", path = "../jacquard-common" }
31
+
jacquard-common = { path = "../jacquard-common" }
+15
-15
crates/jacquard-lexicon/Cargo.toml
+15
-15
crates/jacquard-lexicon/Cargo.toml
···
1
1
[package]
2
2
name = "jacquard-lexicon"
3
+
description = "Lexicon schema parsing and code generation for Jacquard"
3
4
edition.workspace = true
4
5
version.workspace = true
5
6
authors.workspace = true
···
9
10
readme.workspace = true
10
11
documentation.workspace = true
11
12
exclude.workspace = true
12
-
description.workspace = true
13
13
14
14
[[bin]]
15
15
name = "jacquard-codegen"
16
16
path = "src/bin/codegen.rs"
17
17
18
18
[dependencies]
19
-
clap = { workspace = true }
20
-
heck = "0.5.0"
21
-
itertools = "0.14.0"
22
-
jacquard-common = { version = "0.1.0", path = "../jacquard-common" }
23
-
miette = { version = "7.6.0", features = ["fancy"] }
24
-
prettyplease = "0.2.37"
25
-
proc-macro2 = "1.0.101"
26
-
quote = "1.0.41"
27
-
serde = { version = "1.0.228", features = ["derive"] }
28
-
serde_json = "1.0.145"
29
-
serde_repr = "0.1.20"
30
-
serde_with = "3.14.1"
31
-
syn = "2.0.106"
32
-
thiserror = "2.0.17"
19
+
clap.workspace = true
20
+
heck.workspace = true
21
+
itertools.workspace = true
22
+
jacquard-common = { path = "../jacquard-common" }
23
+
miette = { workspace = true, features = ["fancy"] }
24
+
prettyplease.workspace = true
25
+
proc-macro2.workspace = true
26
+
quote.workspace = true
27
+
serde.workspace = true
28
+
serde_json.workspace = true
29
+
serde_repr.workspace = true
30
+
serde_with.workspace = true
31
+
syn.workspace = true
32
+
thiserror.workspace = true
+12
-12
crates/jacquard/Cargo.toml
+12
-12
crates/jacquard/Cargo.toml
···
1
1
[package]
2
2
name = "jacquard"
3
-
description = "Simple and powerful AT Procotol implementation"
3
+
description.workspace = true
4
4
edition.workspace = true
5
5
version.workspace = true
6
6
authors.workspace = true
···
26
26
path = "src/main.rs"
27
27
28
28
[dependencies]
29
-
bytes = "1.10"
30
-
clap = { workspace = true }
31
-
http = "1.3.1"
32
-
jacquard-api = { version = "0.1.0", path = "../jacquard-api" }
29
+
bytes.workspace = true
30
+
clap.workspace = true
31
+
http.workspace = true
32
+
jacquard-api = { path = "../jacquard-api" }
33
33
jacquard-common = { path = "../jacquard-common" }
34
34
jacquard-derive = { path = "../jacquard-derive", optional = true }
35
-
miette = "7.6.0"
36
-
reqwest = { version = "0.12.23", default-features = false, features = ["charset", "http2", "json", "system-proxy", "gzip", "rustls-tls"] }
37
-
serde = { version = "1.0", features = ["derive"] }
38
-
serde_html_form = "0.2"
39
-
serde_ipld_dagcbor = "0.6.4"
40
-
serde_json = "1.0"
41
-
thiserror = "2.0"
35
+
miette.workspace = true
36
+
reqwest = { workspace = true, features = ["charset", "http2", "json", "system-proxy", "gzip", "rustls-tls"] }
37
+
serde.workspace = true
38
+
serde_html_form.workspace = true
39
+
serde_ipld_dagcbor.workspace = true
40
+
serde_json.workspace = true
41
+
thiserror.workspace = true
42
42
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }
+19
-17
crates/jacquard/src/main.rs
+19
-17
crates/jacquard/src/main.rs
···
1
1
use clap::Parser;
2
+
use jacquard::CowStr;
3
+
use jacquard::api::app_bsky::feed::get_timeline::GetTimeline;
4
+
use jacquard::api::com_atproto::server::create_session::CreateSession;
2
5
use jacquard::client::{AuthenticatedClient, Session, XrpcClient};
3
-
use jacquard_api::app_bsky::feed::get_timeline::GetTimeline;
4
-
use jacquard_api::com_atproto::server::create_session::CreateSession;
5
-
use jacquard_common::CowStr;
6
6
use miette::IntoDiagnostic;
7
7
8
8
#[derive(Parser, Debug)]
···
20
20
#[arg(short, long)]
21
21
password: CowStr<'static>,
22
22
}
23
-
24
23
#[tokio::main]
25
24
async fn main() -> miette::Result<()> {
26
25
let args = Args::parse();
27
26
28
27
// Create HTTP client
29
-
let http = reqwest::Client::new();
30
-
let mut client = AuthenticatedClient::new(http, args.pds);
28
+
let mut client = AuthenticatedClient::new(reqwest::Client::new(), args.pds);
31
29
32
30
// Create session
33
-
println!("logging in as {}...", args.username);
34
-
let create_session = CreateSession::new()
35
-
.identifier(args.username)
36
-
.password(args.password)
37
-
.build();
38
-
39
-
let session_output = client.send(create_session).await?.into_output()?;
40
-
let session = Session::from(session_output);
31
+
let session = Session::from(
32
+
client
33
+
.send(
34
+
CreateSession::new()
35
+
.identifier(args.username)
36
+
.password(args.password)
37
+
.build(),
38
+
)
39
+
.await?
40
+
.into_output()?,
41
+
);
41
42
42
43
println!("logged in as {} ({})", session.handle, session.did);
43
44
client.set_session(session);
44
45
45
46
// Fetch timeline
46
47
println!("\nfetching timeline...");
47
-
let timeline_req = GetTimeline::new().limit(5).build();
48
-
49
-
let timeline = client.send(timeline_req).await?.into_output()?;
48
+
let timeline = client
49
+
.send(GetTimeline::new().limit(5).build())
50
+
.await?
51
+
.into_output()?;
50
52
51
53
println!("\ntimeline ({} posts):", timeline.feed.len());
52
54
for (i, post) in timeline.feed.iter().enumerate() {