use reqwest::Client as HttpClient;
use std::sync::Arc;
use std::time::Duration;
use yldfi_common::api::{extract_retry_after, ApiConfig};
use yldfi_common::rate_limit::RateLimiter;
use crate::error::{from_response, graphql_error, Error, Result};
use crate::types::GraphQLResponse;
pub const BASE_URL: &str = "https://kong.yearn.farm/api/gql";
#[derive(Debug, Clone)]
#[must_use = "Config must be passed to Client::with_config() to take effect"]
pub struct Config {
inner: ApiConfig,
rate_limiter: Option<RateLimiter>,
}
impl Config {
pub fn new() -> Self {
Self {
inner: ApiConfig::new(BASE_URL),
rate_limiter: None,
}
}
pub fn with_timeout(mut self, timeout: Duration) -> Self {
self.inner.http.timeout = timeout;
self
}
pub fn with_proxy(mut self, proxy: impl Into<String>) -> Self {
self.inner.http.proxy = Some(proxy.into());
self
}
pub fn with_optional_proxy(mut self, proxy: Option<String>) -> Self {
self.inner.http.proxy = proxy;
self
}
pub fn with_rate_limit(mut self, max_requests: u32, window: Duration) -> Self {
self.rate_limiter = Some(RateLimiter::new(max_requests, window));
self
}
pub fn with_rate_limiter(mut self, limiter: RateLimiter) -> Self {
self.rate_limiter = Some(limiter);
self
}
}
impl Default for Config {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone)]
pub struct Client {
http: Arc<HttpClient>,
base_url: String,
rate_limiter: Option<RateLimiter>,
}
impl Client {
pub fn new() -> Result<Self> {
Self::with_config(Config::new())
}
pub fn with_config(config: Config) -> Result<Self> {
let http = config.inner.build_client()?;
Ok(Self {
http: Arc::new(http),
base_url: BASE_URL.to_string(),
rate_limiter: config.rate_limiter,
})
}
#[must_use]
pub fn with_http_client(http: HttpClient) -> Self {
Self {
http: Arc::new(http),
base_url: BASE_URL.to_string(),
rate_limiter: None,
}
}
#[must_use]
pub fn http(&self) -> &HttpClient {
&self.http
}
#[must_use]
pub fn rate_limiter(&self) -> Option<&RateLimiter> {
self.rate_limiter.as_ref()
}
pub async fn query<T>(&self, query: &str) -> Result<T>
where
T: serde::de::DeserializeOwned,
{
self.query_with_variables(query, serde_json::Value::Null)
.await
}
pub async fn query_with_variables<T>(
&self,
query: &str,
variables: serde_json::Value,
) -> Result<T>
where
T: serde::de::DeserializeOwned,
{
if let Some(limiter) = &self.rate_limiter {
limiter.acquire().await;
}
let body = if variables.is_null() {
serde_json::json!({ "query": query })
} else {
serde_json::json!({
"query": query,
"variables": variables
})
};
let query_preview: String = query.chars().take(100).collect();
let response = self
.http
.post(&self.base_url)
.json(&body)
.send()
.await
.map_err(|e| Error::Api {
status: 0,
message: format!("HTTP request failed for query '{query_preview}...': {e}"),
})?;
let status = response.status().as_u16();
if !response.status().is_success() {
let retry_after = extract_retry_after(response.headers());
let body = response.text().await.unwrap_or_default();
return Err(from_response(status, &body, retry_after));
}
let body = response.text().await.map_err(|e| Error::Api {
status,
message: format!("Failed to read response body: {e}"),
})?;
let gql_response: GraphQLResponse<T> =
serde_json::from_str(&body).map_err(|e| Error::Api {
status,
message: format!(
"Failed to parse GraphQL response for query '{query_preview}...': {e}"
),
})?;
if let Some(errors) = gql_response.errors {
if !errors.is_empty() {
let error_messages: Vec<&str> = errors.iter().map(|e| e.message.as_str()).collect();
let combined = if error_messages.len() == 1 {
error_messages[0].to_string()
} else {
format!(
"{} errors: {}",
error_messages.len(),
error_messages.join("; ")
)
};
return Err(graphql_error(combined));
}
}
gql_response
.data
.ok_or_else(|| graphql_error("No data in GraphQL response"))
}
}