Skip to main content

quicknode_sdk/
config.rs

1#[cfg(feature = "rust")]
2use bon::Builder;
3#[cfg(feature = "node")]
4use napi_derive::napi;
5#[cfg(feature = "python")]
6use pyo3::{pyclass, pymethods};
7#[cfg(feature = "python")]
8use pyo3_stub_gen::derive::{gen_stub_pyclass, gen_stub_pymethods};
9
10use crate::errors::SdkError;
11
12#[cfg_attr(feature = "python", gen_stub_pyclass)]
13#[cfg_attr(feature = "python", pyclass(get_all, set_all))]
14#[cfg_attr(feature = "node", napi(object))]
15#[cfg_attr(feature = "rust", derive(Builder))]
16#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
17pub struct HttpConfig {
18    pub timeout_secs: Option<i64>,
19    pub pool_max_idle_per_host: Option<i32>,
20    /// Custom HTTP headers added to every outbound request.
21    ///
22    /// **These headers OVERRIDE any SDK-managed header with the same name**,
23    /// including `User-Agent`, `x-api-key`, `Accept`, and `Content-Type`.
24    /// Header names are matched case-insensitively. Use this to override the
25    /// auto-generated User-Agent or inject correlation IDs, proxy auth, etc.
26    pub headers: Option<std::collections::HashMap<String, String>>,
27}
28
29#[cfg(feature = "python")]
30#[gen_stub_pymethods]
31#[pymethods]
32impl HttpConfig {
33    #[new]
34    #[pyo3(signature = (timeout_secs=None, pool_max_idle_per_host=None, headers=None))]
35    pub fn new(
36        timeout_secs: Option<i64>,
37        pool_max_idle_per_host: Option<i32>,
38        headers: Option<std::collections::HashMap<String, String>>,
39    ) -> Self {
40        HttpConfig {
41            timeout_secs,
42            pool_max_idle_per_host,
43            headers,
44        }
45    }
46}
47
48/// Identifies the language and runtime making SDK calls. Each binding crate
49/// (Python, Node, Ruby) constructs this and passes it through
50/// [`SdkConfig::new_with_client_info`] so the SDK's auto-generated
51/// `User-Agent` reflects the actual caller, not the underlying Rust core.
52#[derive(Debug, Clone)]
53pub struct ClientInfo {
54    /// Short language identifier, e.g. `"python"`, `"node"`, `"ruby"`, `"rust"`.
55    pub language: String,
56    /// Runtime version of the language, e.g. `"3.12.4"`, `"20.10.0"`, `"3.3.0"`.
57    pub language_version: String,
58    /// Version string of the language-specific SDK package — read from the
59    /// language's own manifest (PyPI version, npm version, gem version).
60    pub sdk_version: String,
61}
62
63#[cfg_attr(feature = "python", gen_stub_pyclass)]
64#[cfg_attr(feature = "python", pyclass(get_all, set_all))]
65#[cfg_attr(feature = "node", napi(object))]
66#[cfg_attr(feature = "rust", derive(Builder))]
67#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
68pub struct AdminConfig {
69    pub base_url: Option<String>,
70}
71
72#[cfg(feature = "python")]
73#[gen_stub_pymethods]
74#[pymethods]
75impl AdminConfig {
76    #[new]
77    #[pyo3(signature = (base_url=None))]
78    pub fn new(base_url: Option<String>) -> Self {
79        AdminConfig { base_url }
80    }
81}
82
83#[cfg_attr(feature = "python", gen_stub_pyclass)]
84#[cfg_attr(feature = "python", pyclass(get_all, set_all))]
85#[cfg_attr(feature = "node", napi(object))]
86#[cfg_attr(feature = "rust", derive(Builder))]
87#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
88pub struct StreamsConfig {
89    pub base_url: Option<String>,
90}
91
92#[cfg(feature = "python")]
93#[gen_stub_pymethods]
94#[pymethods]
95impl StreamsConfig {
96    #[new]
97    #[pyo3(signature = (base_url=None))]
98    pub fn new(base_url: Option<String>) -> Self {
99        StreamsConfig { base_url }
100    }
101}
102
103#[cfg_attr(feature = "python", gen_stub_pyclass)]
104#[cfg_attr(feature = "python", pyclass(get_all, set_all))]
105#[cfg_attr(feature = "node", napi(object))]
106#[cfg_attr(feature = "rust", derive(Builder))]
107#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
108pub struct WebhooksConfig {
109    pub base_url: Option<String>,
110}
111
112#[cfg(feature = "python")]
113#[gen_stub_pymethods]
114#[pymethods]
115impl WebhooksConfig {
116    #[new]
117    #[pyo3(signature = (base_url=None))]
118    pub fn new(base_url: Option<String>) -> Self {
119        WebhooksConfig { base_url }
120    }
121}
122
123#[cfg_attr(feature = "python", gen_stub_pyclass)]
124#[cfg_attr(feature = "python", pyclass(get_all, set_all))]
125#[cfg_attr(feature = "node", napi(object))]
126#[cfg_attr(feature = "rust", derive(Builder))]
127#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
128pub struct KvStoreConfig {
129    pub base_url: Option<String>,
130}
131
132#[cfg(feature = "python")]
133#[gen_stub_pymethods]
134#[pymethods]
135impl KvStoreConfig {
136    #[new]
137    #[pyo3(signature = (base_url=None))]
138    pub fn new(base_url: Option<String>) -> Self {
139        KvStoreConfig { base_url }
140    }
141}
142
143#[cfg_attr(feature = "python", gen_stub_pyclass)]
144#[cfg_attr(feature = "python", pyclass(get_all, set_all))]
145#[cfg_attr(feature = "node", napi(object))]
146#[cfg_attr(feature = "rust", derive(Builder))]
147#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
148pub struct SqlConfig {
149    pub base_url: Option<String>,
150}
151
152#[cfg(feature = "python")]
153#[gen_stub_pymethods]
154#[pymethods]
155impl SqlConfig {
156    #[new]
157    #[pyo3(signature = (base_url=None))]
158    pub fn new(base_url: Option<String>) -> Self {
159        SqlConfig { base_url }
160    }
161}
162
163#[cfg_attr(feature = "python", gen_stub_pyclass)]
164#[cfg_attr(feature = "python", pyclass(get_all, set_all))]
165#[cfg_attr(feature = "node", napi(object))]
166#[cfg_attr(feature = "rust", derive(Builder))]
167#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
168pub struct SdkFullConfig {
169    pub api_key: String,
170    pub http: Option<HttpConfig>,
171    pub admin: Option<AdminConfig>,
172    pub streams: Option<StreamsConfig>,
173    pub webhooks: Option<WebhooksConfig>,
174    pub kvstore: Option<KvStoreConfig>,
175    pub sql: Option<SqlConfig>,
176}
177
178impl SdkFullConfig {
179    pub fn from_api_key(api_key: String) -> Self {
180        SdkFullConfig {
181            api_key,
182            http: None,
183            admin: None,
184            streams: None,
185            webhooks: None,
186            kvstore: None,
187            sql: None,
188        }
189    }
190
191    pub fn from_env() -> Result<Self, SdkError> {
192        config::Config::builder()
193            .add_source(
194                config::Environment::with_prefix("QN_SDK")
195                    .separator("__")
196                    .try_parsing(true),
197            )
198            .build()
199            .map_err(|e| SdkError::Config(e.to_string()))
200            .and_then(Self::from_config)
201    }
202
203    fn from_config(cfg: config::Config) -> Result<Self, SdkError> {
204        cfg.try_deserialize::<SdkFullConfig>()
205            .map_err(|e| SdkError::Config(e.to_string()))
206    }
207}
208
209#[cfg(feature = "python")]
210#[gen_stub_pymethods]
211#[pymethods]
212impl SdkFullConfig {
213    #[new]
214    #[pyo3(signature = (api_key, http=None, admin=None, streams=None, webhooks=None, kvstore=None, sql=None))]
215    pub fn new(
216        api_key: String,
217        http: Option<HttpConfig>,
218        admin: Option<AdminConfig>,
219        streams: Option<StreamsConfig>,
220        webhooks: Option<WebhooksConfig>,
221        kvstore: Option<KvStoreConfig>,
222        sql: Option<SqlConfig>,
223    ) -> Self {
224        SdkFullConfig {
225            api_key,
226            http,
227            admin,
228            streams,
229            webhooks,
230            kvstore,
231            sql,
232        }
233    }
234}
235
236#[cfg(test)]
237#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
238mod tests {
239    use super::*;
240
241    fn build_config(pairs: &[(&str, &str)]) -> config::Config {
242        let mut builder = config::Config::builder();
243        for (k, v) in pairs {
244            builder = builder.set_override(*k, *v).unwrap();
245        }
246        builder.build().unwrap()
247    }
248
249    #[test]
250    fn from_env_missing_api_key_returns_error() {
251        let cfg = config::Config::builder().build().unwrap();
252        assert!(matches!(
253            SdkFullConfig::from_config(cfg),
254            Err(SdkError::Config(_))
255        ));
256    }
257
258    #[test]
259    fn from_env_only_api_key() {
260        let cfg = build_config(&[("api_key", "test-key")]);
261        let config = SdkFullConfig::from_config(cfg).unwrap();
262        assert_eq!(config.api_key, "test-key");
263        assert!(config.http.is_none());
264        assert!(config.admin.is_none());
265    }
266
267    #[test]
268    fn from_env_all_fields() {
269        let cfg = build_config(&[
270            ("api_key", "my-api-key"),
271            ("http.timeout_secs", "30"),
272            ("http.pool_max_idle_per_host", "5"),
273            ("admin.base_url", "https://example.com/"),
274        ]);
275        let config = SdkFullConfig::from_config(cfg).unwrap();
276        assert_eq!(config.api_key, "my-api-key");
277        let http = config.http.unwrap();
278        assert_eq!(http.timeout_secs, Some(30));
279        assert_eq!(http.pool_max_idle_per_host, Some(5));
280        let admin = config.admin.unwrap();
281        assert_eq!(admin.base_url, Some("https://example.com/".to_string()));
282    }
283
284    #[test]
285    fn from_env_invalid_timeout_secs() {
286        let cfg = build_config(&[("api_key", "test-key"), ("http.timeout_secs", "abc")]);
287        assert!(matches!(
288            SdkFullConfig::from_config(cfg),
289            Err(SdkError::Config(_))
290        ));
291    }
292
293    #[test]
294    fn from_env_headers_round_trip() {
295        let cfg = build_config(&[
296            ("api_key", "k"),
297            ("http.headers.x-correlation-id", "abc"),
298            ("http.headers.user-agent", "custom-ua/1.0"),
299        ]);
300        let config = SdkFullConfig::from_config(cfg).unwrap();
301        let headers = config.http.unwrap().headers.unwrap();
302        assert_eq!(
303            headers.get("x-correlation-id").map(String::as_str),
304            Some("abc")
305        );
306        assert_eq!(
307            headers.get("user-agent").map(String::as_str),
308            Some("custom-ua/1.0")
309        );
310    }
311}