-
Notifications
You must be signed in to change notification settings - Fork 30
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* 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
Showing
10 changed files
with
891 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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"] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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). |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(()) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
Oops, something went wrong.