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}
21
22#[cfg(feature = "python")]
23#[gen_stub_pymethods]
24#[pymethods]
25impl HttpConfig {
26    #[new]
27    #[pyo3(signature = (timeout_secs=None, pool_max_idle_per_host=None))]
28    pub fn new(timeout_secs: Option<i64>, pool_max_idle_per_host: Option<i32>) -> Self {
29        HttpConfig {
30            timeout_secs,
31            pool_max_idle_per_host,
32        }
33    }
34}
35
36#[cfg_attr(feature = "python", gen_stub_pyclass)]
37#[cfg_attr(feature = "python", pyclass(get_all, set_all))]
38#[cfg_attr(feature = "node", napi(object))]
39#[cfg_attr(feature = "rust", derive(Builder))]
40#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
41pub struct AdminConfig {
42    pub base_url: Option<String>,
43}
44
45#[cfg(feature = "python")]
46#[gen_stub_pymethods]
47#[pymethods]
48impl AdminConfig {
49    #[new]
50    #[pyo3(signature = (base_url=None))]
51    pub fn new(base_url: Option<String>) -> Self {
52        AdminConfig { base_url }
53    }
54}
55
56#[cfg_attr(feature = "python", gen_stub_pyclass)]
57#[cfg_attr(feature = "python", pyclass(get_all, set_all))]
58#[cfg_attr(feature = "node", napi(object))]
59#[cfg_attr(feature = "rust", derive(Builder))]
60#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
61pub struct StreamsConfig {
62    pub base_url: Option<String>,
63}
64
65#[cfg(feature = "python")]
66#[gen_stub_pymethods]
67#[pymethods]
68impl StreamsConfig {
69    #[new]
70    #[pyo3(signature = (base_url=None))]
71    pub fn new(base_url: Option<String>) -> Self {
72        StreamsConfig { base_url }
73    }
74}
75
76#[cfg_attr(feature = "python", gen_stub_pyclass)]
77#[cfg_attr(feature = "python", pyclass(get_all, set_all))]
78#[cfg_attr(feature = "node", napi(object))]
79#[cfg_attr(feature = "rust", derive(Builder))]
80#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
81pub struct WebhooksConfig {
82    pub base_url: Option<String>,
83}
84
85#[cfg(feature = "python")]
86#[gen_stub_pymethods]
87#[pymethods]
88impl WebhooksConfig {
89    #[new]
90    #[pyo3(signature = (base_url=None))]
91    pub fn new(base_url: Option<String>) -> Self {
92        WebhooksConfig { base_url }
93    }
94}
95
96#[cfg_attr(feature = "python", gen_stub_pyclass)]
97#[cfg_attr(feature = "python", pyclass(get_all, set_all))]
98#[cfg_attr(feature = "node", napi(object))]
99#[cfg_attr(feature = "rust", derive(Builder))]
100#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
101pub struct KvStoreConfig {
102    pub base_url: Option<String>,
103}
104
105#[cfg(feature = "python")]
106#[gen_stub_pymethods]
107#[pymethods]
108impl KvStoreConfig {
109    #[new]
110    #[pyo3(signature = (base_url=None))]
111    pub fn new(base_url: Option<String>) -> Self {
112        KvStoreConfig { base_url }
113    }
114}
115
116#[cfg_attr(feature = "python", gen_stub_pyclass)]
117#[cfg_attr(feature = "python", pyclass(get_all, set_all))]
118#[cfg_attr(feature = "node", napi(object))]
119#[cfg_attr(feature = "rust", derive(Builder))]
120#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
121pub struct SdkFullConfig {
122    pub api_key: String,
123    pub http: Option<HttpConfig>,
124    pub admin: Option<AdminConfig>,
125    pub streams: Option<StreamsConfig>,
126    pub webhooks: Option<WebhooksConfig>,
127    pub kvstore: Option<KvStoreConfig>,
128}
129
130impl SdkFullConfig {
131    pub fn from_api_key(api_key: String) -> Self {
132        SdkFullConfig {
133            api_key,
134            http: None,
135            admin: None,
136            streams: None,
137            webhooks: None,
138            kvstore: None,
139        }
140    }
141
142    pub fn from_env() -> Result<Self, SdkError> {
143        config::Config::builder()
144            .add_source(
145                config::Environment::with_prefix("QN_SDK")
146                    .separator("__")
147                    .try_parsing(true),
148            )
149            .build()
150            .map_err(|e| SdkError::Config(e.to_string()))
151            .and_then(Self::from_config)
152    }
153
154    fn from_config(cfg: config::Config) -> Result<Self, SdkError> {
155        cfg.try_deserialize::<SdkFullConfig>()
156            .map_err(|e| SdkError::Config(e.to_string()))
157    }
158}
159
160#[cfg(feature = "python")]
161#[gen_stub_pymethods]
162#[pymethods]
163impl SdkFullConfig {
164    #[new]
165    #[pyo3(signature = (api_key, http=None, admin=None, streams=None, webhooks=None, kvstore=None))]
166    pub fn new(
167        api_key: String,
168        http: Option<HttpConfig>,
169        admin: Option<AdminConfig>,
170        streams: Option<StreamsConfig>,
171        webhooks: Option<WebhooksConfig>,
172        kvstore: Option<KvStoreConfig>,
173    ) -> Self {
174        SdkFullConfig {
175            api_key,
176            http,
177            admin,
178            streams,
179            webhooks,
180            kvstore,
181        }
182    }
183}
184
185#[cfg(test)]
186#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
187mod tests {
188    use super::*;
189
190    fn build_config(pairs: &[(&str, &str)]) -> config::Config {
191        let mut builder = config::Config::builder();
192        for (k, v) in pairs {
193            builder = builder.set_override(*k, *v).unwrap();
194        }
195        builder.build().unwrap()
196    }
197
198    #[test]
199    fn from_env_missing_api_key_returns_error() {
200        let cfg = config::Config::builder().build().unwrap();
201        assert!(matches!(
202            SdkFullConfig::from_config(cfg),
203            Err(SdkError::Config(_))
204        ));
205    }
206
207    #[test]
208    fn from_env_only_api_key() {
209        let cfg = build_config(&[("api_key", "test-key")]);
210        let config = SdkFullConfig::from_config(cfg).unwrap();
211        assert_eq!(config.api_key, "test-key");
212        assert!(config.http.is_none());
213        assert!(config.admin.is_none());
214    }
215
216    #[test]
217    fn from_env_all_fields() {
218        let cfg = build_config(&[
219            ("api_key", "my-api-key"),
220            ("http.timeout_secs", "30"),
221            ("http.pool_max_idle_per_host", "5"),
222            ("admin.base_url", "https://example.com/"),
223        ]);
224        let config = SdkFullConfig::from_config(cfg).unwrap();
225        assert_eq!(config.api_key, "my-api-key");
226        let http = config.http.unwrap();
227        assert_eq!(http.timeout_secs, Some(30));
228        assert_eq!(http.pool_max_idle_per_host, Some(5));
229        let admin = config.admin.unwrap();
230        assert_eq!(admin.base_url, Some("https://example.com/".to_string()));
231    }
232
233    #[test]
234    fn from_env_invalid_timeout_secs() {
235        let cfg = build_config(&[("api_key", "test-key"), ("http.timeout_secs", "abc")]);
236        assert!(matches!(
237            SdkFullConfig::from_config(cfg),
238            Err(SdkError::Config(_))
239        ));
240    }
241}