Skip to content

Commit

Permalink
feat(api-client-framework): add blockscout services related helper de…
Browse files Browse the repository at this point in the history
…finitions
  • Loading branch information
rimrakhimov committed Jan 22, 2025
1 parent c55b2bb commit b21cedc
Show file tree
Hide file tree
Showing 5 changed files with 221 additions and 0 deletions.
9 changes: 9 additions & 0 deletions libs/api-client-framework/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,12 @@ serde_path_to_error = { version = "0.1.16", default-features = false }
serde_urlencoded = { version = "0.7", default-features = false }
thiserror = { version = "2", default-features = false }
url = { version = "2", default-features = false }

reqwest-retry = { version = "0.7.0", default-features = false, optional = true }

[features]
"blockscout" = [
"dep:reqwest-retry",
"serde/derive",
"url/serde"
]
91 changes: 91 additions & 0 deletions libs/api-client-framework/src/blockscout/client.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
use super::config;
use crate::{Endpoint, Error, HttpApiClient, HttpApiClientConfig};
use reqwest::header::HeaderValue;

pub struct Client {
http_client: HttpApiClient,
api_key: Option<HeaderValue>,
}

impl Client {
pub async fn new(config: config::Config) -> Self {
let http_client_config = HttpApiClientConfig {
http_timeout: config.http_timeout,
default_headers: Default::default(),
middlewares: config.middlewares,
};

let http_client = HttpApiClient::new(config.url, http_client_config)
.unwrap_or_else(|err| panic!("cannot build an http client: {err:#?}"));

let client = Self {
http_client,
api_key: config.api_key,
};

if config.probe_url {
let endpoint = health::HealthCheck::new(Default::default());
if let Err(err) = client.request(&endpoint).await {
panic!("Cannot establish a connection with contracts-info client: {err}")
}
}

client
}

pub async fn request<EndpointType: Endpoint>(
&self,
endpoint: &EndpointType,
) -> Result<<EndpointType as Endpoint>::Response, Error> {
self.http_client.request(endpoint).await
}

pub fn api_key(&self) -> Option<&HeaderValue> {
self.api_key.as_ref()
}
}

/// As we don't have protobuf generated structures here (they are only available inside a service),
/// we have to imitate the service health endpoint.
mod health {
use crate::{serialize_query, Endpoint};
use reqwest::Method;
use serde::{Deserialize, Serialize};

#[derive(Debug, Default, Serialize)]
pub struct HealthCheckRequest {
pub service: String,
}

#[derive(Debug, Deserialize)]
pub struct HealthCheckResponse {
#[serde(rename = "status")]
pub _status: i32,
}

pub struct HealthCheck {
request: HealthCheckRequest,
}

impl HealthCheck {
pub fn new(request: HealthCheckRequest) -> Self {
Self { request }
}
}

impl Endpoint for HealthCheck {
type Response = HealthCheckResponse;

fn method(&self) -> Method {
Method::GET
}

fn path(&self) -> String {
"/health".to_string()
}

fn query(&self) -> Option<String> {
serialize_query(&self.request)
}
}
}
111 changes: 111 additions & 0 deletions libs/api-client-framework/src/blockscout/config.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
use reqwest::header::HeaderValue;
use reqwest_middleware::Middleware;
use reqwest_retry::{policies::ExponentialBackoff, RetryTransientMiddleware};
use serde::{Deserialize, Deserializer};
use std::{fmt, fmt::Formatter, sync::Arc, time::Duration};

#[derive(Clone, Deserialize)]
pub struct Config {
pub url: url::Url,
#[serde(default, deserialize_with = "deserialize_api_key")]
pub api_key: Option<HeaderValue>,
/// The maximum time limit for an API request. If a request takes longer than this, it will be
/// cancelled. Defaults to 30 seconds.
#[serde(default = "defaults::http_timeout")]
pub http_timeout: Duration,
#[serde(default)]
pub probe_url: bool,
#[serde(skip_deserializing)]
pub middlewares: Vec<Arc<dyn Middleware>>,
}

fn deserialize_api_key<'de, D>(deserializer: D) -> Result<Option<HeaderValue>, D::Error>
where
D: Deserializer<'de>,
{
let string = Option::<String>::deserialize(deserializer)?;
string
.map(|value| HeaderValue::from_str(&value))
.transpose()
.map_err(<D::Error as serde::de::Error>::custom)
}

// We have to derive `Debug` manually as we need to skip middlewares field which does not implement it.
impl fmt::Debug for Config {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
#[derive(Debug)]
#[allow(dead_code)]
struct ConfigDebug<'a> {
url: &'a url::Url,
api_key: &'a Option<HeaderValue>,
http_timeout: &'a Duration,
probe_url: &'a bool,
}
let Config {
url,
api_key,
http_timeout,
probe_url,
middlewares: _,
} = self;
fmt::Debug::fmt(
&ConfigDebug {
url,
api_key,
http_timeout,
probe_url,
},
f,
)
}
}

impl Config {
pub fn new(url: url::Url) -> Self {
Self {
url,
api_key: None,
http_timeout: defaults::http_timeout(),
middlewares: vec![],
probe_url: false,
}
}

pub fn with_retry_middleware(self, max_retries: u32) -> Self {
let retry_policy = ExponentialBackoff::builder().build_with_max_retries(max_retries);
let middleware = RetryTransientMiddleware::new_with_policy(retry_policy);
self.with_middleware(middleware)
}

pub fn with_middleware<M: Middleware>(self, middleware: M) -> Self {
self.with_arc_middleware(Arc::new(middleware))
}

pub fn with_arc_middleware<M: Middleware>(mut self, middleware: Arc<M>) -> Self {
self.middlewares.push(middleware);
self
}

pub fn probe_url(mut self, value: bool) -> Self {
self.probe_url = value;
self
}

pub fn api_key(mut self, api_key: Option<HeaderValue>) -> Self {
self.api_key = api_key;
self
}

pub fn http_timeout(mut self, timeout: Duration) -> Self {
self.http_timeout = timeout;
self
}
}

mod defaults {
use std::time::Duration;

pub fn http_timeout() -> Duration {
Duration::from_secs(30)
}
}
5 changes: 5 additions & 0 deletions libs/api-client-framework/src/blockscout/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
mod client;
mod config;

pub use client::Client;
pub use config::Config;
5 changes: 5 additions & 0 deletions libs/api-client-framework/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@
mod async_client;
mod endpoint;

/// Blockscout services related structs.
/// Contains config and client definitions to be used by blockscout-rs services.
#[cfg(feature = "blockscout")]
pub mod blockscout;

pub use async_client::{HttpApiClient, HttpApiClientConfig};
pub use endpoint::{serialize_query, Endpoint};

Expand Down

0 comments on commit b21cedc

Please sign in to comment.