tastytrade 0.4.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
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
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 {}

/// Which tastytrade deployment a request was aimed at.
///
/// Carried on transport failures because "the request failed" means something
/// different depending on whether real money was involved.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Environment {
    /// `api.cert.tastyworks.com`, the sandbox.
    Certification,
    /// `api.tastyworks.com`.
    Production,
}

impl Display for Environment {
    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
        match self {
            Environment::Certification => write!(f, "certification"),
            Environment::Production => write!(f, "production"),
        }
    }
}

/// Everything known about a failed request that is safe to hand to a caller.
///
/// The operation is the endpoint with account identifiers already replaced, so
/// this whole struct can be logged or reported without leaking who it was
/// about.
#[derive(Debug, Clone)]
pub struct RequestContext {
    /// HTTP method.
    pub method: &'static str,
    /// Redacted endpoint, e.g. `/accounts/{account}/balances`.
    pub operation: String,
    /// Which deployment answered.
    pub environment: Environment,
    /// HTTP status, when one was received.
    pub status: Option<u16>,
}

impl Display for RequestContext {
    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
        match self.status {
            Some(status) => write!(
                f,
                "{} {} on {} returned {}",
                self.method, self.operation, self.environment, status
            ),
            None => write!(
                f,
                "{} {} on {}",
                self.method, self.operation, self.environment
            ),
        }
    }
}

/// Represents errors that can occur within the Tastytrade API client.
#[derive(Debug)]
pub enum TastyTradeError {
    /// A request that reached the venue and came back as a failure.
    ///
    /// Carries the sanitised context and, when the venue sent a structured
    /// error document, the broker's own codes. Never carries a response body.
    Request {
        /// Where and what, already sanitised.
        context: RequestContext,
        /// The broker's error document, when it sent one.
        api: Option<ApiError>,
    },
    /// 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),
    /// A precondition this crate enforces was not met.
    ///
    /// Nothing was sent: the request was refused locally because the caller
    /// asked for something the crate will not do on their behalf, such as
    /// placing an order reviewed against a different account or venue.
    Precondition(String),
}

impl Display for TastyTradeError {
    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
        match self {
            TastyTradeError::Request { context, api } => match api {
                Some(api) => write!(f, "{}: {}", context, api.message),
                None => write!(f, "{}", context),
            },
            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),
            TastyTradeError::Precondition(msg) => write!(f, "Precondition not met: {}", msg),
        }
    }
}

impl TastyTradeError {
    /// Whether retrying the same request could plausibly succeed.
    ///
    /// Retryable means transport trouble or a venue that asked to be tried
    /// again: connection failures, timeouts, `408`, `429`, and `5xx` other
    /// than `501`. Everything else is fatal — a `4xx` will keep being a `4xx`,
    /// and a rejected credential does not improve by asking twice.
    ///
    /// This says nothing about whether retrying is *safe*. Order placement is
    /// not idempotent and must never be replayed on this signal alone.
    pub fn is_retryable(&self) -> bool {
        match self {
            TastyTradeError::Request { context, .. } => match context.status {
                Some(408) | Some(429) => true,
                Some(501) => false,
                Some(status) => (500..600).contains(&status),
                None => true,
            },
            TastyTradeError::Http(err) => err.is_timeout() || err.is_connect() || err.is_request(),
            TastyTradeError::Io(_) | TastyTradeError::Connection(_) => true,
            TastyTradeError::WebSocket(_) | TastyTradeError::Streaming(_) => true,
            TastyTradeError::Api(_)
            | TastyTradeError::Json(_)
            | TastyTradeError::DxFeed(_)
            | TastyTradeError::Auth(_)
            | TastyTradeError::Unknown(_)
            | TastyTradeError::ConfigError(_)
            // Nothing was sent, so there is nothing to send again.
            | TastyTradeError::Precondition(_) => false,
        }
    }
}

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 {
            // The broker document is the source when there is one; the
            // context itself is data, not an error.
            Self::Request { api, .. } => api.as_ref().map(|e| e as &(dyn Error + 'static)),
            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,
            Self::Precondition(_) => 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");
    }
}

#[cfg(test)]
mod retryability_tests {
    use super::*;

    fn request_with(status: Option<u16>) -> TastyTradeError {
        TastyTradeError::Request {
            context: RequestContext {
                method: "GET",
                operation: "/accounts/{account}/balances".to_string(),
                environment: Environment::Certification,
                status,
            },
            api: None,
        }
    }

    #[test]
    fn client_errors_are_fatal_and_server_errors_are_not() {
        for status in [400, 401, 403, 404, 422, 501] {
            assert!(
                !request_with(Some(status)).is_retryable(),
                "{status} must not be retried"
            );
        }
        for status in [408, 429, 500, 502, 503, 504] {
            assert!(
                request_with(Some(status)).is_retryable(),
                "{status} should be retryable"
            );
        }
    }

    /// No status means the request never got an answer, which is exactly the
    /// case worth trying again.
    #[test]
    fn a_request_with_no_status_is_retryable() {
        assert!(request_with(None).is_retryable());
    }

    #[test]
    fn configuration_and_authentication_failures_are_never_retryable() {
        assert!(!TastyTradeError::ConfigError("missing".into()).is_retryable());
        assert!(!TastyTradeError::Auth("rejected".into()).is_retryable());
        assert!(!TastyTradeError::Unknown("odd".into()).is_retryable());
    }

    /// The context is the whole point: it says where, against which
    /// deployment, and with what status, without naming the account.
    #[test]
    fn the_context_renders_without_the_account_number() {
        let rendered = request_with(Some(500)).to_string();
        assert!(
            rendered.contains("/accounts/{account}/balances"),
            "{rendered}"
        );
        assert!(rendered.contains("certification"), "{rendered}");
        assert!(rendered.contains("500"), "{rendered}");
    }
}