use std::time::Duration;
use serde::de::DeserializeOwned;
use serde_json::{json, Value};
use tracing::{debug, warn};
use crate::error::{Error, GraphqlErrorEntry, Result};
use crate::queries;
use crate::types::*;
#[derive(Debug, Clone)]
pub struct ClientConfig {
pub graphql_uri: String,
pub retries: u32,
pub retry_delay: Duration,
pub timeout: Duration,
}
impl Default for ClientConfig {
fn default() -> Self {
Self {
graphql_uri: "http://localhost:8080/".to_string(),
retries: 3,
retry_delay: Duration::from_secs(5),
timeout: Duration::from_secs(30),
}
}
}
pub struct ArchiveClient {
config: ClientConfig,
http: reqwest::Client,
}
impl Default for ArchiveClient {
fn default() -> Self {
Self::with_config(ClientConfig::default())
}
}
impl ArchiveClient {
pub fn new(graphql_uri: &str) -> Self {
Self::with_config(ClientConfig {
graphql_uri: graphql_uri.to_string(),
..Default::default()
})
}
pub fn with_config(config: ClientConfig) -> Self {
assert!(config.retries >= 1, "retries must be at least 1");
assert!(
!config.timeout.is_zero(),
"timeout must be greater than zero"
);
let http = reqwest::Client::builder()
.timeout(config.timeout)
.build()
.expect("failed to build HTTP client");
Self { config, http }
}
pub fn graphql_uri(&self) -> &str {
&self.config.graphql_uri
}
pub fn query<'a>(&'a self, query: &'a str) -> QueryBuilder<'a> {
QueryBuilder {
client: self,
query,
variables: None,
name: None,
}
}
pub async fn execute_query(
&self,
query: &str,
variables: Option<Value>,
query_name: &str,
) -> Result<Value> {
let mut payload = json!({ "query": query });
if let Some(vars) = variables {
payload["variables"] = vars;
}
let mut last_err: Option<reqwest::Error> = None;
for attempt in 1..=self.config.retries {
debug!(
query_name,
attempt,
max = self.config.retries,
"GraphQL request"
);
match self
.http
.post(&self.config.graphql_uri)
.json(&payload)
.send()
.await
{
Ok(resp) => {
let status = resp.status();
if status.is_server_error() || status == reqwest::StatusCode::REQUEST_TIMEOUT {
warn!(query_name, attempt, %status, "retryable HTTP error");
last_err = Some(resp.error_for_status().unwrap_err());
if attempt < self.config.retries {
tokio::time::sleep(self.config.retry_delay).await;
}
continue;
}
let body: Value = match resp.json().await {
Ok(b) => b,
Err(e) => {
warn!(query_name, attempt, error = %e, "failed to parse JSON");
last_err = Some(e);
if attempt < self.config.retries {
tokio::time::sleep(self.config.retry_delay).await;
}
continue;
}
};
if let Some(errors) = body.get("errors").and_then(|e| e.as_array()) {
let entries: Vec<GraphqlErrorEntry> = errors
.iter()
.map(|e| GraphqlErrorEntry {
message: e
.get("message")
.and_then(|m| m.as_str())
.unwrap_or("unknown error")
.to_string(),
})
.collect();
let messages = entries
.iter()
.map(|e| e.message.as_str())
.collect::<Vec<_>>()
.join("; ");
return Err(Error::Graphql {
query_name: query_name.to_string(),
messages,
errors: entries,
});
}
return Ok(body
.get("data")
.cloned()
.unwrap_or(Value::Object(Default::default())));
}
Err(e) => {
warn!(query_name, attempt, error = %e, "transport error");
last_err = Some(e);
}
}
if attempt < self.config.retries {
tokio::time::sleep(self.config.retry_delay).await;
}
}
Err(Error::Connection {
query_name: query_name.to_string(),
attempts: self.config.retries,
source: last_err.expect("at least one attempt must have been made"),
})
}
pub async fn get_events(&self, input: EventFilterOptionsInput) -> Result<Vec<EventOutput>> {
let data = self
.execute_query(
queries::EVENTS_QUERY,
Some(json!({ "input": input })),
"get_events",
)
.await?;
decode_field(&data, "events", "get_events")
}
pub async fn get_actions(&self, input: ActionFilterOptionsInput) -> Result<Vec<ActionOutput>> {
let data = self
.execute_query(
queries::ACTIONS_QUERY,
Some(json!({ "input": input })),
"get_actions",
)
.await?;
decode_field(&data, "actions", "get_actions")
}
pub async fn get_network_state(&self) -> Result<NetworkStateOutput> {
let data = self
.execute_query(queries::NETWORK_STATE_QUERY, None, "get_network_state")
.await?;
decode_field(&data, "networkState", "get_network_state")
}
pub async fn get_blocks(&self, opts: GetBlocksOptions) -> Result<Vec<Block>> {
let vars = json!({
"query": opts.query,
"limit": opts.limit,
"sortBy": opts.sort_by,
});
let data = self
.execute_query(queries::BLOCKS_QUERY, Some(vars), "get_blocks")
.await?;
decode_field(&data, "blocks", "get_blocks")
}
}
#[derive(Debug, Clone, Default)]
pub struct GetBlocksOptions {
pub query: Option<BlockQueryInput>,
pub limit: Option<i64>,
pub sort_by: Option<BlockSortBy>,
}
#[must_use = "call .send() to execute the query"]
pub struct QueryBuilder<'a> {
client: &'a ArchiveClient,
query: &'a str,
variables: Option<Value>,
name: Option<&'a str>,
}
impl<'a> QueryBuilder<'a> {
pub fn variables(mut self, variables: Value) -> Self {
self.variables = Some(variables);
self
}
pub fn name(mut self, name: &'a str) -> Self {
self.name = Some(name);
self
}
pub async fn send(self) -> Result<Value> {
self.client
.execute_query(self.query, self.variables, self.name.unwrap_or("custom"))
.await
}
}
fn decode_field<T: DeserializeOwned>(data: &Value, field: &str, query_name: &str) -> Result<T> {
let v = data
.get(field)
.filter(|v| !v.is_null())
.ok_or_else(|| Error::MissingField {
query_name: query_name.to_string(),
field: field.to_string(),
})?;
serde_json::from_value(v.clone()).map_err(|source| Error::Decode {
query_name: query_name.to_string(),
source,
})
}