Skip to content

Commit 5382f1f

Browse files
Merge pull request #58 from CleverCloud/clg/refactor
refactor
2 parents 69e2f8d + 7268c6f commit 5382f1f

File tree

13 files changed

+1265
-837
lines changed

13 files changed

+1265
-837
lines changed

Cargo.toml

Lines changed: 38 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -4,20 +4,44 @@ description = "A rust implementation of the oauth 1.0a protocol fully-async with
44
version = "2.1.2"
55
edition = "2024"
66
rust-version = "1.85.0"
7-
authors = ["Florentin Dubois <[email protected]>"]
7+
authors = [
8+
"Florentin Dubois <[email protected]>",
9+
"Cédric Lemaire-Giroud <[email protected]>",
10+
]
811
license-file = "LICENSE"
912
readme = "README.md"
1013
repository = "https://github.com/CleverCloud/oauth10a-rust"
1114
keywords = ["clevercloud", "client", "logging", "metrics", "oauth1a"]
1215

1316
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
1417

18+
[features]
19+
default = ["logging", "client"]
20+
tracing = ["dep:tracing"]
21+
logging = ["log", "tracing/log-always"]
22+
metrics = ["dep:prometheus"]
23+
execute = []
24+
client = ["execute"]
25+
rest = ["execute", "dep:serde", "dep:serde_json"]
26+
sse = [
27+
"execute",
28+
"dep:serde",
29+
"dep:serde_json",
30+
"dep:bytes",
31+
"dep:futures",
32+
"dep:mime",
33+
"dep:memchr",
34+
"dep:tokio",
35+
]
36+
serde = ["dep:serde"]
37+
zeroize = ["dep:zeroize"]
38+
1539
[dependencies]
16-
base64 = { version = "^0.22.1", optional = true }
17-
bytes = { version = "^1.10.1", features = ["serde"], optional = true }
18-
crypto-common = { version = "^0.1.6", optional = true }
40+
base64 = { version = "^0.22.1" }
41+
bytes = { version = "^1.10.1", optional = true }
42+
crypto-common = { version = "^0.1.6" }
1943
futures = { version = "^0.3.31", optional = true }
20-
hmac = { version = "^0.12.1", features = ["std"], optional = true }
44+
hmac = { version = "^0.12.1" }
2145
log = { version = "^0.4.27", optional = true }
2246
memchr = { version = "^2.7.4", optional = true }
2347
mime = { version = "^0.3.17", optional = true }
@@ -32,46 +56,24 @@ reqwest = { version = "^0.12.15", default-features = true, features = [
3256
"json",
3357
"hickory-dns",
3458
"stream",
35-
], optional = true }
59+
] }
3660
serde = { version = "^1.0.219", features = ["derive"], optional = true }
3761
serde_json = { version = "^1.0.140", features = [
3862
"preserve_order",
3963
"float_roundtrip",
4064
], optional = true }
41-
sha2 = { version = "^0.10.9", optional = true }
42-
thiserror = { version = "^2.0.12", optional = true }
43-
tokio = { version = "^1.44.2", optional = true, default-features = false, features = [
65+
sha2 = { version = "^0.10.9" }
66+
thiserror = { version = "^2.0.12" }
67+
tokio = { version = "^1.45.1", optional = true, default-features = false, features = [
4468
"time",
4569
] }
4670
tracing = { version = "^0.1.41", optional = true }
47-
url = { version = "^2.5.4", default-features = false, features = [
48-
"serde",
49-
], optional = true }
50-
urlencoding = { version = "^2.1.3", optional = true }
51-
uuid = { version = "^1.16.0", features = ["serde", "v4"], optional = true }
71+
url = { version = "2.5.4", features = ["serde"] }
72+
urlencoding = { version = "^2.1.3" }
73+
uuid = { version = "^1.17.0", features = ["serde", "v4"] }
74+
zeroize = { version = "^1.8.1", features = ["derive"], optional = true }
5275

5376
[dev-dependencies]
5477
anyhow = { version = "^1.0.98" }
5578
axum = { version = "^0.8.4" }
56-
tokio = { version = "^1.44.2", features = ["full"] }
57-
58-
[features]
59-
default = ["client", "logging"]
60-
client = [
61-
"base64",
62-
"bytes",
63-
"crypto-common",
64-
"hmac",
65-
"reqwest",
66-
"serde",
67-
"serde_json",
68-
"sha2",
69-
"thiserror",
70-
"url",
71-
"urlencoding",
72-
"uuid",
73-
]
74-
logging = ["log", "tracing/log-always"]
75-
tracing = ["dep:tracing"]
76-
metrics = ["prometheus"]
77-
sse = ["dep:futures", "dep:mime", "dep:memchr", "dep:tokio"]
79+
tokio = { version = "^1.45.1", features = ["full"] }

README.md

Lines changed: 41 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,59 +1,77 @@
11
# OAuth 1.0a crate
22

3-
> This crate provides an oauth 1.0a client implementation fully-async with
3+
> This crate provides an OAuth 1.0a client implementation fully-async with
44
> logging, metrics and tracing facilities. It was firstly designed to interact
5-
> with the Clever-Cloud's api, but has been extended to be more generic.
5+
> with the Clever-Cloud's API, but has been extended to be more generic.
66
77
## Status
88

9-
This crate is ready for production, if you may have bugs, so please an issue to
10-
fix the trouble.
9+
This crate is ready for production. If you find a bug, please open an issue.
1110

1211
## Installation
1312

1413
To install this dependency, just add the following line to your `Cargo.toml` manifest.
1514

1615
```toml
17-
oauth10a = { version = "^1.5.1", features = ["metrics"] }
16+
oauth10a = { version = "^2.1.2", features = ["metrics"] }
1817
```
1918

2019
## Usage
2120

22-
Below, you will find an example of executing a simple request to an api.
21+
Below, you will find an example of executing a simple request to an API.
2322

2423
```rust
25-
use std::error::Error;
24+
use oauth10a::{client::Client, credentials::Credentials, rest::RestClient};
2625

27-
use oauth10a::client::{Client, Credentials, RestClient};
26+
type MyData = std::collections::BTreeMap<String, String>;
27+
type MyError = String;
2828

2929
#[tokio::main]
30-
async fn main() -> Result<(), Box<dyn Error + Send + Sync>> {
31-
let client = Client::from(Credentials::oauth1("".to_string(), "".to_string(), "".to_string(), "".to_string()));
32-
let _obj: BtreeMap<String, String> = client.get("https://example.com/object.json").await?;
30+
async fn main() -> Result<(), Box<dyn core::error::Error + Send + Sync>> {
31+
let client = Client::new().with_credentials(Credentials::OAuth1 {
32+
token: "",
33+
secret: "",
34+
consumer_key: "",
35+
consumer_secret: "",
36+
});
37+
38+
match client.get::<MyData, MyError>("https://example.com/object.json").await {
39+
// received HTTP response with JSON payload deserializing to `MyData`
40+
Ok(Ok(response)) => (),
41+
// received HTTP error response with JSON payload deserializing to `MyError`
42+
Ok(Err(error_response)) => (),
43+
// client failed to execute request
44+
Err(rest_error) => ()
45+
}
3346

3447
Ok(())
3548
}
3649
```
3750

3851
## Features
3952

40-
| name | description |
41-
|---------|---------------------------------------------------------------|
42-
| default | Default enable features are `client` and `logging` |
43-
| client | The oauth 1.0a client implementation |
44-
| logging | Use the `log` facility crate to print logs |
45-
| metrics | Use `prometheus` crates to register metrics |
46-
| tracing | Use `tracing` crate to add `tracing::instrument` on functions |
47-
| sse | Enables streaming Server-Sent Events (SSE) |
53+
| name | description |
54+
| ------- | ----------------------------------------------------------------------------------------- |
55+
| default | Default enable features are `client` and `logging` |
56+
| execute | Provides the `ExecuteRequest` trait |
57+
| client | Provides an HTTP client that implements `ExecuteRequest` handling request's authorization |
58+
| logging | Use the `log` facility crate to print logs |
59+
| metrics | Use `prometheus` crate to register metrics |
60+
| tracing | Use `tracing` crate to add `tracing::instrument` on functions |
61+
| rest | Enables RESTful API helper methods |
62+
| sse | Enables streaming Server-Sent Events (SSE) |
63+
| serde | Provides `serde` implementation for `Credentials` |
64+
| zeroize | Provides `zeroize::Zeroize` implementations on `Credentials` |
4865

4966
### Metrics
5067

5168
Below, the exposed metrics gathered by prometheus:
5269

53-
| name | labels | kind | description |
54-
| -------------------------------- | --------------------------------------------------------------- | ------- | -------------------------- |
55-
| oauth10a_client_request | endpoint: String, method: String, status: Integer | Counter | number of request on api |
56-
| oauth10a_client_request_duration | endpoint: String, method: String, status: Integer, unit: String | Counter | duration of request on api |
70+
| name | labels | kind | description |
71+
| -------------------------------- | --------------------------------------------------------------- | ------- | ---------------------------------- |
72+
| oauth10a_client_request | endpoint: String, method: String, status: Integer | Counter | number of request on API |
73+
| oauth10a_client_request_duration | endpoint: String, method: String, status: Integer, unit: String | Counter | duration of request on API |
74+
| oauth10a_client_sse | endpoint: String | Counter | number of events received from API |
5775

5876
## License
5977

src/client.rs

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
//! HTTP client
2+
3+
use core::fmt;
4+
5+
use reqwest::{Request, Response};
6+
7+
use crate::{
8+
credentials::{AuthorizationError, Credentials},
9+
execute::ExecuteRequest,
10+
};
11+
12+
// CLIENT ERROR ////////////////////////////////////////////////////////////////
13+
14+
#[derive(Debug, thiserror::Error)]
15+
pub enum ClientError<E = reqwest::Error> {
16+
#[error("failed to authorize request, {0}")]
17+
Authorize(#[from] AuthorizationError),
18+
#[error("failed to execute request, {0}")]
19+
Execute(E),
20+
}
21+
22+
// CLIENT //////////////////////////////////////////////////////////////////////
23+
24+
/// HTTP client with optional [`Credentials`].
25+
///
26+
/// When credentials are provided, the client, will ensure requests are authorized
27+
/// before they are executed.
28+
#[derive(Debug, Default, Clone)]
29+
#[must_use]
30+
pub struct Client<T = reqwest::Client> {
31+
inner: T,
32+
credentials: Option<Credentials>,
33+
}
34+
35+
impl<T> From<T> for Client<T> {
36+
fn from(value: T) -> Self {
37+
Self {
38+
inner: value,
39+
credentials: None,
40+
}
41+
}
42+
}
43+
44+
impl From<&Credentials> for Client {
45+
fn from(value: &Credentials) -> Self {
46+
Self::from(value.clone())
47+
}
48+
}
49+
50+
impl<T: Into<Box<str>>> From<Credentials<T>> for Client {
51+
fn from(value: Credentials<T>) -> Self {
52+
Self {
53+
inner: reqwest::Client::new(),
54+
credentials: Some(value.into()),
55+
}
56+
}
57+
}
58+
59+
impl Client {
60+
pub fn new() -> Self {
61+
Self::default()
62+
}
63+
}
64+
65+
impl<T: fmt::Debug> Client<T> {
66+
/// Sets the `credentials` to be used by the client to authorize HTTP request,
67+
/// discarding the current value, if any.
68+
#[cfg_attr(feature = "tracing", tracing::instrument)]
69+
pub fn set_credentials(&mut self, credentials: Option<Credentials>) {
70+
self.credentials = credentials;
71+
}
72+
73+
#[cfg_attr(feature = "tracing", tracing::instrument)]
74+
fn set_credentials_from<U: Into<Box<str>> + fmt::Debug>(
75+
&mut self,
76+
credentials: Option<Credentials<U>>,
77+
) {
78+
self.credentials = credentials.map(Credentials::into);
79+
}
80+
81+
/// Fills the `credentials` to be used by the client to authorize HTTP request,
82+
/// discarding the current value, if any.
83+
pub fn with_credentials<U: Into<Box<str>> + fmt::Debug>(
84+
mut self,
85+
credentials: impl Into<Option<Credentials<U>>>,
86+
) -> Self {
87+
self.set_credentials_from(credentials.into());
88+
self
89+
}
90+
91+
/// Returns the credentials that will be used by this client to authorized
92+
/// subsequent HTTP requests.
93+
#[cfg_attr(feature = "tracing", tracing::instrument)]
94+
pub fn credentials(&self) -> Option<Credentials<&str>> {
95+
self.credentials.as_ref().map(Credentials::as_ref)
96+
}
97+
98+
/// Returns a shared reference to the inner HTTP client.
99+
#[cfg_attr(feature = "tracing", tracing::instrument)]
100+
pub fn inner(&self) -> &T {
101+
&self.inner
102+
}
103+
104+
/// Appends an `Authorization` header to the `request`, if this client has credentials and unless it is already set.
105+
///
106+
/// Returns `true` if the `Authorization` header was inserted.
107+
///
108+
/// # Errors
109+
///
110+
/// Upon failure to produce the header value.
111+
///
112+
/// If the client doesn't have credentials, this method is infallible.
113+
#[cfg_attr(feature = "tracing", tracing::instrument)]
114+
pub fn authorize(&self, request: &mut Request) -> Result<bool, AuthorizationError> {
115+
match self.credentials() {
116+
None => Ok(false),
117+
Some(credentials) => credentials.authorize(request),
118+
}
119+
}
120+
}
121+
122+
impl<T: ExecuteRequest> ExecuteRequest for Client<T> {
123+
type Error = ClientError<T::Error>;
124+
125+
fn execute_request(
126+
&self,
127+
mut request: Request,
128+
) -> impl Future<Output = Result<Response, Self::Error>> + Send + 'static {
129+
let result = self
130+
.authorize(&mut request)
131+
.map(|_| self.inner.execute_request(request));
132+
133+
async move { result?.await.map_err(ClientError::Execute) }
134+
}
135+
}
136+
137+
#[cfg(feature = "zeroize")]
138+
impl<T> Drop for Client<T> {
139+
fn drop(&mut self) {
140+
use zeroize::Zeroize;
141+
142+
if let Some(mut credentials) = self.credentials.take() {
143+
credentials.zeroize();
144+
}
145+
}
146+
}

0 commit comments

Comments
 (0)