dune_api/
client.rs

1//! HTTP client for the Dune API
2
3use crate::error::{self, Error, Result};
4use crate::executions::ExecutionsApi;
5use crate::matviews::MatviewsApi;
6use crate::pipelines::PipelinesApi;
7use crate::queries::QueriesApi;
8use crate::tables::TablesApi;
9use crate::usage::UsageApi;
10use reqwest::header::{HeaderMap, HeaderValue};
11use secrecy::{ExposeSecret, SecretString};
12use std::fmt;
13use std::time::Duration;
14use yldfi_common::http::HttpClientConfig;
15
16const BASE_URL: &str = "https://api.dune.com/api";
17
18/// Configuration for the Dune API client
19#[derive(Clone)]
20pub struct Config {
21    /// API key for authentication (redacted in Debug output)
22    pub api_key: SecretString,
23    /// Base URL for the API
24    pub base_url: String,
25    /// HTTP client configuration (timeout, proxy, user-agent)
26    pub http: HttpClientConfig,
27}
28
29impl fmt::Debug for Config {
30    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
31        f.debug_struct("Config")
32            .field("api_key", &"[REDACTED]")
33            .field("base_url", &self.base_url)
34            .field("http", &self.http)
35            .finish()
36    }
37}
38
39impl Config {
40    /// Create a new configuration with the given API key
41    pub fn new(api_key: impl Into<String>) -> Self {
42        Self {
43            api_key: SecretString::from(api_key.into()),
44            base_url: BASE_URL.to_string(),
45            http: HttpClientConfig::default(),
46        }
47    }
48
49    /// Set a custom base URL
50    #[must_use]
51    pub fn with_base_url(mut self, url: impl Into<String>) -> Self {
52        self.base_url = url.into();
53        self
54    }
55
56    /// Set a custom timeout
57    #[must_use]
58    pub fn with_timeout(mut self, timeout: Duration) -> Self {
59        self.http.timeout = timeout;
60        self
61    }
62
63    /// Set a proxy URL
64    #[must_use]
65    pub fn with_proxy(mut self, proxy: impl Into<String>) -> Self {
66        self.http.proxy = Some(proxy.into());
67        self
68    }
69
70    /// Set optional proxy URL
71    #[must_use]
72    pub fn with_optional_proxy(mut self, proxy: Option<String>) -> Self {
73        self.http.proxy = proxy;
74        self
75    }
76}
77
78/// Dune Analytics API client
79#[derive(Clone)]
80pub struct Client {
81    http: reqwest::Client,
82    base_url: String,
83}
84
85impl Client {
86    /// Create a new Dune client with the given API key
87    pub fn new(api_key: &str) -> Result<Self> {
88        Self::with_config(Config::new(api_key))
89    }
90
91    /// Create a client with a custom base URL (useful for testing)
92    pub fn with_base_url(api_key: &str, base_url: &str) -> Result<Self> {
93        Self::with_config(Config::new(api_key).with_base_url(base_url))
94    }
95
96    /// Create a client with custom configuration
97    pub fn with_config(config: Config) -> Result<Self> {
98        let mut headers = HeaderMap::new();
99        headers.insert(
100            "X-Dune-API-Key",
101            HeaderValue::from_str(config.api_key.expose_secret())
102                .map_err(|_| error::invalid_api_key())?,
103        );
104
105        // Build the HTTP client with proxy support and custom headers
106        let mut builder = reqwest::Client::builder()
107            .timeout(config.http.timeout)
108            .user_agent(&config.http.user_agent)
109            .default_headers(headers);
110
111        if let Some(ref proxy_url) = config.http.proxy {
112            let proxy = reqwest::Proxy::all(proxy_url).map_err(|e| Error::Api {
113                status: 0,
114                message: format!("Invalid proxy URL: {}", e),
115            })?;
116            builder = builder.proxy(proxy);
117        }
118
119        let http = builder.build()?;
120
121        Ok(Self {
122            http,
123            base_url: config.base_url,
124        })
125    }
126
127    /// Create a new client from environment variable
128    ///
129    /// Uses `DUNE_API_KEY` environment variable
130    pub fn from_env() -> Result<Self> {
131        let api_key = std::env::var("DUNE_API_KEY").map_err(|_| error::invalid_api_key())?;
132        Self::new(&api_key)
133    }
134
135    /// Get the HTTP client
136    pub(crate) fn http(&self) -> &reqwest::Client {
137        &self.http
138    }
139
140    /// Get the base URL
141    pub(crate) fn base_url(&self) -> &str {
142        &self.base_url
143    }
144
145    /// Access the Queries API
146    pub fn queries(&self) -> QueriesApi<'_> {
147        QueriesApi::new(self)
148    }
149
150    /// Access the Executions API
151    pub fn executions(&self) -> ExecutionsApi<'_> {
152        ExecutionsApi::new(self)
153    }
154
155    /// Access the Tables (uploads) API
156    pub fn tables(&self) -> TablesApi<'_> {
157        TablesApi::new(self)
158    }
159
160    /// Access the Materialized Views API
161    pub fn matviews(&self) -> MatviewsApi<'_> {
162        MatviewsApi::new(self)
163    }
164
165    /// Access the Pipelines API
166    pub fn pipelines(&self) -> PipelinesApi<'_> {
167        PipelinesApi::new(self)
168    }
169
170    /// Access the Usage API
171    pub fn usage(&self) -> UsageApi<'_> {
172        UsageApi::new(self)
173    }
174}