Skip to content

Commit

Permalink
feat: Add xrpc-client package (#63)
Browse files Browse the repository at this point in the history
* Add xrpc-client implementation

* Add README, doc tests and comments

* Add workflows

* Fix root Cargo.toml

* Oops...

* Add tests

* Fix tests

* Add tests

* Use atrium-xrpc 0.5, rename methods

* Add docs

* Ready to publish
  • Loading branch information
sugyan authored Nov 10, 2023
1 parent 8d96231 commit d1750f7
Show file tree
Hide file tree
Showing 10 changed files with 891 additions and 0 deletions.
28 changes: 28 additions & 0 deletions .github/workflows/xrpc-client.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
name: XRPC Client

on:
push:
branches: ["main"]
pull_request:
branches: ["main"]

env:
CARGO_TERM_COLOR: always

jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Build
run: cargo build -p atrium-xrpc-client --verbose
- name: Run tests
run: |
cargo test -p atrium-xrpc-client --lib
cargo test -p atrium-xrpc-client --lib --no-default-features --features=reqwest-native
cargo test -p atrium-xrpc-client --lib --no-default-features --features=reqwest-rustls
cargo test -p atrium-xrpc-client --lib --no-default-features --features=isahc
cargo test -p atrium-xrpc-client --lib --no-default-features --features=surf
cargo test -p atrium-xrpc-client --lib --all-features
- name: Run doctests
run: cargo test -p atrium-xrpc-client --doc --all-features
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ members = [
"atrium-codegen",
"atrium-lex",
"atrium-xrpc",
"atrium-xrpc-client",
"atrium-xrpc-server",
"examples/concurrent",
"examples/firehose",
Expand Down
49 changes: 49 additions & 0 deletions atrium-xrpc-client/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
[package]
name = "atrium-xrpc-client"
version = "0.1.0"
authors = ["sugyan <[email protected]>"]
edition = "2021"
description = "XRPC Client library for AT Protocol (Bluesky)"
documentation = "https://docs.rs/atrium-xrpc-client"
readme = "README.md"
repository = "https://github.com/sugyan/atrium"
license = "MIT"
keywords = ["atproto", "bluesky"]

[dependencies]
async-trait = "0.1.74"
atrium-xrpc = "0.5.0"
http = "0.2.9"

[dependencies.isahc]
version = "1.7.2"
optional = true

[dependencies.reqwest]
version = "0.11.22"
default-features = false
optional = true

[dependencies.surf]
version = "2.3.2"
default-features = false
optional = true

[features]
default = ["reqwest-native"]
isahc = ["dep:isahc"]
reqwest-native = ["reqwest/native-tls"]
reqwest-rustls = ["reqwest/rustls-tls"]
surf = ["dep:surf"]

[dev-dependencies]
surf = { version = "2.3.2", default-features = false, features = ["h1-client-rustls"] }
http-client = { version = "6.5.3", default-features = false, features = ["h1_client", "rustls"] }
mockito = "1.2.0"
tokio = { version = "1.33.0", features = ["macros"] }
serde = { version = "1.0.192", features = ["derive"] }
futures = { version = "0.3.29", default-features = false }

[package.metadata.docs.rs]
all-features = true
rustdoc-args = ["--cfg", "docsrs"]
157 changes: 157 additions & 0 deletions atrium-xrpc-client/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
# ATrium XRPC Client

This library provides clients that implement the [`XrpcClient`](https://docs.rs/atrium-xrpc/latest/atrium_xrpc/trait.XrpcClient.html) defined in [`atrium-xrpc`](../atrium-xrpc/). To accommodate a wide range of use cases, four feature flags are provided to allow developers to choose the best asynchronous HTTP client library for their project as a backend.

## Features

- `reqwest-native` (default)
- `reqwest-rustls`
- `isahc`
- `surf`

Usage examples are provided below.

### `reqwest-native` and `reqwest-rustls`

If you are using [`tokio`](https://crates.io/crates/tokio) as your asynchronous runtime, you may find it convenient to utilize the [`reqwest`](https://crates.io/crates/reqwest) backend with this feature, which is a high-level asynchronous HTTP Client. Within this crate, you have the choice of configuring `reqwest` with either `reqwest/native-tls` or `reqwest/rustls-tls`.

```toml
[dependencies]
atrium-xrpc-client = "*"
```

To use the `reqwest::Client` with the `rustls` TLS backend, specify the feature as follows:

```toml
[dependencies]
atrium-xrpc-client = { version = "*", default-features = false, features = ["reqwest-rustls"]}
```

In either case, you can use the `ReqwestClient`:

```rust
use atrium_xrpc_client::reqwest::ReqwestClient;

fn main() -> Result<(), Box<dyn std::error::Error>> {
let client = ReqwestClient::new("https://bsky.social");
Ok(())
}
```

You can also directly specify a `reqwest::Client` with your own configuration:

```toml
[dependencies]
atrium-xrpc-client = { version = "*", default-features = false }
reqwest = { version = "0.11.22", default-features = false, features = ["rustls-tls"] }
```

```rust
use atrium_xrpc_client::reqwest::ReqwestClientBuilder;

fn main() -> Result<(), Box<dyn std::error::Error>> {
let client = ReqwestClientBuilder::new("https://bsky.social")
.client(
reqwest::ClientBuilder::new()
.timeout(std::time::Duration::from_millis(1000))
.use_rustls_tls()
.build()?,
)
.build();
Ok(())
}
```

For more details, refer to the [`reqwest` documentation](https://docs.rs/reqwest).

### `isahc`

The `reqwest` client may not work on asynchronous runtimes other than `tokio`. As an alternative, we offer the feature that uses [`isahc`](https://crates.io/crates/isahc) as the backend.

```toml
[dependencies]
atrium-xrpc-client = { version = "*", default-features = false, features = ["isahc"]}
```

```rust
use atrium_xrpc_client::isahc::IsahcClient;

fn main() -> Result<(), Box<dyn std::error::Error>> {
let client = IsahcClient::new("https://bsky.social");
Ok(())
}
```

Similarly, you can directly specify an isahc::HttpClient with your own settings:

```toml
[dependencies]
atrium-xrpc-client = { version = "*", default-features = false, features = ["isahc"]}
isahc = "1.7.2"
```

```rust
use atrium_xrpc_client::isahc::IsahcClientBuilder;
use isahc::config::Configurable;

fn main() -> Result<(), Box<dyn std::error::Error>> {
let client = IsahcClientBuilder::new("https://bsky.social")
.client(
isahc::HttpClientBuilder::new()
.timeout(std::time::Duration::from_millis(1000))
.build()?,
)
.build();
Ok(())
}
```

For more details, refer to the [`isahc` documentation](https://docs.rs/isahc).


### `surf`

For cases such as using `rustls` with asynchronous runtimes other than `tokio`, we also provide a feature that uses [`surf`](https://crates.io/crates/surf) built with [`async-std`](https://crates.io/crates/async-std) as a backend.

Using `DefaultClient` with `surf` is complicated by the various feature flags. Therefore, unlike the first two options, you must always specify surf::Client when creating a client with this module.

```toml
[dependencies]
atrium-xrpc-client = { version = "*", default-features = false, features = ["surf"]}
surf = { version = "2.3.2", default-features = false, features = ["h1-client-rustls"] }
```

```rust
use atrium_xrpc_client::surf::SurfClient;

fn main() -> Result<(), Box<dyn std::error::Error>> {
let client = SurfClient::new("https://bsky.social", surf::Client::new());
Ok(())
}
```

Using [`http_client`](https://crates.io/crates/http-client) and its bundled implementation may clarify which backend you are using:

```toml
[dependencies]
atrium-xrpc-client = { version = "*", default-features = false, features = ["surf"]}
surf = { version = "2.3.2", default-features = false }
http-client = { version = "6.5.3", default-features = false, features = ["h1_client", "rustls"] }
```

```rust
use atrium_xrpc_client::surf::SurfClient;

fn main() -> Result<(), Box<dyn std::error::Error>> {
let client = SurfClient::new(
"https://bsky.social",
surf::Client::with_http_client(http_client::h1::H1Client::try_from(
http_client::Config::default()
.set_timeout(Some(std::time::Duration::from_millis(1000))),
)?),
);
Ok(())
}
```

For more details, refer to the [`surf` documentation](https://docs.rs/surf).
116 changes: 116 additions & 0 deletions atrium-xrpc-client/src/isahc.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
#![doc = "XrpcClient implementation for [isahc]"]
use async_trait::async_trait;
use atrium_xrpc::{HttpClient, XrpcClient};
use http::{Request, Response};
use isahc::{AsyncReadResponseExt, HttpClient as Client};
use std::sync::Arc;

/// A [`isahc`] based asynchronous client to make XRPC requests with.
///
/// To change the [`isahc::HttpClient`] used internally to a custom configured one,
/// use the [`IsahcClientBuilder`].
///
/// You do **not** have to wrap the `Client` in an [`Rc`] or [`Arc`] to **reuse** it,
/// because it already uses an [`Arc`] internally.
///
/// [`Rc`]: std::rc::Rc
pub struct IsahcClient {
base_uri: String,
client: Arc<Client>,
}

impl IsahcClient {
/// Create a new [`IsahcClient`] using the default configuration.
pub fn new(base_uri: impl AsRef<str>) -> Self {
IsahcClientBuilder::new(base_uri).build()
}
}

/// A client builder, capable of creating custom [`IsahcClient`] instances.
pub struct IsahcClientBuilder {
base_uri: String,
client: Option<Client>,
}

impl IsahcClientBuilder {
/// Create a new [`IsahcClientBuilder`] for building a custom client.
pub fn new(base_uri: impl AsRef<str>) -> Self {
Self {
base_uri: base_uri.as_ref().into(),
client: None,
}
}
/// Sets the [`isahc::HttpClient`] to use.
pub fn client(mut self, client: Client) -> Self {
self.client = Some(client);
self
}
/// Build an [`IsahcClient`] using the configured options.
pub fn build(self) -> IsahcClient {
IsahcClient {
base_uri: self.base_uri,
client: Arc::new(
self.client
.unwrap_or(Client::new().expect("failed to create isahc client")),
),
}
}
}

#[async_trait]
impl HttpClient for IsahcClient {
async fn send_http(
&self,
request: Request<Vec<u8>>,
) -> Result<Response<Vec<u8>>, Box<dyn std::error::Error + Send + Sync + 'static>> {
let mut response = self.client.send_async(request).await?;
let mut builder = Response::builder().status(response.status());
for (k, v) in response.headers() {
builder = builder.header(k, v);
}
builder
.body(response.bytes().await?.to_vec())
.map_err(Into::into)
}
}

impl XrpcClient for IsahcClient {
fn base_uri(&self) -> &str {
&self.base_uri
}
}

#[cfg(test)]
mod tests {
use super::*;
use isahc::config::Configurable;
use std::time::Duration;

#[test]
fn new() -> Result<(), Box<dyn std::error::Error>> {
let client = IsahcClient::new("http://localhost:8080");
assert_eq!(client.base_uri(), "http://localhost:8080");
Ok(())
}

#[test]
fn builder_without_client() -> Result<(), Box<dyn std::error::Error>> {
let client = IsahcClientBuilder::new("http://localhost:8080").build();
assert_eq!(client.base_uri(), "http://localhost:8080");
Ok(())
}

#[test]
fn builder_with_client() -> Result<(), Box<dyn std::error::Error>> {
let client = IsahcClientBuilder::new("http://localhost:8080")
.client(
Client::builder()
.default_header(http::header::USER_AGENT, "USER_AGENT")
.timeout(Duration::from_millis(500))
.build()?,
)
.build();
assert_eq!(client.base_uri(), "http://localhost:8080");
Ok(())
}
}
18 changes: 18 additions & 0 deletions atrium-xrpc-client/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
#![cfg_attr(docsrs, feature(doc_cfg))]
#![doc = include_str!("../README.md")]

#[cfg_attr(docsrs, doc(cfg(feature = "isahc")))]
#[cfg(feature = "isahc")]
pub mod isahc;
#[cfg_attr(
docsrs,
doc(cfg(any(feature = "reqwest-native", feature = "reqwest-rustls")))
)]
#[cfg(any(feature = "reqwest-native", feature = "reqwest-rustls"))]
pub mod reqwest;
#[cfg_attr(docsrs, doc(cfg(feature = "surf")))]
#[cfg(feature = "surf")]
pub mod surf;

#[cfg(test)]
mod tests;
Loading

0 comments on commit d1750f7

Please sign in to comment.