Skip to main content

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    #![allow(clippy::disallowed_methods)]
321    use super::*;
322
323    #[test]
324    fn test_builder_default() {
325        let builder = BitgetBuilder::new();
326        assert_eq!(builder.config.id, "bitget");
327        assert_eq!(builder.config.name, "Bitget");
328        assert!(!builder.config.sandbox);
329        assert_eq!(builder.options.product_type, "spot");
330    }
331
332    #[test]
333    fn test_builder_api_key() {
334        let builder = BitgetBuilder::new().api_key("test-key");
335        assert_eq!(
336            builder.config.api_key.as_ref().map(|s| s.expose_secret()),
337            Some("test-key")
338        );
339    }
340
341    #[test]
342    fn test_builder_secret() {
343        let builder = BitgetBuilder::new().secret("test-secret");
344        assert_eq!(
345            builder.config.secret.as_ref().map(|s| s.expose_secret()),
346            Some("test-secret")
347        );
348    }
349
350    #[test]
351    fn test_builder_passphrase() {
352        let builder = BitgetBuilder::new().passphrase("test-passphrase");
353        assert_eq!(
354            builder.config.password.as_ref().map(|s| s.expose_secret()),
355            Some("test-passphrase")
356        );
357    }
358
359    #[test]
360    fn test_builder_sandbox() {
361        let builder = BitgetBuilder::new().sandbox(true);
362        assert!(builder.config.sandbox);
363        assert!(builder.options.testnet);
364    }
365
366    #[test]
367    fn test_builder_product_type() {
368        let builder = BitgetBuilder::new().product_type("umcbl");
369        assert_eq!(builder.options.product_type, "umcbl");
370    }
371
372    #[test]
373    fn test_builder_default_type() {
374        let builder = BitgetBuilder::new().default_type(DefaultType::Swap);
375        assert_eq!(builder.options.default_type, DefaultType::Swap);
376    }
377
378    #[test]
379    fn test_builder_default_type_from_string() {
380        let builder = BitgetBuilder::new().default_type("futures");
381        assert_eq!(builder.options.default_type, DefaultType::Futures);
382    }
383
384    #[test]
385    fn test_builder_default_sub_type() {
386        let builder = BitgetBuilder::new().default_sub_type(DefaultSubType::Inverse);
387        assert_eq!(
388            builder.options.default_sub_type,
389            Some(DefaultSubType::Inverse)
390        );
391    }
392
393    #[test]
394    fn test_builder_default_type_and_sub_type() {
395        let builder = BitgetBuilder::new()
396            .default_type(DefaultType::Swap)
397            .default_sub_type(DefaultSubType::Linear);
398        assert_eq!(builder.options.default_type, DefaultType::Swap);
399        assert_eq!(
400            builder.options.default_sub_type,
401            Some(DefaultSubType::Linear)
402        );
403    }
404
405    #[test]
406    fn test_builder_timeout() {
407        let builder = BitgetBuilder::new().timeout(Duration::from_secs(60));
408        assert_eq!(builder.config.timeout, Duration::from_secs(60));
409    }
410
411    #[test]
412    fn test_builder_connect_timeout() {
413        let builder = BitgetBuilder::new().connect_timeout(Duration::from_secs(15));
414        assert_eq!(builder.config.connect_timeout, Duration::from_secs(15));
415    }
416
417    #[test]
418    fn test_builder_connect_timeout_secs() {
419        let builder = BitgetBuilder::new().connect_timeout_secs(20);
420        assert_eq!(builder.config.connect_timeout, Duration::from_secs(20));
421    }
422
423    #[test]
424    fn test_builder_recv_window() {
425        let builder = BitgetBuilder::new().recv_window(10000);
426        assert_eq!(builder.options.recv_window, 10000);
427    }
428
429    #[test]
430    fn test_builder_chaining() {
431        let builder = BitgetBuilder::new()
432            .api_key("key")
433            .secret("secret")
434            .passphrase("pass")
435            .sandbox(true)
436            .timeout(Duration::from_secs(30))
437            .recv_window(5000)
438            .product_type("spot")
439            .default_type(DefaultType::Swap)
440            .default_sub_type(DefaultSubType::Linear);
441
442        assert_eq!(
443            builder.config.api_key.as_ref().map(|s| s.expose_secret()),
444            Some("key")
445        );
446        assert_eq!(
447            builder.config.secret.as_ref().map(|s| s.expose_secret()),
448            Some("secret")
449        );
450        assert_eq!(
451            builder.config.password.as_ref().map(|s| s.expose_secret()),
452            Some("pass")
453        );
454        assert!(builder.config.sandbox);
455        assert_eq!(builder.config.timeout, Duration::from_secs(30));
456        assert_eq!(builder.options.recv_window, 5000);
457        assert_eq!(builder.options.product_type, "spot");
458        assert_eq!(builder.options.default_type, DefaultType::Swap);
459        assert_eq!(
460            builder.options.default_sub_type,
461            Some(DefaultSubType::Linear)
462        );
463    }
464
465    #[test]
466    fn test_builder_build() {
467        let result = BitgetBuilder::new().build();
468        assert!(result.is_ok());
469
470        let bitget = result.unwrap();
471        assert_eq!(bitget.id(), "bitget");
472        assert_eq!(bitget.name(), "Bitget");
473    }
474
475    #[test]
476    fn test_builder_build_with_credentials() {
477        let result = BitgetBuilder::new()
478            .api_key("test-key")
479            .secret("test-secret")
480            .passphrase("test-passphrase")
481            .build();
482
483        assert!(result.is_ok());
484    }
485}