1use std::time::Duration;
2
3use reqwest::StatusCode;
4use thiserror::Error;
5
6pub type Result<T> = std::result::Result<T, Error>;
7
8#[derive(Debug, Error)]
9pub enum Error {
10 #[error("http request failed: {0}")]
11 Http(#[source] Box<reqwest_middleware::Error>),
12
13 #[error("websocket request failed: {0}")]
14 WebSocket(#[source] Box<tokio_tungstenite::tungstenite::Error>),
15
16 #[error("failed to deserialize tradingview payload: {0}")]
17 Json(#[from] serde_json::Error),
18
19 #[error("failed to format time value: {0}")]
20 TimeFormat(#[from] time::error::Format),
21
22 #[error("invalid endpoint url: {0}")]
23 UrlParse(#[from] url::ParseError),
24
25 #[error("tradingview returned an API error: {0}")]
26 ApiMessage(String),
27
28 #[error("tradingview returned HTTP {status}: {body}")]
29 ApiStatus { status: StatusCode, body: String },
30
31 #[error("search query cannot be empty")]
32 EmptySearchQuery,
33
34 #[error("scan page limit must be greater than zero")]
35 InvalidPageLimit,
36
37 #[error("history request returned no bars for {symbol}")]
38 HistoryEmpty { symbol: String },
39
40 #[error("scan returned no rows for {symbol}")]
41 SymbolNotFound { symbol: String },
42
43 #[error("scan validation is unavailable: {reason}")]
44 ScanValidationUnavailable { reason: String },
45
46 #[error("scan query uses fields unsupported for {route}: {fields:?}")]
47 UnsupportedScanFields { route: String, fields: Vec<String> },
48
49 #[error("quote session returned no data for {symbol}")]
50 QuoteEmpty { symbol: String },
51
52 #[error("quote session returned status {status} for {symbol}")]
53 QuoteSymbolFailed { symbol: String, status: String },
54
55 #[error("history batch concurrency must be greater than zero")]
56 InvalidBatchConcurrency,
57
58 #[error("history pagination exceeded safe limit for {symbol} after {rounds} rounds")]
59 HistoryPaginationLimitExceeded { symbol: String, rounds: usize },
60
61 #[error("history download failed for {symbol}: {source}")]
62 HistoryDownloadFailed {
63 symbol: String,
64 #[source]
65 source: Box<Error>,
66 },
67
68 #[error("retry min interval {min:?} cannot exceed max interval {max:?}")]
69 InvalidRetryBounds { min: Duration, max: Duration },
70
71 #[error("invalid websocket frame: {0}")]
72 Protocol(&'static str),
73}
74
75impl From<reqwest_middleware::Error> for Error {
76 fn from(value: reqwest_middleware::Error) -> Self {
77 Self::Http(Box::new(value))
78 }
79}
80
81impl From<reqwest::Error> for Error {
82 fn from(value: reqwest::Error) -> Self {
83 let error: reqwest_middleware::Error = value.into();
84 Self::Http(Box::new(error))
85 }
86}
87
88impl From<tokio_tungstenite::tungstenite::Error> for Error {
89 fn from(value: tokio_tungstenite::tungstenite::Error) -> Self {
90 Self::WebSocket(Box::new(value))
91 }
92}