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 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#[derive(Debug, Clone)]
53pub struct ClientInfo {
54 pub language: String,
56 pub language_version: String,
58 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, serde::Serialize, serde::Deserialize)]
148pub struct SdkFullConfig {
149 pub api_key: String,
150 pub http: Option<HttpConfig>,
151 pub admin: Option<AdminConfig>,
152 pub streams: Option<StreamsConfig>,
153 pub webhooks: Option<WebhooksConfig>,
154 pub kvstore: Option<KvStoreConfig>,
155}
156
157impl SdkFullConfig {
158 pub fn from_api_key(api_key: String) -> Self {
159 SdkFullConfig {
160 api_key,
161 http: None,
162 admin: None,
163 streams: None,
164 webhooks: None,
165 kvstore: None,
166 }
167 }
168
169 pub fn from_env() -> Result<Self, SdkError> {
170 config::Config::builder()
171 .add_source(
172 config::Environment::with_prefix("QN_SDK")
173 .separator("__")
174 .try_parsing(true),
175 )
176 .build()
177 .map_err(|e| SdkError::Config(e.to_string()))
178 .and_then(Self::from_config)
179 }
180
181 fn from_config(cfg: config::Config) -> Result<Self, SdkError> {
182 cfg.try_deserialize::<SdkFullConfig>()
183 .map_err(|e| SdkError::Config(e.to_string()))
184 }
185}
186
187#[cfg(feature = "python")]
188#[gen_stub_pymethods]
189#[pymethods]
190impl SdkFullConfig {
191 #[new]
192 #[pyo3(signature = (api_key, http=None, admin=None, streams=None, webhooks=None, kvstore=None))]
193 pub fn new(
194 api_key: String,
195 http: Option<HttpConfig>,
196 admin: Option<AdminConfig>,
197 streams: Option<StreamsConfig>,
198 webhooks: Option<WebhooksConfig>,
199 kvstore: Option<KvStoreConfig>,
200 ) -> Self {
201 SdkFullConfig {
202 api_key,
203 http,
204 admin,
205 streams,
206 webhooks,
207 kvstore,
208 }
209 }
210}
211
212#[cfg(test)]
213#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
214mod tests {
215 use super::*;
216
217 fn build_config(pairs: &[(&str, &str)]) -> config::Config {
218 let mut builder = config::Config::builder();
219 for (k, v) in pairs {
220 builder = builder.set_override(*k, *v).unwrap();
221 }
222 builder.build().unwrap()
223 }
224
225 #[test]
226 fn from_env_missing_api_key_returns_error() {
227 let cfg = config::Config::builder().build().unwrap();
228 assert!(matches!(
229 SdkFullConfig::from_config(cfg),
230 Err(SdkError::Config(_))
231 ));
232 }
233
234 #[test]
235 fn from_env_only_api_key() {
236 let cfg = build_config(&[("api_key", "test-key")]);
237 let config = SdkFullConfig::from_config(cfg).unwrap();
238 assert_eq!(config.api_key, "test-key");
239 assert!(config.http.is_none());
240 assert!(config.admin.is_none());
241 }
242
243 #[test]
244 fn from_env_all_fields() {
245 let cfg = build_config(&[
246 ("api_key", "my-api-key"),
247 ("http.timeout_secs", "30"),
248 ("http.pool_max_idle_per_host", "5"),
249 ("admin.base_url", "https://example.com/"),
250 ]);
251 let config = SdkFullConfig::from_config(cfg).unwrap();
252 assert_eq!(config.api_key, "my-api-key");
253 let http = config.http.unwrap();
254 assert_eq!(http.timeout_secs, Some(30));
255 assert_eq!(http.pool_max_idle_per_host, Some(5));
256 let admin = config.admin.unwrap();
257 assert_eq!(admin.base_url, Some("https://example.com/".to_string()));
258 }
259
260 #[test]
261 fn from_env_invalid_timeout_secs() {
262 let cfg = build_config(&[("api_key", "test-key"), ("http.timeout_secs", "abc")]);
263 assert!(matches!(
264 SdkFullConfig::from_config(cfg),
265 Err(SdkError::Config(_))
266 ));
267 }
268
269 #[test]
270 fn from_env_headers_round_trip() {
271 let cfg = build_config(&[
272 ("api_key", "k"),
273 ("http.headers.x-correlation-id", "abc"),
274 ("http.headers.user-agent", "custom-ua/1.0"),
275 ]);
276 let config = SdkFullConfig::from_config(cfg).unwrap();
277 let headers = config.http.unwrap().headers.unwrap();
278 assert_eq!(
279 headers.get("x-correlation-id").map(String::as_str),
280 Some("abc")
281 );
282 assert_eq!(
283 headers.get("user-agent").map(String::as_str),
284 Some("custom-ua/1.0")
285 );
286 }
287}