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
use crate::macros::string_enum;
string_enum! {
/// Schwab streamer service identifier.
///
/// Open enum: any wire string Schwab adds later that does not match a known
/// variant decodes into [`Service::Unknown`] with the raw identifier
/// preserved. Consumers can still see the original name and the dispatcher
/// routes such messages to [`DataContent::Raw`](super::DataContent::Raw).
Service {
/// Administrative service (login/logout/heartbeat).
Admin = "ADMIN",
/// Level-one equity quotes.
LevelOneEquities = "LEVELONE_EQUITIES",
/// Level-one option quotes.
LevelOneOptions = "LEVELONE_OPTIONS",
/// Level-one futures quotes.
LevelOneFutures = "LEVELONE_FUTURES",
/// Level-one futures-option quotes.
LevelOneFuturesOptions = "LEVELONE_FUTURES_OPTIONS",
/// Level-one forex quotes.
LevelOneForex = "LEVELONE_FOREX",
/// NYSE order book.
NyseBook = "NYSE_BOOK",
/// Nasdaq order book.
NasdaqBook = "NASDAQ_BOOK",
/// Options order book.
OptionsBook = "OPTIONS_BOOK",
/// Chart bars for equities.
ChartEquity = "CHART_EQUITY",
/// Chart bars for futures.
ChartFutures = "CHART_FUTURES",
/// Equity-screener results.
ScreenerEquity = "SCREENER_EQUITY",
/// Option-screener results.
ScreenerOption = "SCREENER_OPTION",
/// Account activity feed.
AccountActivity = "ACCT_ACTIVITY",
}
}
string_enum! {
/// A command string Schwab sends on a streamer frame.
///
/// Open enum: a command string Schwab adds later that does not match a
/// known variant decodes into [`StreamerCommand::Unknown`] with the raw
/// wire value preserved, so an unrecognized command never fails the whole
/// frame.
StreamerCommand {
/// Session login.
Login = "LOGIN",
/// Subscribe (replace existing subscription).
Subs = "SUBS",
/// Add to an existing subscription.
Add = "ADD",
/// Unsubscribe.
Unsubs = "UNSUBS",
/// Change the field set without re-subscribing.
View = "VIEW",
/// Session logout.
Logout = "LOGOUT",
}
}
/// Status code on a streamer `response` frame, reporting the outcome of the
/// command the frame acknowledges.
///
/// Open enum: a numeric code Schwab adds later that does not match a known
/// variant decodes into [`ResponseCode::Unknown`] with the raw value
/// preserved, so an unrecognized code never fails the whole frame.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Deserialize)]
#[serde(from = "u8")]
#[non_exhaustive]
pub enum ResponseCode {
/// Success.
Ok,
/// Login was denied.
LoginDenied,
/// Unspecified failure.
UnknownFailure,
/// The requested service is unavailable.
ServiceNotAvailable,
/// Schwab is closing the connection.
CloseConnection,
/// Subscription would exceed the symbol limit.
ReachedSymbolLimit,
/// Streaming connection not found on Schwab's side.
StreamConnNotFound,
/// The command frame was malformed.
BadCommandFormat,
/// `SUBS` command failed.
FailedCommandSubs,
/// `UNSUBS` command failed.
FailedCommandUnsubs,
/// `ADD` command failed.
FailedCommandAdd,
/// `VIEW` command failed.
FailedCommandView,
/// `SUBS` command succeeded.
SucceededCommandSubs,
/// `UNSUBS` command succeeded.
SucceededCommandUnsubs,
/// `ADD` command succeeded.
SucceededCommandAdd,
/// `VIEW` command succeeded.
SucceededCommandView,
/// Schwab is asking the client to stop streaming.
StopStreaming,
/// A status code Schwab sent that this crate does not recognize. The
/// raw wire value is preserved so callers can still route on it.
Unknown(u8),
}
impl From<u8> for ResponseCode {
fn from(code: u8) -> Self {
match code {
0 => ResponseCode::Ok,
3 => ResponseCode::LoginDenied,
9 => ResponseCode::UnknownFailure,
11 => ResponseCode::ServiceNotAvailable,
12 => ResponseCode::CloseConnection,
19 => ResponseCode::ReachedSymbolLimit,
20 => ResponseCode::StreamConnNotFound,
21 => ResponseCode::BadCommandFormat,
22 => ResponseCode::FailedCommandSubs,
23 => ResponseCode::FailedCommandUnsubs,
24 => ResponseCode::FailedCommandAdd,
25 => ResponseCode::FailedCommandView,
26 => ResponseCode::SucceededCommandSubs,
27 => ResponseCode::SucceededCommandUnsubs,
28 => ResponseCode::SucceededCommandAdd,
29 => ResponseCode::SucceededCommandView,
30 => ResponseCode::StopStreaming,
other => ResponseCode::Unknown(other),
}
}
}
impl ResponseCode {
/// `true` if the code reports that the acknowledged command succeeded.
/// Every other code, including [`ResponseCode::Unknown`], is a failure
/// or a connection-lifecycle signal the caller must handle.
pub fn is_success(&self) -> bool {
matches!(
self,
ResponseCode::Ok
| ResponseCode::SucceededCommandSubs
| ResponseCode::SucceededCommandUnsubs
| ResponseCode::SucceededCommandAdd
| ResponseCode::SucceededCommandView
)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn known_response_codes_deserialize() {
assert_eq!(
serde_json::from_str::<ResponseCode>("0").unwrap(),
ResponseCode::Ok
);
assert_eq!(
serde_json::from_str::<ResponseCode>("3").unwrap(),
ResponseCode::LoginDenied
);
assert_eq!(
serde_json::from_str::<ResponseCode>("30").unwrap(),
ResponseCode::StopStreaming
);
}
#[test]
fn unknown_response_code_falls_back() {
// A code Schwab assigns after this crate was published must not
// fail the response frame.
assert_eq!(
serde_json::from_str::<ResponseCode>("99").unwrap(),
ResponseCode::Unknown(99)
);
}
#[test]
fn unassigned_codes_in_range_fall_back_to_unknown() {
// Values with no documented meaning (1, 2, 4-8, 10, ...) decode as
// Unknown rather than failing.
assert_eq!(
serde_json::from_str::<ResponseCode>("1").unwrap(),
ResponseCode::Unknown(1)
);
assert_eq!(
serde_json::from_str::<ResponseCode>("10").unwrap(),
ResponseCode::Unknown(10)
);
}
#[test]
fn out_of_range_code_is_a_decode_error() {
// The wire value is a u8; a larger number is a genuine decode
// failure, not an Unknown code.
assert!(serde_json::from_str::<ResponseCode>("256").is_err());
}
#[test]
fn is_success_only_for_ok_and_succeeded_codes() {
assert!(ResponseCode::Ok.is_success());
assert!(ResponseCode::SucceededCommandSubs.is_success());
assert!(ResponseCode::SucceededCommandUnsubs.is_success());
assert!(ResponseCode::SucceededCommandAdd.is_success());
assert!(ResponseCode::SucceededCommandView.is_success());
assert!(!ResponseCode::LoginDenied.is_success());
assert!(!ResponseCode::FailedCommandSubs.is_success());
assert!(!ResponseCode::ServiceNotAvailable.is_success());
assert!(!ResponseCode::StopStreaming.is_success());
assert!(!ResponseCode::Unknown(99).is_success());
}
#[test]
fn known_streamer_commands_round_trip() {
for cmd in [
StreamerCommand::Login,
StreamerCommand::Subs,
StreamerCommand::Add,
StreamerCommand::Unsubs,
StreamerCommand::View,
StreamerCommand::Logout,
] {
let json = serde_json::to_string(&cmd).unwrap();
let back: StreamerCommand = serde_json::from_str(&json).unwrap();
assert_eq!(cmd, back);
}
}
#[test]
fn unknown_streamer_command_falls_back() {
// A command string Schwab adds after this crate was published must
// not fail the response frame; the raw value is preserved so
// callers can route on it.
let parsed: StreamerCommand = serde_json::from_str(r#""SOMETHING_NEW""#).unwrap();
assert_eq!(parsed, StreamerCommand::Unknown("SOMETHING_NEW".into()));
// Serializes back to the same raw value.
assert_eq!(
serde_json::to_string(&parsed).unwrap(),
r#""SOMETHING_NEW""#
);
}
}