ccxt_exchanges/bitget/
builder.rs

1//! Bitget exchange builder pattern implementation.
2//!
3//! Provides a fluent API for constructing Bitget exchange instances with
4//! type-safe configuration options.
5
6use super::{Bitget, BitgetOptions};
7use ccxt_core::config::{ProxyConfig, RetryPolicy};
8use ccxt_core::types::default_type::{DefaultSubType, DefaultType};
9use ccxt_core::{ExchangeConfig, Result};
10use serde_json::Value;
11use std::collections::HashMap;
12use std::time::Duration;
13
14/// Builder for creating Bitget exchange instances.
15///
16/// Provides a fluent API for configuring all aspects of the Bitget exchange,
17/// including authentication, connection settings, and Bitget-specific options.
18///
19/// # Example
20///
21/// ```no_run
22/// use ccxt_exchanges::bitget::BitgetBuilder;
23///
24/// let bitget = BitgetBuilder::new()
25///     .api_key("your-api-key")
26///     .secret("your-secret")
27///     .passphrase("your-passphrase")
28///     .sandbox(true)
29///     .timeout(30)
30///     .build()
31///     .unwrap();
32/// ```
33#[derive(Debug, Clone)]
34pub struct BitgetBuilder {
35    /// Exchange configuration
36    config: ExchangeConfig,
37    /// Bitget-specific options
38    options: BitgetOptions,
39}
40
41impl Default for BitgetBuilder {
42    fn default() -> Self {
43        Self::new()
44    }
45}
46
47impl BitgetBuilder {
48    /// Creates a new builder with default configuration.
49    ///
50    /// # Example
51    ///
52    /// ```no_run
53    /// use ccxt_exchanges::bitget::BitgetBuilder;
54    ///
55    /// let builder = BitgetBuilder::new();
56    /// ```
57    pub fn new() -> Self {
58        Self {
59            config: ExchangeConfig {
60                id: "bitget".to_string(),
61                name: "Bitget".to_string(),
62                ..Default::default()
63            },
64            options: BitgetOptions::default(),
65        }
66    }
67
68    /// Sets the API key for authentication.
69    ///
70    /// # Arguments
71    ///
72    /// * `key` - The API key string.
73    pub fn api_key(mut self, key: impl Into<String>) -> Self {
74        self.config.api_key = Some(ccxt_core::SecretString::new(key));
75        self
76    }
77
78    /// Sets the API secret for authentication.
79    ///
80    /// # Arguments
81    ///
82    /// * `secret` - The API secret string.
83    pub fn secret(mut self, secret: impl Into<String>) -> Self {
84        self.config.secret = Some(ccxt_core::SecretString::new(secret));
85        self
86    }
87
88    /// Sets the passphrase for authentication.
89    ///
90    /// Bitget requires a passphrase in addition to API key and secret.
91    ///
92    /// # Arguments
93    ///
94    /// * `passphrase` - The passphrase string.
95    pub fn passphrase(mut self, passphrase: impl Into<String>) -> Self {
96        self.config.password = Some(ccxt_core::SecretString::new(passphrase));
97        self
98    }
99
100    /// Enables or disables sandbox/demo mode.
101    ///
102    /// When enabled, the exchange will connect to Bitget's demo
103    /// environment instead of the production environment.
104    ///
105    /// # Arguments
106    ///
107    /// * `enabled` - Whether to enable sandbox mode.
108    pub fn sandbox(mut self, enabled: bool) -> Self {
109        self.config.sandbox = enabled;
110        self.options.testnet = enabled;
111        self
112    }
113
114    /// Sets the product type for trading.
115    ///
116    /// Valid values: "spot", "umcbl" (USDT-M futures), "dmcbl" (Coin-M futures).
117    ///
118    /// Note: Consider using `default_type()` and `default_sub_type()` instead
119    /// for a more type-safe configuration.
120    ///
121    /// # Arguments
122    ///
123    /// * `product_type` - The product type string.
124    pub fn product_type(mut self, product_type: impl Into<String>) -> Self {
125        self.options.product_type = product_type.into();
126        self
127    }
128
129    /// Sets the default market type for trading.
130    ///
131    /// This determines which product type to use for API calls.
132    /// Bitget uses product_type-based filtering:
133    /// - `Spot` -> product_type=spot
134    /// - `Swap` + Linear -> product_type=umcbl (USDT-M)
135    /// - `Swap` + Inverse -> product_type=dmcbl (Coin-M)
136    /// - `Futures` + Linear -> product_type=umcbl (USDT-M futures)
137    /// - `Futures` + Inverse -> product_type=dmcbl (Coin-M futures)
138    ///
139    /// # Arguments
140    ///
141    /// * `default_type` - The default market type (spot, swap, futures, margin, option).
142    ///
143    /// # Example
144    ///
145    /// ```no_run
146    /// use ccxt_exchanges::bitget::BitgetBuilder;
147    /// use ccxt_core::types::default_type::DefaultType;
148    ///
149    /// let bitget = BitgetBuilder::new()
150    ///     .default_type(DefaultType::Swap)
151    ///     .build()
152    ///     .unwrap();
153    /// ```
154    pub fn default_type(mut self, default_type: impl Into<DefaultType>) -> Self {
155        self.options.default_type = default_type.into();
156        self
157    }
158
159    /// Sets the default sub-type for contract settlement.
160    ///
161    /// - `Linear`: USDT-margined contracts (product_type=umcbl)
162    /// - `Inverse`: Coin-margined contracts (product_type=dmcbl)
163    ///
164    /// Only applicable when `default_type` is `Swap` or `Futures`.
165    /// Ignored for `Spot` and `Option` types.
166    ///
167    /// # Arguments
168    ///
169    /// * `sub_type` - The contract settlement type (linear or inverse).
170    ///
171    /// # Example
172    ///
173    /// ```no_run
174    /// use ccxt_exchanges::bitget::BitgetBuilder;
175    /// use ccxt_core::types::default_type::{DefaultType, DefaultSubType};
176    ///
177    /// let bitget = BitgetBuilder::new()
178    ///     .default_type(DefaultType::Swap)
179    ///     .default_sub_type(DefaultSubType::Linear)
180    ///     .build()
181    ///     .unwrap();
182    /// ```
183    pub fn default_sub_type(mut self, sub_type: DefaultSubType) -> Self {
184        self.options.default_sub_type = Some(sub_type);
185        self
186    }
187
188    /// Sets the request timeout.
189    pub fn timeout(mut self, timeout: Duration) -> Self {
190        self.config.timeout = timeout;
191        self
192    }
193
194    /// Sets the request timeout in seconds (convenience method).
195    pub fn timeout_secs(mut self, seconds: u64) -> Self {
196        self.config.timeout = Duration::from_secs(seconds);
197        self
198    }
199
200    /// Sets the TCP connection timeout.
201    ///
202    /// # Arguments
203    ///
204    /// * `timeout` - Connection timeout duration.
205    pub fn connect_timeout(mut self, timeout: Duration) -> Self {
206        self.config.connect_timeout = timeout;
207        self
208    }
209
210    /// Sets the TCP connection timeout in seconds (convenience method).
211    ///
212    /// # Arguments
213    ///
214    /// * `seconds` - Connection timeout duration in seconds.
215    pub fn connect_timeout_secs(mut self, seconds: u64) -> Self {
216        self.config.connect_timeout = Duration::from_secs(seconds);
217        self
218    }
219
220    /// Sets the retry policy.
221    pub fn retry_policy(mut self, policy: RetryPolicy) -> Self {
222        self.config.retry_policy = Some(policy);
223        self
224    }
225
226    /// Sets the receive window for signed requests.
227    ///
228    /// The receive window specifies how long a request is valid after
229    /// the timestamp. Default is 5000 milliseconds.
230    ///
231    /// # Arguments
232    ///
233    /// * `millis` - Receive window in milliseconds.
234    pub fn recv_window(mut self, millis: u64) -> Self {
235        self.options.recv_window = millis;
236        self
237    }
238
239    /// Enables or disables rate limiting.
240    ///
241    /// # Arguments
242    ///
243    /// * `enabled` - Whether to enable rate limiting.
244    pub fn enable_rate_limit(mut self, enabled: bool) -> Self {
245        self.config.enable_rate_limit = enabled;
246        self
247    }
248
249    /// Sets the HTTP proxy configuration.
250    pub fn proxy(mut self, proxy: ProxyConfig) -> Self {
251        self.config.proxy = Some(proxy);
252        self
253    }
254
255    /// Sets the HTTP proxy URL (convenience method).
256    pub fn proxy_url(mut self, url: impl Into<String>) -> Self {
257        self.config.proxy = Some(ProxyConfig::new(url));
258        self
259    }
260
261    /// Enables or disables verbose logging.
262    ///
263    /// # Arguments
264    ///
265    /// * `enabled` - Whether to enable verbose logging.
266    pub fn verbose(mut self, enabled: bool) -> Self {
267        self.config.verbose = enabled;
268        self
269    }
270
271    /// Sets a custom option.
272    ///
273    /// # Arguments
274    ///
275    /// * `key` - Option key.
276    /// * `value` - Option value as JSON.
277    pub fn option(mut self, key: impl Into<String>, value: Value) -> Self {
278        self.config.options.insert(key.into(), value);
279        self
280    }
281
282    /// Sets multiple custom options.
283    ///
284    /// # Arguments
285    ///
286    /// * `options` - HashMap of option key-value pairs.
287    pub fn options(mut self, options: HashMap<String, Value>) -> Self {
288        self.config.options.extend(options);
289        self
290    }
291
292    /// Returns the current configuration (for testing purposes).
293    #[cfg(test)]
294    pub fn get_config(&self) -> &ExchangeConfig {
295        &self.config
296    }
297
298    /// Returns the current options (for testing purposes).
299    #[cfg(test)]
300    pub fn get_options(&self) -> &BitgetOptions {
301        &self.options
302    }
303
304    /// Builds the Bitget exchange instance.
305    ///
306    /// # Returns
307    ///
308    /// Returns a `Result` containing the configured `Bitget` instance.
309    ///
310    /// # Errors
311    ///
312    /// Returns an error if the exchange cannot be initialized.
313    pub fn build(self) -> Result<Bitget> {
314        Bitget::new_with_options(self.config, self.options)
315    }
316}
317
318#[cfg(test)]
319mod tests {
320    use super::*;
321
322    #[test]
323    fn test_builder_default() {
324        let builder = BitgetBuilder::new();
325        assert_eq!(builder.config.id, "bitget");
326        assert_eq!(builder.config.name, "Bitget");
327        assert!(!builder.config.sandbox);
328        assert_eq!(builder.options.product_type, "spot");
329    }
330
331    #[test]
332    fn test_builder_api_key() {
333        let builder = BitgetBuilder::new().api_key("test-key");
334        assert_eq!(
335            builder.config.api_key.as_ref().map(|s| s.expose_secret()),
336            Some("test-key")
337        );
338    }
339
340    #[test]
341    fn test_builder_secret() {
342        let builder = BitgetBuilder::new().secret("test-secret");
343        assert_eq!(
344            builder.config.secret.as_ref().map(|s| s.expose_secret()),
345            Some("test-secret")
346        );
347    }
348
349    #[test]
350    fn test_builder_passphrase() {
351        let builder = BitgetBuilder::new().passphrase("test-passphrase");
352        assert_eq!(
353            builder.config.password.as_ref().map(|s| s.expose_secret()),
354            Some("test-passphrase")
355        );
356    }
357
358    #[test]
359    fn test_builder_sandbox() {
360        let builder = BitgetBuilder::new().sandbox(true);
361        assert!(builder.config.sandbox);
362        assert!(builder.options.testnet);
363    }
364
365    #[test]
366    fn test_builder_product_type() {
367        let builder = BitgetBuilder::new().product_type("umcbl");
368        assert_eq!(builder.options.product_type, "umcbl");
369    }
370
371    #[test]
372    fn test_builder_default_type() {
373        let builder = BitgetBuilder::new().default_type(DefaultType::Swap);
374        assert_eq!(builder.options.default_type, DefaultType::Swap);
375    }
376
377    #[test]
378    fn test_builder_default_type_from_string() {
379        let builder = BitgetBuilder::new().default_type("futures");
380        assert_eq!(builder.options.default_type, DefaultType::Futures);
381    }
382
383    #[test]
384    fn test_builder_default_sub_type() {
385        let builder = BitgetBuilder::new().default_sub_type(DefaultSubType::Inverse);
386        assert_eq!(
387            builder.options.default_sub_type,
388            Some(DefaultSubType::Inverse)
389        );
390    }
391
392    #[test]
393    fn test_builder_default_type_and_sub_type() {
394        let builder = BitgetBuilder::new()
395            .default_type(DefaultType::Swap)
396            .default_sub_type(DefaultSubType::Linear);
397        assert_eq!(builder.options.default_type, DefaultType::Swap);
398        assert_eq!(
399            builder.options.default_sub_type,
400            Some(DefaultSubType::Linear)
401        );
402    }
403
404    #[test]
405    fn test_builder_timeout() {
406        let builder = BitgetBuilder::new().timeout(Duration::from_secs(60));
407        assert_eq!(builder.config.timeout, Duration::from_secs(60));
408    }
409
410    #[test]
411    fn test_builder_connect_timeout() {
412        let builder = BitgetBuilder::new().connect_timeout(Duration::from_secs(15));
413        assert_eq!(builder.config.connect_timeout, Duration::from_secs(15));
414    }
415
416    #[test]
417    fn test_builder_connect_timeout_secs() {
418        let builder = BitgetBuilder::new().connect_timeout_secs(20);
419        assert_eq!(builder.config.connect_timeout, Duration::from_secs(20));
420    }
421
422    #[test]
423    fn test_builder_recv_window() {
424        let builder = BitgetBuilder::new().recv_window(10000);
425        assert_eq!(builder.options.recv_window, 10000);
426    }
427
428    #[test]
429    fn test_builder_chaining() {
430        let builder = BitgetBuilder::new()
431            .api_key("key")
432            .secret("secret")
433            .passphrase("pass")
434            .sandbox(true)
435            .timeout(Duration::from_secs(30))
436            .recv_window(5000)
437            .product_type("spot")
438            .default_type(DefaultType::Swap)
439            .default_sub_type(DefaultSubType::Linear);
440
441        assert_eq!(
442            builder.config.api_key.as_ref().map(|s| s.expose_secret()),
443            Some("key")
444        );
445        assert_eq!(
446            builder.config.secret.as_ref().map(|s| s.expose_secret()),
447            Some("secret")
448        );
449        assert_eq!(
450            builder.config.password.as_ref().map(|s| s.expose_secret()),
451            Some("pass")
452        );
453        assert!(builder.config.sandbox);
454        assert_eq!(builder.config.timeout, Duration::from_secs(30));
455        assert_eq!(builder.options.recv_window, 5000);
456        assert_eq!(builder.options.product_type, "spot");
457        assert_eq!(builder.options.default_type, DefaultType::Swap);
458        assert_eq!(
459            builder.options.default_sub_type,
460            Some(DefaultSubType::Linear)
461        );
462    }
463
464    #[test]
465    fn test_builder_build() {
466        let result = BitgetBuilder::new().build();
467        assert!(result.is_ok());
468
469        let bitget = result.unwrap();
470        assert_eq!(bitget.id(), "bitget");
471        assert_eq!(bitget.name(), "Bitget");
472    }
473
474    #[test]
475    fn test_builder_build_with_credentials() {
476        let result = BitgetBuilder::new()
477            .api_key("test-key")
478            .secret("test-secret")
479            .passphrase("test-passphrase")
480            .build();
481
482        assert!(result.is_ok());
483    }
484}