tastytrade 0.2.2

Library for trading through tastytrade's API
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
use pretty_simple_display::{DebugPretty, DisplaySimple};
use serde::{Deserialize, Serialize};
use std::error::Error;
use std::fmt::{self, Display, Formatter};
use std::io;

/// Represents errors that can occur during interactions with DxFeed.
///
/// This enum provides variants for different types of errors related to DxFeed.
/// Currently, it only includes a variant for connection errors.
#[derive(DebugPretty, DisplaySimple, Serialize)]
pub enum DxFeedError {
    /// Represents an error encountered while creating a connection to DxFeed.
    /// This can occur due to various reasons, such as network issues or invalid
    /// connection parameters.
    CreateConnectionError,
}

impl Error for DxFeedError {}

/// Represents an error returned by the Tastytrade API.
///
/// This struct provides detailed information about errors encountered when interacting with the Tastytrade API.  It includes an optional error code, a human-readable error message, and an optional list of inner errors for more specific diagnostic information.
#[derive(DebugPretty, DisplaySimple, Serialize, Deserialize)]
pub struct ApiError {
    /// An optional error code. This can be used for programmatic identification of specific errors.
    pub code: Option<String>,
    /// A human-readable error message. This provides a description of the error that occurred.
    pub message: String,
    /// An optional list of inner errors. These provide more detailed information about the error, such as specific validation failures.
    pub errors: Option<Vec<InnerApiError>>,
}

/// Represents an inner API error.  This struct is typically nested within a top-level `ApiError` to provide more detailed error information.
#[derive(DebugPretty, DisplaySimple, Serialize, Deserialize)]
pub struct InnerApiError {
    /// An optional error code.  This can be used for programmatic identification of specific errors.
    pub code: Option<String>,
    /// A human-readable error message.  This provides a description of the error that occurred.
    pub message: String,
}

impl Error for ApiError {}

/// Represents errors that can occur within the Tastytrade API client.
#[derive(Debug)]
pub enum TastyTradeError {
    /// Represents an error returned from the Tastytrade API.  This variant contains an `ApiError` struct, which provides details about the API error, including an error code and message.
    Api(ApiError),
    /// Represents an HTTP error during communication with the Tastytrade API.  This variant wraps a `reqwest::Error`, which provides details about the underlying HTTP error.
    Http(reqwest::Error),
    /// Represents an error during JSON serialization or deserialization.  This variant wraps a `serde_json::Error`, which provides details about the JSON error.
    Json(serde_json::Error),
    /// Represents an error originating from the DxFeed data stream.  This variant contains a `DxFeedError` enum, which provides details about the specific DxFeed error.
    DxFeed(DxFeedError),
    /// Represents an error that occurred during WebSocket communication, often related to real-time data streaming. This variant wraps a `tokio_tungstenite::tungstenite::Error`, providing details about the WebSocket error.
    WebSocket(Box<tokio_tungstenite::tungstenite::Error>),
    /// Represents an I/O error. This variant wraps a standard `io::Error`, providing details about the I/O operation that failed.
    Io(io::Error),
    /// Represents an authentication error. This variant contains a `String` describing the authentication failure.
    Auth(String),
    /// Represents a connection error, typically during the initial connection establishment phase.  This variant contains a `String` describing the connection failure.
    Connection(String),
    /// Represents an error related to real-time data streaming after a successful connection. This variant contains a `String` describing the streaming error.
    Streaming(String),
    /// Represents an unknown or unexpected error. This variant contains a `String` describing the error.
    Unknown(String),
    /// Represents an error within the client configuration. This variant contains a `String` describing the configuration error.
    ConfigError(String),
}

impl Display for TastyTradeError {
    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
        match self {
            TastyTradeError::Api(err) => write!(f, "API error: {}", err),
            TastyTradeError::Http(err) => write!(f, "HTTP error: {}", err),
            TastyTradeError::Json(err) => write!(f, "JSON error: {}", err),
            TastyTradeError::DxFeed(err) => write!(f, "DxFeed error: {}", err),
            TastyTradeError::WebSocket(err) => write!(f, "WebSocket error: {}", err),
            TastyTradeError::Io(err) => write!(f, "I/O error: {}", err),
            TastyTradeError::Auth(msg) => write!(f, "Authentication failed: {}", msg),
            TastyTradeError::Connection(msg) => write!(f, "Connection error: {}", msg),
            TastyTradeError::Streaming(msg) => write!(f, "Streaming error: {}", msg),
            TastyTradeError::Unknown(msg) => write!(f, "Unknown error: {}", msg),
            TastyTradeError::ConfigError(msg) => write!(f, "Configuration error: {}", msg),
        }
    }
}

impl Error for TastyTradeError {
    /// Returns the underlying source of the error if available.
    ///
    /// Some errors, such as `Auth`, `Connection`, `Streaming`, `Unknown`, and `ConfigError` do not have
    /// an underlying source error.  This is because these errors are generated internally within the
    /// library and do not wrap external errors.  In these cases, this function will return `None`.
    ///
    /// For errors that wrap an external error, such as `Api`, `Http`, `Json`, `DxFeed`, `WebSocket`, and `Io`,
    /// this function will return a reference to the underlying error as a trait object `&(dyn Error + 'static)`.
    /// This allows access to the original error information for debugging and handling.
    ///
    /// # Examples
    ///
    /// ```
    /// use std::error::Error;
    /// use tastytrade::TastyTradeError;
    /// use std::io;
    ///
    /// let error = TastyTradeError::Io(io::Error::new(io::ErrorKind::Other, "IO error"));
    ///
    /// if let Some(source) = error.source() {
    ///     println!("Source error: {}", source);
    /// } else {
    ///     println!("No source error available.");
    /// }
    ///
    /// let error = TastyTradeError::Auth("Authentication failed".to_string());
    ///
    /// if let Some(source) = error.source() {
    ///     println!("Source error: {}", source);
    /// } else {
    ///     println!("No source error available.");
    /// }
    /// ```
    fn source(&self) -> Option<&(dyn Error + 'static)> {
        match self {
            Self::Api(err) => Some(err),
            Self::Http(err) => Some(err),
            Self::Json(err) => Some(err),
            Self::DxFeed(err) => Some(err),
            Self::WebSocket(err) => Some(err.as_ref()),
            Self::Io(err) => Some(err),
            Self::Auth(_) => None,
            Self::Connection(_) => None,
            Self::Streaming(_) => None,
            Self::Unknown(_) => None,
            Self::ConfigError(_) => None,
        }
    }
}

impl From<ApiError> for TastyTradeError {
    /// Converts an `ApiError` into a `TastyTradeError`.
    ///
    /// This function implements the `From` trait for converting an `ApiError`
    /// into a `TastyTradeError::Api` variant. This allows for seamless error
    /// propagation and handling within the library.
    ///
    /// # Arguments
    ///
    /// * `err` - The `ApiError` to be converted.
    ///
    /// # Returns
    ///
    /// A `TastyTradeError::Api` containing the provided `ApiError`.
    ///
    /// # Example
    ///
    /// ```
    /// use tastytrade::{ApiError, TastyTradeError};
    ///
    /// let api_error = ApiError {
    ///     code: Some("400".to_string()),
    ///     message: "Bad Request".to_string(),
    ///     errors: None,
    /// };
    ///
    /// let tasty_error: TastyTradeError = api_error.into();
    ///
    /// assert!(matches!(tasty_error, TastyTradeError::Api(_)));
    /// ```
    fn from(err: ApiError) -> Self {
        Self::Api(err)
    }
}

impl From<reqwest::Error> for TastyTradeError {
    /// Converts a `reqwest::Error` into a `TastyTradeError`.
    ///
    /// This function maps a `reqwest::Error`, which represents an error during an HTTP request,
    /// into a `TastyTradeError::Http` variant. This allows the library to handle HTTP errors
    /// consistently with other error types within the `TastyTradeError` enum.
    ///
    fn from(err: reqwest::Error) -> Self {
        Self::Http(err)
    }
}

impl From<serde_json::Error> for TastyTradeError {
    /// Converts a `serde_json::Error` into a `TastyTradeError`.
    ///
    /// This function maps a serialization/deserialization error from the `serde_json`
    /// crate into a `TastyTradeError::Json` variant.  This allows for consistent error
    /// handling within the `tastytrade` crate, wrapping external errors into the crate's
    /// own error type.
    fn from(err: serde_json::Error) -> Self {
        Self::Json(err)
    }
}

impl From<DxFeedError> for TastyTradeError {
    /// Converts a `DxFeedError` into a `TastyTradeError`.
    ///
    /// This function maps a `DxFeedError` to the `DxFeed` variant of the `TastyTradeError` enum.  This allows for consistent error handling across the library by representing errors from the DxFeed library within the TastyTrade error handling framework.
    ///
    /// # Arguments
    ///
    /// * `err` - The `DxFeedError` to be converted.
    ///
    /// # Returns
    ///
    /// * A `TastyTradeError` representing the provided `DxFeedError`.
    ///
    /// # Example
    ///
    /// ```
    /// use tastytrade::{DxFeedError, TastyTradeError};
    ///
    /// let dxfeed_error = DxFeedError::CreateConnectionError;
    /// let tastytrade_error = TastyTradeError::from(dxfeed_error);
    ///
    /// assert!(matches!(tastytrade_error, TastyTradeError::DxFeed(_)));
    /// ```
    fn from(err: DxFeedError) -> Self {
        Self::DxFeed(err)
    }
}

impl From<tokio_tungstenite::tungstenite::Error> for TastyTradeError {
    /// Converts a `tokio_tungstenite::tungstenite::Error` into a `TastyTradeError`.
    /// This function maps a WebSocket error from the underlying `tungstenite` crate
    /// to a `TastyTradeError::WebSocket` variant, allowing for consistent error
    /// handling throughout the library.
    ///
    /// # Arguments
    ///
    /// * `err` - The `tokio_tungstenite::tungstenite::Error` to be converted.
    ///
    /// # Returns
    ///
    /// * A `TastyTradeError::WebSocket` containing the original `tungstenite` error.
    ///
    /// # Example
    ///
    /// ```
    /// use tastytrade::TastyTradeError;
    /// use tokio_tungstenite::tungstenite::Error;
    ///
    /// let ws_error = Error::ConnectionClosed; // Example tungstenite error
    /// let tasty_error = TastyTradeError::from(ws_error);
    ///
    /// assert!(matches!(tasty_error, TastyTradeError::WebSocket(_)));
    /// ```
    fn from(err: tokio_tungstenite::tungstenite::Error) -> Self {
        Self::WebSocket(Box::new(err))
    }
}

impl From<io::Error> for TastyTradeError {
    /// Converts an [`io::Error`] into a [`TastyTradeError`].
    ///
    /// This function maps an [`io::Error`] to the `Io` variant of the
    /// [`TastyTradeError`] enum, allowing for consistent error handling
    /// within the library.  This is typically used in situations where
    /// I/O operations might fail, such as reading from files or network
    /// connections, and those errors need to be propagated up as
    /// [`TastyTradeError`]s.
    ///
    /// # Example
    ///
    /// ```
    /// use tastytrade::TastyTradeError;
    /// use std::io;
    ///
    /// let io_error = io::Error::new(io::ErrorKind::Other, "An I/O error occurred");
    /// let tasty_error = TastyTradeError::from(io_error);
    ///
    /// assert!(matches!(tasty_error, TastyTradeError::Io(_)));
    /// ```
    fn from(err: io::Error) -> Self {
        Self::Io(err)
    }
}

impl From<dxlink::DXLinkError> for TastyTradeError {
    /// Converts a `dxlink::DXLinkError` into a `TastyTradeError`.
    ///
    /// This function maps the various error types from the `dxlink` crate
    /// to the corresponding variants of the `TastyTradeError` enum.  This
    /// allows for consistent error handling throughout the library.
    fn from(err: dxlink::DXLinkError) -> Self {
        match err {
            dxlink::DXLinkError::Authentication(e) => Self::Auth(e),
            dxlink::DXLinkError::Connection(e) => Self::Connection(e),
            dxlink::DXLinkError::WebSocket(e) => {
                // Convert tungstenite 0.26 error to 0.27 error by creating a new error with the same message
                let converted_error = tokio_tungstenite::tungstenite::Error::Io(
                    std::io::Error::other(format!("WebSocket error: {}", e)),
                );
                Self::WebSocket(Box::new(converted_error))
            }
            dxlink::DXLinkError::Serialization(e) => Self::Json(e),
            _ => Self::Streaming(format!("DXLink error: {}", err)),
        }
    }
}

impl TastyTradeError {
    /// Creates a new `TastyTradeError` of the `Auth` variant.
    ///
    /// This function is used to create an error specifically related to authentication issues, such as invalid credentials.
    /// It takes a message string as input, which is converted into a `String` and stored within the `Auth` variant of the `TastyTradeError` enum.
    ///
    /// # Examples
    ///
    /// ```
    /// use tastytrade::TastyTradeError;
    ///
    /// let error = TastyTradeError::auth_error("Invalid username or password");
    ///
    /// assert!(matches!(error, TastyTradeError::Auth(_)));
    /// ```
    pub fn auth_error(msg: impl Into<String>) -> Self {
        Self::Auth(msg.into())
    }

    /// Creates a new `TastyTradeError` of the `Connection` variant.
    ///
    /// This function is used to create an error specifically related to connection issues, such as network failures or inability to reach the Tastytrade API.
    /// It takes a message string as input, which is converted into a `String` and stored within the `Connection` variant of the `TastyTradeError` enum.
    ///
    /// # Examples
    ///
    /// ```
    /// use tastytrade::TastyTradeError;
    ///
    /// let error = TastyTradeError::connection_error("Failed to connect to the server");
    ///
    /// assert!(matches!(error, TastyTradeError::Connection(_)));
    /// ```
    pub fn connection_error(msg: impl Into<String>) -> Self {
        Self::Connection(msg.into())
    }

    /// Creates a new `TastyTradeError` of the `Streaming` variant.
    ///
    /// This function is used to create an error specifically related to streaming data issues, such as disconnections or data parsing errors. It takes a message string as input, which is converted into a `String` and stored within the `Streaming` variant of the `TastyTradeError` enum.
    ///
    /// # Examples
    ///
    /// ```
    /// use tastytrade::TastyTradeError;
    ///
    /// let error = TastyTradeError::streaming_error("Streaming connection lost");
    ///
    /// assert!(matches!(error, TastyTradeError::Streaming(_)));
    /// ```
    pub fn streaming_error(msg: impl Into<String>) -> Self {
        Self::Streaming(msg.into())
    }

    /// Creates a new `TastyTradeError` of the `Unknown` variant.
    ///
    /// This function is used to create an error representing an unknown or unexpected error condition.  It takes a message string as input, which is converted into a `String` and stored within the `Unknown` variant of the `TastyTradeError` enum.
    ///
    /// # Examples
    ///
    /// ```
    /// use tastytrade::TastyTradeError;
    ///
    /// let error = TastyTradeError::unknown_error("Something went wrong");
    ///
    /// assert!(matches!(error, TastyTradeError::Unknown(_)));
    /// ```
    pub fn unknown_error(msg: impl Into<String>) -> Self {
        Self::Unknown(msg.into())
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::io;

    #[test]
    fn test_dxfeed_error_display() {
        let error = DxFeedError::CreateConnectionError;
        let display_str = format!("{}", error);
        assert!(display_str.contains("CreateConnectionError"));
    }

    #[test]
    fn test_api_error_display() {
        let api_error = ApiError {
            code: Some("TEST_CODE".to_string()),
            message: "Test message".to_string(),
            errors: None,
        };
        let display_str = format!("{}", api_error);
        assert!(display_str.contains("TEST_CODE"));
        assert!(display_str.contains("Test message"));
    }

    #[test]
    fn test_api_error_display_no_code() {
        let api_error = ApiError {
            code: None,
            message: "Test message without code".to_string(),
            errors: None,
        };
        let display_str = format!("{}", api_error);
        assert!(display_str.contains("Test message without code"));
    }

    #[test]
    fn test_tastytrade_error_display_variants() {
        let api_error = ApiError {
            code: Some("API_ERROR".to_string()),
            message: "API error message".to_string(),
            errors: None,
        };

        let test_cases = vec![
            (TastyTradeError::Api(api_error), "API error"),
            (
                TastyTradeError::Auth("Auth failed".to_string()),
                "Authentication failed",
            ),
            (
                TastyTradeError::Connection("Connection failed".to_string()),
                "Connection error",
            ),
            (
                TastyTradeError::Streaming("Stream failed".to_string()),
                "Streaming error",
            ),
            (
                TastyTradeError::Unknown("Unknown error".to_string()),
                "Unknown error",
            ),
            (
                TastyTradeError::ConfigError("Config error".to_string()),
                "Configuration error",
            ),
        ];

        for (error, expected_prefix) in test_cases {
            let display_str = format!("{}", error);
            assert!(
                display_str.contains(expected_prefix),
                "Error '{}' should contain '{}'",
                display_str,
                expected_prefix
            );
        }
    }

    #[test]
    fn test_from_api_error() {
        let api_error = ApiError {
            code: Some("TEST".to_string()),
            message: "Test message".to_string(),
            errors: None,
        };
        let tastytrade_error = TastyTradeError::from(api_error);

        match tastytrade_error {
            TastyTradeError::Api(err) => {
                assert_eq!(err.code, Some("TEST".to_string()));
                assert_eq!(err.message, "Test message");
            }
            _ => panic!("Expected Api variant"),
        }
    }

    #[test]
    fn test_from_serde_json_error() {
        let json_error = serde_json::from_str::<serde_json::Value>("invalid json").unwrap_err();
        let tastytrade_error = TastyTradeError::from(json_error);

        match tastytrade_error {
            TastyTradeError::Json(_) => {} // Success
            _ => panic!("Expected Json variant"),
        }
    }

    #[test]
    fn test_from_io_error() {
        let io_error = io::Error::new(io::ErrorKind::NotFound, "File not found");
        let tastytrade_error = TastyTradeError::from(io_error);

        match tastytrade_error {
            TastyTradeError::Io(_) => {} // Success
            _ => panic!("Expected Io variant"),
        }
    }

    #[test]
    fn test_from_dxfeed_error() {
        let dxfeed_error = DxFeedError::CreateConnectionError;
        let tastytrade_error = TastyTradeError::from(dxfeed_error);

        match tastytrade_error {
            TastyTradeError::DxFeed(DxFeedError::CreateConnectionError) => {} // Success
            _ => panic!("Expected DxFeed variant"),
        }
    }

    #[test]
    fn test_constructor_methods() {
        let auth_error = TastyTradeError::auth_error("Invalid credentials");
        match auth_error {
            TastyTradeError::Auth(msg) => assert_eq!(msg, "Invalid credentials"),
            _ => panic!("Expected Auth variant"),
        }

        let connection_error = TastyTradeError::connection_error("Connection timeout");
        match connection_error {
            TastyTradeError::Connection(msg) => assert_eq!(msg, "Connection timeout"),
            _ => panic!("Expected Connection variant"),
        }

        let streaming_error = TastyTradeError::streaming_error("Stream interrupted");
        match streaming_error {
            TastyTradeError::Streaming(msg) => assert_eq!(msg, "Stream interrupted"),
            _ => panic!("Expected Streaming variant"),
        }

        let unknown_error = TastyTradeError::unknown_error("Something went wrong");
        match unknown_error {
            TastyTradeError::Unknown(msg) => assert_eq!(msg, "Something went wrong"),
            _ => panic!("Expected Unknown variant"),
        }
    }

    #[test]
    fn test_error_source() {
        let api_error = ApiError {
            code: Some("TEST".to_string()),
            message: "Test message".to_string(),
            errors: None,
        };
        let tastytrade_error = TastyTradeError::Api(api_error);

        // Test that source() returns Some for Api errors
        assert!(tastytrade_error.source().is_some());

        // Test that source() returns None for string-based errors
        let auth_error = TastyTradeError::Auth("Auth failed".to_string());
        assert!(auth_error.source().is_none());
    }

    #[test]
    fn test_inner_api_error() {
        let inner_error = InnerApiError {
            code: Some("INNER_CODE".to_string()),
            message: "Inner error message".to_string(),
        };

        assert_eq!(inner_error.code, Some("INNER_CODE".to_string()));
        assert_eq!(inner_error.message, "Inner error message");
    }

    #[test]
    fn test_api_error_with_inner_errors() {
        let inner_error = InnerApiError {
            code: Some("VALIDATION_ERROR".to_string()),
            message: "Field is required".to_string(),
        };

        let api_error = ApiError {
            code: Some("BAD_REQUEST".to_string()),
            message: "Request validation failed".to_string(),
            errors: Some(vec![inner_error]),
        };

        assert_eq!(api_error.code, Some("BAD_REQUEST".to_string()));
        assert_eq!(api_error.message, "Request validation failed");
        assert!(api_error.errors.is_some());

        let inner_errors = api_error.errors.unwrap();
        assert_eq!(inner_errors.len(), 1);
        assert_eq!(inner_errors[0].code, Some("VALIDATION_ERROR".to_string()));
        assert_eq!(inner_errors[0].message, "Field is required");
    }
}