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