Skip to content

Commit b21cedc

Browse files
committed
feat(api-client-framework): add blockscout services related helper definitions
1 parent c55b2bb commit b21cedc

File tree

5 files changed

+221
-0
lines changed

5 files changed

+221
-0
lines changed

libs/api-client-framework/Cargo.toml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,12 @@ serde_path_to_error = { version = "0.1.16", default-features = false }
1414
serde_urlencoded = { version = "0.7", default-features = false }
1515
thiserror = { version = "2", default-features = false }
1616
url = { version = "2", default-features = false }
17+
18+
reqwest-retry = { version = "0.7.0", default-features = false, optional = true }
19+
20+
[features]
21+
"blockscout" = [
22+
"dep:reqwest-retry",
23+
"serde/derive",
24+
"url/serde"
25+
]
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
use super::config;
2+
use crate::{Endpoint, Error, HttpApiClient, HttpApiClientConfig};
3+
use reqwest::header::HeaderValue;
4+
5+
pub struct Client {
6+
http_client: HttpApiClient,
7+
api_key: Option<HeaderValue>,
8+
}
9+
10+
impl Client {
11+
pub async fn new(config: config::Config) -> Self {
12+
let http_client_config = HttpApiClientConfig {
13+
http_timeout: config.http_timeout,
14+
default_headers: Default::default(),
15+
middlewares: config.middlewares,
16+
};
17+
18+
let http_client = HttpApiClient::new(config.url, http_client_config)
19+
.unwrap_or_else(|err| panic!("cannot build an http client: {err:#?}"));
20+
21+
let client = Self {
22+
http_client,
23+
api_key: config.api_key,
24+
};
25+
26+
if config.probe_url {
27+
let endpoint = health::HealthCheck::new(Default::default());
28+
if let Err(err) = client.request(&endpoint).await {
29+
panic!("Cannot establish a connection with contracts-info client: {err}")
30+
}
31+
}
32+
33+
client
34+
}
35+
36+
pub async fn request<EndpointType: Endpoint>(
37+
&self,
38+
endpoint: &EndpointType,
39+
) -> Result<<EndpointType as Endpoint>::Response, Error> {
40+
self.http_client.request(endpoint).await
41+
}
42+
43+
pub fn api_key(&self) -> Option<&HeaderValue> {
44+
self.api_key.as_ref()
45+
}
46+
}
47+
48+
/// As we don't have protobuf generated structures here (they are only available inside a service),
49+
/// we have to imitate the service health endpoint.
50+
mod health {
51+
use crate::{serialize_query, Endpoint};
52+
use reqwest::Method;
53+
use serde::{Deserialize, Serialize};
54+
55+
#[derive(Debug, Default, Serialize)]
56+
pub struct HealthCheckRequest {
57+
pub service: String,
58+
}
59+
60+
#[derive(Debug, Deserialize)]
61+
pub struct HealthCheckResponse {
62+
#[serde(rename = "status")]
63+
pub _status: i32,
64+
}
65+
66+
pub struct HealthCheck {
67+
request: HealthCheckRequest,
68+
}
69+
70+
impl HealthCheck {
71+
pub fn new(request: HealthCheckRequest) -> Self {
72+
Self { request }
73+
}
74+
}
75+
76+
impl Endpoint for HealthCheck {
77+
type Response = HealthCheckResponse;
78+
79+
fn method(&self) -> Method {
80+
Method::GET
81+
}
82+
83+
fn path(&self) -> String {
84+
"/health".to_string()
85+
}
86+
87+
fn query(&self) -> Option<String> {
88+
serialize_query(&self.request)
89+
}
90+
}
91+
}
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
use reqwest::header::HeaderValue;
2+
use reqwest_middleware::Middleware;
3+
use reqwest_retry::{policies::ExponentialBackoff, RetryTransientMiddleware};
4+
use serde::{Deserialize, Deserializer};
5+
use std::{fmt, fmt::Formatter, sync::Arc, time::Duration};
6+
7+
#[derive(Clone, Deserialize)]
8+
pub struct Config {
9+
pub url: url::Url,
10+
#[serde(default, deserialize_with = "deserialize_api_key")]
11+
pub api_key: Option<HeaderValue>,
12+
/// The maximum time limit for an API request. If a request takes longer than this, it will be
13+
/// cancelled. Defaults to 30 seconds.
14+
#[serde(default = "defaults::http_timeout")]
15+
pub http_timeout: Duration,
16+
#[serde(default)]
17+
pub probe_url: bool,
18+
#[serde(skip_deserializing)]
19+
pub middlewares: Vec<Arc<dyn Middleware>>,
20+
}
21+
22+
fn deserialize_api_key<'de, D>(deserializer: D) -> Result<Option<HeaderValue>, D::Error>
23+
where
24+
D: Deserializer<'de>,
25+
{
26+
let string = Option::<String>::deserialize(deserializer)?;
27+
string
28+
.map(|value| HeaderValue::from_str(&value))
29+
.transpose()
30+
.map_err(<D::Error as serde::de::Error>::custom)
31+
}
32+
33+
// We have to derive `Debug` manually as we need to skip middlewares field which does not implement it.
34+
impl fmt::Debug for Config {
35+
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
36+
#[derive(Debug)]
37+
#[allow(dead_code)]
38+
struct ConfigDebug<'a> {
39+
url: &'a url::Url,
40+
api_key: &'a Option<HeaderValue>,
41+
http_timeout: &'a Duration,
42+
probe_url: &'a bool,
43+
}
44+
let Config {
45+
url,
46+
api_key,
47+
http_timeout,
48+
probe_url,
49+
middlewares: _,
50+
} = self;
51+
fmt::Debug::fmt(
52+
&ConfigDebug {
53+
url,
54+
api_key,
55+
http_timeout,
56+
probe_url,
57+
},
58+
f,
59+
)
60+
}
61+
}
62+
63+
impl Config {
64+
pub fn new(url: url::Url) -> Self {
65+
Self {
66+
url,
67+
api_key: None,
68+
http_timeout: defaults::http_timeout(),
69+
middlewares: vec![],
70+
probe_url: false,
71+
}
72+
}
73+
74+
pub fn with_retry_middleware(self, max_retries: u32) -> Self {
75+
let retry_policy = ExponentialBackoff::builder().build_with_max_retries(max_retries);
76+
let middleware = RetryTransientMiddleware::new_with_policy(retry_policy);
77+
self.with_middleware(middleware)
78+
}
79+
80+
pub fn with_middleware<M: Middleware>(self, middleware: M) -> Self {
81+
self.with_arc_middleware(Arc::new(middleware))
82+
}
83+
84+
pub fn with_arc_middleware<M: Middleware>(mut self, middleware: Arc<M>) -> Self {
85+
self.middlewares.push(middleware);
86+
self
87+
}
88+
89+
pub fn probe_url(mut self, value: bool) -> Self {
90+
self.probe_url = value;
91+
self
92+
}
93+
94+
pub fn api_key(mut self, api_key: Option<HeaderValue>) -> Self {
95+
self.api_key = api_key;
96+
self
97+
}
98+
99+
pub fn http_timeout(mut self, timeout: Duration) -> Self {
100+
self.http_timeout = timeout;
101+
self
102+
}
103+
}
104+
105+
mod defaults {
106+
use std::time::Duration;
107+
108+
pub fn http_timeout() -> Duration {
109+
Duration::from_secs(30)
110+
}
111+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
mod client;
2+
mod config;
3+
4+
pub use client::Client;
5+
pub use config::Config;

libs/api-client-framework/src/lib.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,11 @@
33
mod async_client;
44
mod endpoint;
55

6+
/// Blockscout services related structs.
7+
/// Contains config and client definitions to be used by blockscout-rs services.
8+
#[cfg(feature = "blockscout")]
9+
pub mod blockscout;
10+
611
pub use async_client::{HttpApiClient, HttpApiClientConfig};
712
pub use endpoint::{serialize_query, Endpoint};
813

0 commit comments

Comments
 (0)