1use std::{
2 cmp::Ordering,
3 fmt::Display,
4 ops::{Deref, DerefMut},
5};
6
7use derive_more::derive::From;
8use itertools::Itertools as _;
9use serde::{de::Error, Deserialize, Serialize};
10use utoipa::ToSchema;
11
12use crate::{
13 payload::AggregatedPriceFeedData,
14 time::{DurationUs, FixedRate, TimestampUs},
15 ChannelId, Price, PriceFeedId, PriceFeedProperty, Rate,
16};
17
18#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, ToSchema)]
19#[serde(rename_all = "camelCase")]
20pub struct LatestPriceRequestRepr {
21 #[schema(example = json!([1]))]
23 pub price_feed_ids: Option<Vec<PriceFeedId>>,
24 #[schema(example = schema_default_symbols)]
25 pub symbols: Option<Vec<String>>,
26 pub properties: Vec<PriceFeedProperty>,
27 #[serde(alias = "chains")]
29 pub formats: Vec<Format>,
30 #[serde(default)]
31 pub json_binary_encoding: JsonBinaryEncoding,
32 #[serde(default = "default_parsed")]
35 pub parsed: bool,
36 pub channel: Channel,
37 #[serde(default = "default_market_sessions")]
38 pub market_sessions: Vec<MarketSession>,
39}
40
41#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, ToSchema)]
42#[serde(rename_all = "camelCase")]
43pub struct LatestPriceRequest(LatestPriceRequestRepr);
44
45impl<'de> Deserialize<'de> for LatestPriceRequest {
46 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
47 where
48 D: serde::Deserializer<'de>,
49 {
50 let value = LatestPriceRequestRepr::deserialize(deserializer)?;
51 Self::new(value).map_err(Error::custom)
52 }
53}
54
55impl LatestPriceRequest {
56 pub fn new(value: LatestPriceRequestRepr) -> Result<Self, &'static str> {
57 validate_price_feed_ids_or_symbols(&value.price_feed_ids, &value.symbols)?;
58 validate_optional_nonempty_vec_has_unique_elements(
59 &value.price_feed_ids,
60 "no price feed ids specified",
61 "duplicate price feed ids specified",
62 )?;
63 validate_optional_nonempty_vec_has_unique_elements(
64 &value.symbols,
65 "no symbols specified",
66 "duplicate symbols specified",
67 )?;
68 validate_formats(&value.formats)?;
69 validate_properties(&value.properties)?;
70 Ok(Self(value))
71 }
72}
73
74impl Deref for LatestPriceRequest {
75 type Target = LatestPriceRequestRepr;
76
77 fn deref(&self) -> &Self::Target {
78 &self.0
79 }
80}
81impl DerefMut for LatestPriceRequest {
82 fn deref_mut(&mut self) -> &mut Self::Target {
83 &mut self.0
84 }
85}
86
87#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, ToSchema)]
88#[serde(rename_all = "camelCase")]
89pub struct PriceRequestRepr {
90 pub timestamp: TimestampUs,
91 pub price_feed_ids: Option<Vec<PriceFeedId>>,
93 #[schema(default)]
94 pub symbols: Option<Vec<String>>,
95 pub properties: Vec<PriceFeedProperty>,
96 pub formats: Vec<Format>,
97 #[serde(default)]
98 pub json_binary_encoding: JsonBinaryEncoding,
99 #[serde(default = "default_parsed")]
102 pub parsed: bool,
103 pub channel: Channel,
104 #[serde(default = "default_market_sessions")]
105 pub market_sessions: Vec<MarketSession>,
106}
107
108#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, ToSchema)]
109#[serde(rename_all = "camelCase")]
110pub struct PriceRequest(PriceRequestRepr);
111
112impl<'de> Deserialize<'de> for PriceRequest {
113 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
114 where
115 D: serde::Deserializer<'de>,
116 {
117 let value = PriceRequestRepr::deserialize(deserializer)?;
118 Self::new(value).map_err(Error::custom)
119 }
120}
121
122impl PriceRequest {
123 pub fn new(value: PriceRequestRepr) -> Result<Self, &'static str> {
124 validate_price_feed_ids_or_symbols(&value.price_feed_ids, &value.symbols)?;
125 validate_optional_nonempty_vec_has_unique_elements(
126 &value.price_feed_ids,
127 "no price feed ids specified",
128 "duplicate price feed ids specified",
129 )?;
130 validate_optional_nonempty_vec_has_unique_elements(
131 &value.symbols,
132 "no symbols specified",
133 "duplicate symbols specified",
134 )?;
135 validate_formats(&value.formats)?;
136 validate_properties(&value.properties)?;
137 Ok(Self(value))
138 }
139}
140
141impl Deref for PriceRequest {
142 type Target = PriceRequestRepr;
143
144 fn deref(&self) -> &Self::Target {
145 &self.0
146 }
147}
148impl DerefMut for PriceRequest {
149 fn deref_mut(&mut self) -> &mut Self::Target {
150 &mut self.0
151 }
152}
153
154#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, ToSchema)]
155#[serde(rename_all = "camelCase")]
156pub struct ReducePriceRequest {
157 pub payload: JsonUpdate,
158 pub price_feed_ids: Vec<PriceFeedId>,
159}
160
161pub type LatestPriceResponse = JsonUpdate;
162pub type ReducePriceResponse = JsonUpdate;
163pub type PriceResponse = JsonUpdate;
164
165pub fn default_parsed() -> bool {
166 true
167}
168
169pub fn default_market_sessions() -> Vec<MarketSession> {
170 vec![
171 MarketSession::Regular,
172 MarketSession::PreMarket,
173 MarketSession::PostMarket,
174 MarketSession::OverNight,
175 MarketSession::Closed,
176 ]
177}
178
179pub fn schema_default_symbols() -> Option<Vec<String>> {
180 None
181}
182pub fn schema_default_price_feed_ids() -> Option<Vec<PriceFeedId>> {
183 Some(vec![PriceFeedId(1)])
184}
185
186#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Serialize, Deserialize, ToSchema)]
187#[serde(rename_all = "camelCase")]
188pub enum DeliveryFormat {
189 #[default]
191 Json,
192 Binary,
194}
195
196#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, ToSchema)]
197#[serde(rename_all = "camelCase")]
198pub enum Format {
199 Evm,
200 Solana,
201 LeEcdsa,
202 LeUnsigned,
203}
204
205#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Serialize, Deserialize, ToSchema)]
206#[serde(rename_all = "camelCase")]
207pub enum JsonBinaryEncoding {
208 #[default]
209 Base64,
210 Hex,
211}
212
213#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, From, ToSchema)]
214#[schema(example = "fixed_rate@200ms")]
215pub enum Channel {
216 FixedRate(FixedRate),
217 #[schema(rename = "real_time")]
218 RealTime,
219}
220
221impl PartialOrd for Channel {
222 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
223 let rate_left = match self {
224 Channel::FixedRate(rate) => rate.duration().as_micros(),
225 Channel::RealTime => FixedRate::MIN.duration().as_micros(),
226 };
227 let rate_right = match other {
228 Channel::FixedRate(rate) => rate.duration().as_micros(),
229 Channel::RealTime => FixedRate::MIN.duration().as_micros(),
230 };
231 Some(rate_left.cmp(&rate_right))
232 }
233}
234
235impl Serialize for Channel {
236 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
237 where
238 S: serde::Serializer,
239 {
240 match self {
241 Channel::FixedRate(fixed_rate) => serializer.serialize_str(&format!(
242 "fixed_rate@{}ms",
243 fixed_rate.duration().as_millis()
244 )),
245 Channel::RealTime => serializer.serialize_str("real_time"),
246 }
247 }
248}
249
250impl Display for Channel {
251 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
252 match self {
253 Channel::FixedRate(fixed_rate) => {
254 write!(f, "fixed_rate@{}ms", fixed_rate.duration().as_millis())
255 }
256 Channel::RealTime => write!(f, "real_time"),
257 }
258 }
259}
260
261impl Channel {
262 pub fn id(&self) -> ChannelId {
263 match self {
264 Channel::FixedRate(fixed_rate) => match fixed_rate.duration().as_millis() {
265 50 => ChannelId::FIXED_RATE_50,
266 200 => ChannelId::FIXED_RATE_200,
267 1000 => ChannelId::FIXED_RATE_1000,
268 _ => panic!("unknown channel: {self:?}"),
269 },
270 Channel::RealTime => ChannelId::REAL_TIME,
271 }
272 }
273}
274
275#[test]
276fn id_supports_all_fixed_rates() {
277 for rate in FixedRate::ALL {
278 Channel::FixedRate(rate).id();
279 }
280}
281
282fn parse_channel(value: &str) -> Option<Channel> {
283 if value == "real_time" {
284 Some(Channel::RealTime)
285 } else if let Some(rest) = value.strip_prefix("fixed_rate@") {
286 let ms_value = rest.strip_suffix("ms")?;
287 Some(Channel::FixedRate(FixedRate::from_millis(
288 ms_value.parse().ok()?,
289 )?))
290 } else {
291 None
292 }
293}
294
295impl<'de> Deserialize<'de> for Channel {
296 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
297 where
298 D: serde::Deserializer<'de>,
299 {
300 let value = <String>::deserialize(deserializer)?;
301 parse_channel(&value).ok_or_else(|| Error::custom("unknown channel"))
302 }
303}
304
305#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, ToSchema)]
306#[serde(rename_all = "camelCase")]
307pub struct SubscriptionParamsRepr {
308 pub price_feed_ids: Option<Vec<PriceFeedId>>,
310 #[schema(default)]
311 pub symbols: Option<Vec<String>>,
312 pub properties: Vec<PriceFeedProperty>,
313 #[serde(alias = "chains")]
315 pub formats: Vec<Format>,
316 #[serde(default)]
317 pub delivery_format: DeliveryFormat,
318 #[serde(default)]
319 pub json_binary_encoding: JsonBinaryEncoding,
320 #[serde(default = "default_parsed")]
323 pub parsed: bool,
324 pub channel: Channel,
325 #[serde(default, alias = "ignoreInvalidFeedIds")]
327 pub ignore_invalid_feeds: bool,
328 #[serde(default = "default_market_sessions")]
330 pub market_sessions: Vec<MarketSession>,
331}
332
333#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, ToSchema)]
334#[serde(rename_all = "camelCase")]
335pub struct SubscriptionParams(SubscriptionParamsRepr);
336
337impl<'de> Deserialize<'de> for SubscriptionParams {
338 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
339 where
340 D: serde::Deserializer<'de>,
341 {
342 let value = SubscriptionParamsRepr::deserialize(deserializer)?;
343 Self::new(value).map_err(Error::custom)
344 }
345}
346
347impl SubscriptionParams {
348 pub fn new(value: SubscriptionParamsRepr) -> Result<Self, &'static str> {
349 validate_price_feed_ids_or_symbols(&value.price_feed_ids, &value.symbols)?;
350 validate_optional_nonempty_vec_has_unique_elements(
351 &value.price_feed_ids,
352 "no price feed ids specified",
353 "duplicate price feed ids specified",
354 )?;
355 validate_optional_nonempty_vec_has_unique_elements(
356 &value.symbols,
357 "no symbols specified",
358 "duplicate symbols specified",
359 )?;
360 validate_formats(&value.formats)?;
361 validate_properties(&value.properties)?;
362 Ok(Self(value))
363 }
364}
365
366impl Deref for SubscriptionParams {
367 type Target = SubscriptionParamsRepr;
368
369 fn deref(&self) -> &Self::Target {
370 &self.0
371 }
372}
373impl DerefMut for SubscriptionParams {
374 fn deref_mut(&mut self) -> &mut Self::Target {
375 &mut self.0
376 }
377}
378
379#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, ToSchema)]
380#[serde(rename_all = "camelCase")]
381pub struct JsonBinaryData {
382 pub encoding: JsonBinaryEncoding,
383 pub data: String,
384}
385
386#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, ToSchema)]
387#[serde(rename_all = "camelCase")]
388pub struct JsonUpdate {
389 #[serde(skip_serializing_if = "Option::is_none")]
391 pub parsed: Option<ParsedPayload>,
392 #[serde(skip_serializing_if = "Option::is_none")]
394 pub evm: Option<JsonBinaryData>,
395 #[serde(skip_serializing_if = "Option::is_none")]
397 pub solana: Option<JsonBinaryData>,
398 #[serde(skip_serializing_if = "Option::is_none")]
400 pub le_ecdsa: Option<JsonBinaryData>,
401 #[serde(skip_serializing_if = "Option::is_none")]
403 pub le_unsigned: Option<JsonBinaryData>,
404}
405
406#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, ToSchema)]
407#[serde(rename_all = "camelCase")]
408pub struct ParsedPayload {
409 #[serde(with = "crate::serde_str::timestamp")]
410 pub timestamp_us: TimestampUs,
411 pub price_feeds: Vec<ParsedFeedPayload>,
412}
413
414#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, ToSchema)]
415#[serde(rename_all = "camelCase")]
416pub struct ParsedFeedPayload {
417 pub price_feed_id: PriceFeedId,
418 #[serde(skip_serializing_if = "Option::is_none")]
419 #[serde(with = "crate::serde_str::option_price")]
420 #[serde(default)]
421 pub price: Option<Price>,
422 #[serde(skip_serializing_if = "Option::is_none")]
423 #[serde(with = "crate::serde_str::option_price")]
424 #[serde(default)]
425 pub best_bid_price: Option<Price>,
426 #[serde(skip_serializing_if = "Option::is_none")]
427 #[serde(with = "crate::serde_str::option_price")]
428 #[serde(default)]
429 pub best_ask_price: Option<Price>,
430 #[serde(skip_serializing_if = "Option::is_none")]
431 #[serde(default)]
432 pub publisher_count: Option<u16>,
433 #[serde(skip_serializing_if = "Option::is_none")]
434 #[serde(default)]
435 pub exponent: Option<i16>,
436 #[serde(skip_serializing_if = "Option::is_none")]
437 #[serde(default)]
438 pub confidence: Option<Price>,
439 #[serde(skip_serializing_if = "Option::is_none")]
440 #[serde(default)]
441 pub funding_rate: Option<Rate>,
442 #[serde(skip_serializing_if = "Option::is_none")]
443 #[serde(default)]
444 pub funding_timestamp: Option<TimestampUs>,
445 #[serde(skip_serializing_if = "Option::is_none")]
447 #[serde(default)]
448 pub funding_rate_interval: Option<DurationUs>,
449 #[serde(skip_serializing_if = "Option::is_none")]
450 #[serde(default)]
451 pub market_session: Option<MarketSession>,
452 #[serde(skip_serializing_if = "Option::is_none")]
453 #[serde(with = "crate::serde_str::option_price")]
454 #[serde(default)]
455 pub ema_price: Option<Price>,
456 #[serde(skip_serializing_if = "Option::is_none")]
457 #[serde(default)]
458 pub ema_confidence: Option<Price>,
459 #[serde(skip_serializing_if = "Option::is_none")]
460 #[serde(default)]
461 pub feed_update_timestamp: Option<TimestampUs>,
462}
463
464impl ParsedFeedPayload {
465 pub fn new(
466 price_feed_id: PriceFeedId,
467 data: &AggregatedPriceFeedData,
468 properties: &[PriceFeedProperty],
469 ) -> Self {
470 let mut output = Self {
471 price_feed_id,
472 price: None,
473 best_bid_price: None,
474 best_ask_price: None,
475 publisher_count: None,
476 exponent: None,
477 confidence: None,
478 funding_rate: None,
479 funding_timestamp: None,
480 funding_rate_interval: None,
481 market_session: None,
482 ema_price: None,
483 ema_confidence: None,
484 feed_update_timestamp: None,
485 };
486 for &property in properties {
487 match property {
488 PriceFeedProperty::Price => {
489 output.price = data.price;
490 }
491 PriceFeedProperty::BestBidPrice => {
492 output.best_bid_price = data.best_bid_price;
493 }
494 PriceFeedProperty::BestAskPrice => {
495 output.best_ask_price = data.best_ask_price;
496 }
497 PriceFeedProperty::PublisherCount => {
498 output.publisher_count = Some(data.publisher_count);
499 }
500 PriceFeedProperty::Exponent => {
501 output.exponent = Some(data.exponent);
502 }
503 PriceFeedProperty::Confidence => {
504 output.confidence = data.confidence;
505 }
506 PriceFeedProperty::FundingRate => {
507 output.funding_rate = data.funding_rate;
508 }
509 PriceFeedProperty::FundingTimestamp => {
510 output.funding_timestamp = data.funding_timestamp;
511 }
512 PriceFeedProperty::FundingRateInterval => {
513 output.funding_rate_interval = data.funding_rate_interval;
514 }
515 PriceFeedProperty::MarketSession => {
516 output.market_session = Some(data.market_session);
517 }
518 PriceFeedProperty::EmaPrice => {
519 output.ema_price = data.ema_price;
520 }
521 PriceFeedProperty::EmaConfidence => {
522 output.ema_confidence = data.ema_confidence;
523 }
524 PriceFeedProperty::FeedUpdateTimestamp => {
525 output.feed_update_timestamp = data.feed_update_timestamp;
526 }
527 }
528 }
529 output
530 }
531
532 pub fn new_full(
533 price_feed_id: PriceFeedId,
534 exponent: Option<i16>,
535 data: &AggregatedPriceFeedData,
536 ) -> Self {
537 Self {
538 price_feed_id,
539 price: data.price,
540 best_bid_price: data.best_bid_price,
541 best_ask_price: data.best_ask_price,
542 publisher_count: Some(data.publisher_count),
543 exponent,
544 confidence: data.confidence,
545 funding_rate: data.funding_rate,
546 funding_timestamp: data.funding_timestamp,
547 funding_rate_interval: data.funding_rate_interval,
548 market_session: Some(data.market_session),
549 ema_price: data.ema_price,
550 ema_confidence: data.ema_confidence,
551 feed_update_timestamp: data.feed_update_timestamp,
552 }
553 }
554}
555
556#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, ToSchema)]
558#[serde(tag = "type")]
559#[serde(rename_all = "camelCase")]
560pub enum WsRequest {
561 Subscribe(SubscribeRequest),
562 Unsubscribe(UnsubscribeRequest),
563}
564
565#[derive(
566 Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize, ToSchema,
567)]
568pub struct SubscriptionId(pub u64);
569
570#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, ToSchema)]
571#[serde(rename_all = "camelCase")]
572pub struct SubscribeRequest {
573 pub subscription_id: SubscriptionId,
574 #[serde(flatten)]
575 pub params: SubscriptionParams,
576}
577
578#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, ToSchema)]
579#[serde(rename_all = "camelCase")]
580pub struct UnsubscribeRequest {
581 pub subscription_id: SubscriptionId,
582}
583
584#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, From, ToSchema)]
586#[serde(tag = "type")]
587#[serde(rename_all = "camelCase")]
588pub enum WsResponse {
589 Error(ErrorResponse),
590 Subscribed(SubscribedResponse),
591 SubscribedWithInvalidFeedIdsIgnored(SubscribedWithInvalidFeedIdsIgnoredResponse),
592 Unsubscribed(UnsubscribedResponse),
593 SubscriptionError(SubscriptionErrorResponse),
594 StreamUpdated(StreamUpdatedResponse),
595}
596
597#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, ToSchema)]
599#[serde(rename_all = "camelCase")]
600pub struct SubscribedResponse {
601 pub subscription_id: SubscriptionId,
602}
603
604#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, ToSchema)]
605#[serde(rename_all = "camelCase")]
606pub struct InvalidFeedSubscriptionDetails {
607 pub unknown_ids: Vec<PriceFeedId>,
608 pub unknown_symbols: Vec<String>,
609 pub unsupported_channels: Vec<PriceFeedId>,
610 pub unstable: Vec<PriceFeedId>,
611}
612
613#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, ToSchema)]
614#[serde(rename_all = "camelCase")]
615pub struct SubscribedWithInvalidFeedIdsIgnoredResponse {
616 pub subscription_id: SubscriptionId,
617 pub subscribed_feed_ids: Vec<PriceFeedId>,
618 pub ignored_invalid_feed_ids: InvalidFeedSubscriptionDetails,
619}
620
621#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, ToSchema)]
622#[serde(rename_all = "camelCase")]
623pub struct UnsubscribedResponse {
624 pub subscription_id: SubscriptionId,
625}
626
627#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, ToSchema)]
630#[serde(rename_all = "camelCase")]
631pub struct SubscriptionErrorResponse {
632 pub subscription_id: SubscriptionId,
633 pub error: String,
634}
635
636#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, ToSchema)]
639#[serde(rename_all = "camelCase")]
640pub struct ErrorResponse {
641 pub error: String,
642}
643
644#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, ToSchema)]
647#[serde(rename_all = "camelCase")]
648pub struct StreamUpdatedResponse {
649 pub subscription_id: SubscriptionId,
650 #[serde(flatten)]
651 pub payload: JsonUpdate,
652}
653
654fn validate_price_feed_ids_or_symbols(
656 price_feed_ids: &Option<Vec<PriceFeedId>>,
657 symbols: &Option<Vec<String>>,
658) -> Result<(), &'static str> {
659 if price_feed_ids.is_none() && symbols.is_none() {
660 return Err("either price feed ids or symbols must be specified");
661 }
662 if price_feed_ids.is_some() && symbols.is_some() {
663 return Err("either price feed ids or symbols must be specified, not both");
664 }
665 Ok(())
666}
667
668fn validate_optional_nonempty_vec_has_unique_elements<T>(
669 vec: &Option<Vec<T>>,
670 empty_msg: &'static str,
671 duplicate_msg: &'static str,
672) -> Result<(), &'static str>
673where
674 T: Eq + std::hash::Hash,
675{
676 if let Some(items) = vec {
677 if items.is_empty() {
678 return Err(empty_msg);
679 }
680 if !items.iter().all_unique() {
681 return Err(duplicate_msg);
682 }
683 }
684 Ok(())
685}
686
687fn validate_properties(properties: &[PriceFeedProperty]) -> Result<(), &'static str> {
688 if properties.is_empty() {
689 return Err("no properties specified");
690 }
691 if !properties.iter().all_unique() {
692 return Err("duplicate properties specified");
693 }
694 Ok(())
695}
696
697fn validate_formats(formats: &[Format]) -> Result<(), &'static str> {
698 if !formats.iter().all_unique() {
699 return Err("duplicate formats or chains specified");
700 }
701 Ok(())
702}
703
704#[derive(
705 Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, Hash, From, ToSchema, Default,
706)]
707#[serde(rename_all = "camelCase")]
708#[schema(example = "regular")]
709pub enum MarketSession {
710 #[default]
711 Regular,
712 PreMarket,
713 PostMarket,
714 OverNight,
715 Closed,
716}
717
718#[derive(
719 Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, Hash, From, ToSchema, Default,
720)]
721#[serde(rename_all = "camelCase")]
722#[schema(example = "open")]
723pub enum TradingStatus {
724 #[default]
725 Open,
726 Closed,
727 Halted,
728 CorpAction,
729}
730
731impl From<MarketSession> for i16 {
732 fn from(s: MarketSession) -> i16 {
733 match s {
734 MarketSession::Regular => 0,
735 MarketSession::PreMarket => 1,
736 MarketSession::PostMarket => 2,
737 MarketSession::OverNight => 3,
738 MarketSession::Closed => 4,
739 }
740 }
741}
742
743impl TryFrom<i16> for MarketSession {
744 type Error = anyhow::Error;
745
746 fn try_from(value: i16) -> Result<MarketSession, Self::Error> {
747 match value {
748 0 => Ok(MarketSession::Regular),
749 1 => Ok(MarketSession::PreMarket),
750 2 => Ok(MarketSession::PostMarket),
751 3 => Ok(MarketSession::OverNight),
752 4 => Ok(MarketSession::Closed),
753 _ => Err(anyhow::anyhow!("invalid MarketSession value: {}", value)),
754 }
755 }
756}