1use {
2 std::{
3 cmp::Ordering,
4 fmt::Display,
5 ops::{Deref, DerefMut},
6 },
7 utoipa::PartialSchema,
8};
9
10use derive_more::derive::From;
11use itertools::Itertools as _;
12use serde::{de::Error, Deserialize, Serialize};
13use utoipa::ToSchema;
14
15use crate::{
16 payload::AggregatedPriceFeedData,
17 time::{DurationUs, FixedRate, TimestampUs},
18 ChannelId, Price, PriceFeedId, PriceFeedProperty, Rate,
19};
20
21#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, ToSchema)]
22#[serde(rename_all = "camelCase")]
23#[schema(examples(LatestPriceRequestRepr::example1))]
24pub struct LatestPriceRequestRepr {
25 pub price_feed_ids: Option<Vec<PriceFeedId>>,
28 pub symbols: Option<Vec<String>>,
31 pub properties: Vec<PriceFeedProperty>,
33 #[serde(alias = "chains")]
36 pub formats: Vec<Format>,
37 #[serde(default)]
38 pub json_binary_encoding: JsonBinaryEncoding,
39 #[serde(default = "default_parsed")]
42 pub parsed: bool,
43 pub channel: Channel,
45}
46
47impl LatestPriceRequestRepr {
48 fn example1() -> Self {
49 Self {
50 price_feed_ids: None,
51 symbols: Some(vec!["Crypto.BTC/USD".into()]),
52 properties: vec![PriceFeedProperty::Price, PriceFeedProperty::Confidence],
53 formats: vec![Format::Evm],
54 json_binary_encoding: JsonBinaryEncoding::Hex,
55 parsed: true,
56 channel: Channel::RealTime,
57 }
58 }
59}
60
61#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, ToSchema)]
62#[serde(rename_all = "camelCase")]
63pub struct LatestPriceRequest(LatestPriceRequestRepr);
64
65impl<'de> Deserialize<'de> for LatestPriceRequest {
66 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
67 where
68 D: serde::Deserializer<'de>,
69 {
70 let value = LatestPriceRequestRepr::deserialize(deserializer)?;
71 Self::new(value).map_err(Error::custom)
72 }
73}
74
75impl LatestPriceRequest {
76 pub fn new(value: LatestPriceRequestRepr) -> Result<Self, &'static str> {
77 validate_price_feed_ids_or_symbols(&value.price_feed_ids, &value.symbols)?;
78 validate_optional_nonempty_vec_has_unique_elements(
79 &value.price_feed_ids,
80 "no price feed ids specified",
81 "duplicate price feed ids specified",
82 )?;
83 validate_optional_nonempty_vec_has_unique_elements(
84 &value.symbols,
85 "no symbols specified",
86 "duplicate symbols specified",
87 )?;
88 validate_formats(&value.formats)?;
89 validate_properties(&value.properties)?;
90 Ok(Self(value))
91 }
92}
93
94impl Deref for LatestPriceRequest {
95 type Target = LatestPriceRequestRepr;
96
97 fn deref(&self) -> &Self::Target {
98 &self.0
99 }
100}
101impl DerefMut for LatestPriceRequest {
102 fn deref_mut(&mut self) -> &mut Self::Target {
103 &mut self.0
104 }
105}
106
107#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, ToSchema)]
108#[serde(rename_all = "camelCase")]
109pub struct PriceRequestRepr {
110 pub timestamp: TimestampUs,
112 pub price_feed_ids: Option<Vec<PriceFeedId>>,
115 #[schema(default)]
118 pub symbols: Option<Vec<String>>,
119 pub properties: Vec<PriceFeedProperty>,
121 pub formats: Vec<Format>,
123 #[serde(default)]
124 pub json_binary_encoding: JsonBinaryEncoding,
125 #[serde(default = "default_parsed")]
128 pub parsed: bool,
129 pub channel: Channel,
131}
132
133#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, ToSchema)]
134#[serde(rename_all = "camelCase")]
135pub struct PriceRequest(PriceRequestRepr);
136
137impl<'de> Deserialize<'de> for PriceRequest {
138 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
139 where
140 D: serde::Deserializer<'de>,
141 {
142 let value = PriceRequestRepr::deserialize(deserializer)?;
143 Self::new(value).map_err(Error::custom)
144 }
145}
146
147impl PriceRequest {
148 pub fn new(value: PriceRequestRepr) -> Result<Self, &'static str> {
149 validate_price_feed_ids_or_symbols(&value.price_feed_ids, &value.symbols)?;
150 validate_optional_nonempty_vec_has_unique_elements(
151 &value.price_feed_ids,
152 "no price feed ids specified",
153 "duplicate price feed ids specified",
154 )?;
155 validate_optional_nonempty_vec_has_unique_elements(
156 &value.symbols,
157 "no symbols specified",
158 "duplicate symbols specified",
159 )?;
160 validate_formats(&value.formats)?;
161 validate_properties(&value.properties)?;
162 Ok(Self(value))
163 }
164}
165
166impl Deref for PriceRequest {
167 type Target = PriceRequestRepr;
168
169 fn deref(&self) -> &Self::Target {
170 &self.0
171 }
172}
173impl DerefMut for PriceRequest {
174 fn deref_mut(&mut self) -> &mut Self::Target {
175 &mut self.0
176 }
177}
178
179#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, ToSchema)]
180#[serde(rename_all = "camelCase")]
181pub struct ReducePriceRequest {
182 pub payload: JsonUpdate,
185 pub price_feed_ids: Vec<PriceFeedId>,
187}
188
189pub type LatestPriceResponse = JsonUpdate;
190pub type ReducePriceResponse = JsonUpdate;
191pub type PriceResponse = JsonUpdate;
192
193pub fn default_parsed() -> bool {
194 true
195}
196
197pub fn schema_default_symbols() -> Option<Vec<String>> {
198 None
199}
200pub fn schema_default_price_feed_ids() -> Option<Vec<PriceFeedId>> {
201 Some(vec![PriceFeedId(1)])
202}
203
204#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Serialize, Deserialize, ToSchema)]
205#[serde(rename_all = "camelCase")]
206pub enum DeliveryFormat {
207 #[default]
209 Json,
210 Binary,
212}
213
214#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, ToSchema)]
215#[serde(rename_all = "camelCase")]
216pub enum Format {
217 Evm,
218 Solana,
219 LeEcdsa,
220 LeUnsigned,
221}
222
223#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Serialize, Deserialize, ToSchema)]
224#[serde(rename_all = "camelCase")]
225pub enum JsonBinaryEncoding {
226 #[default]
227 Base64,
228 Hex,
229}
230
231#[derive(Serialize, Deserialize, ToSchema)]
232pub enum ChannelSchemaRepr {
233 #[serde(rename = "real_time")]
234 RealTime,
235 #[serde(rename = "fixed_rate@50ms")]
236 FixedRate50ms,
237 #[serde(rename = "fixed_rate@200ms")]
238 FixedRate200ms,
239 #[serde(rename = "fixed_rate@1000ms")]
240 FixedRate1000ms,
241}
242
243#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, From)]
244pub enum Channel {
245 FixedRate(FixedRate),
246 RealTime,
247}
248
249impl PartialSchema for Channel {
250 fn schema() -> utoipa::openapi::RefOr<utoipa::openapi::schema::Schema> {
251 ChannelSchemaRepr::schema()
252 }
253}
254
255impl ToSchema for Channel {
256 fn name() -> std::borrow::Cow<'static, str> {
257 ChannelSchemaRepr::name()
258 }
259
260 fn schemas(
261 schemas: &mut Vec<(
262 String,
263 utoipa::openapi::RefOr<utoipa::openapi::schema::Schema>,
264 )>,
265 ) {
266 ChannelSchemaRepr::schemas(schemas)
267 }
268}
269
270impl PartialOrd for Channel {
271 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
272 let rate_left = match self {
273 Channel::FixedRate(rate) => rate.duration().as_micros(),
274 Channel::RealTime => FixedRate::MIN.duration().as_micros(),
275 };
276 let rate_right = match other {
277 Channel::FixedRate(rate) => rate.duration().as_micros(),
278 Channel::RealTime => FixedRate::MIN.duration().as_micros(),
279 };
280 Some(rate_left.cmp(&rate_right))
281 }
282}
283
284impl Serialize for Channel {
285 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
286 where
287 S: serde::Serializer,
288 {
289 match self {
290 Channel::FixedRate(fixed_rate) => serializer.serialize_str(&format!(
291 "fixed_rate@{}ms",
292 fixed_rate.duration().as_millis()
293 )),
294 Channel::RealTime => serializer.serialize_str("real_time"),
295 }
296 }
297}
298
299impl Display for Channel {
300 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
301 match self {
302 Channel::FixedRate(fixed_rate) => {
303 write!(f, "fixed_rate@{}ms", fixed_rate.duration().as_millis())
304 }
305 Channel::RealTime => write!(f, "real_time"),
306 }
307 }
308}
309
310impl Channel {
311 pub fn id(&self) -> ChannelId {
312 match self {
313 Channel::FixedRate(fixed_rate) => match fixed_rate.duration().as_millis() {
314 50 => ChannelId::FIXED_RATE_50,
315 200 => ChannelId::FIXED_RATE_200,
316 1000 => ChannelId::FIXED_RATE_1000,
317 _ => panic!("unknown channel: {self:?}"),
318 },
319 Channel::RealTime => ChannelId::REAL_TIME,
320 }
321 }
322}
323
324#[test]
325fn id_supports_all_fixed_rates() {
326 for rate in FixedRate::ALL {
327 Channel::FixedRate(rate).id();
328 }
329}
330
331fn parse_channel(value: &str) -> Option<Channel> {
332 if value == "real_time" {
333 Some(Channel::RealTime)
334 } else if let Some(rest) = value.strip_prefix("fixed_rate@") {
335 let ms_value = rest.strip_suffix("ms")?;
336 Some(Channel::FixedRate(FixedRate::from_millis(
337 ms_value.parse().ok()?,
338 )?))
339 } else {
340 None
341 }
342}
343
344impl<'de> Deserialize<'de> for Channel {
345 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
346 where
347 D: serde::Deserializer<'de>,
348 {
349 let value = <String>::deserialize(deserializer)?;
350 parse_channel(&value).ok_or_else(|| Error::custom("unknown channel"))
351 }
352}
353
354#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, ToSchema)]
355#[serde(rename_all = "camelCase")]
356pub struct SubscriptionParamsRepr {
357 pub price_feed_ids: Option<Vec<PriceFeedId>>,
360 #[schema(default)]
363 pub symbols: Option<Vec<String>>,
364 pub properties: Vec<PriceFeedProperty>,
366 #[serde(alias = "chains")]
370 pub formats: Vec<Format>,
371 #[serde(default)]
375 pub delivery_format: DeliveryFormat,
376 #[serde(default)]
379 pub json_binary_encoding: JsonBinaryEncoding,
380 #[serde(default = "default_parsed")]
383 pub parsed: bool,
384 pub channel: Channel,
386 #[serde(default, alias = "ignoreInvalidFeedIds")]
389 pub ignore_invalid_feeds: bool,
390}
391
392#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, ToSchema)]
393#[serde(rename_all = "camelCase")]
394pub struct SubscriptionParams(SubscriptionParamsRepr);
395
396impl<'de> Deserialize<'de> for SubscriptionParams {
397 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
398 where
399 D: serde::Deserializer<'de>,
400 {
401 let value = SubscriptionParamsRepr::deserialize(deserializer)?;
402 Self::new(value).map_err(Error::custom)
403 }
404}
405
406impl SubscriptionParams {
407 pub fn new(value: SubscriptionParamsRepr) -> Result<Self, &'static str> {
408 validate_price_feed_ids_or_symbols(&value.price_feed_ids, &value.symbols)?;
409 validate_optional_nonempty_vec_has_unique_elements(
410 &value.price_feed_ids,
411 "no price feed ids specified",
412 "duplicate price feed ids specified",
413 )?;
414 validate_optional_nonempty_vec_has_unique_elements(
415 &value.symbols,
416 "no symbols specified",
417 "duplicate symbols specified",
418 )?;
419 validate_formats(&value.formats)?;
420 validate_properties(&value.properties)?;
421 Ok(Self(value))
422 }
423}
424
425impl Deref for SubscriptionParams {
426 type Target = SubscriptionParamsRepr;
427
428 fn deref(&self) -> &Self::Target {
429 &self.0
430 }
431}
432impl DerefMut for SubscriptionParams {
433 fn deref_mut(&mut self) -> &mut Self::Target {
434 &mut self.0
435 }
436}
437
438#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, ToSchema)]
439#[serde(rename_all = "camelCase")]
440pub struct JsonBinaryData {
441 pub encoding: JsonBinaryEncoding,
443 pub data: String,
445}
446
447#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, ToSchema)]
448#[serde(rename_all = "camelCase")]
449pub struct JsonUpdate {
450 #[serde(skip_serializing_if = "Option::is_none")]
453 pub parsed: Option<ParsedPayload>,
454 #[serde(skip_serializing_if = "Option::is_none")]
456 pub evm: Option<JsonBinaryData>,
457 #[serde(skip_serializing_if = "Option::is_none")]
459 pub solana: Option<JsonBinaryData>,
460 #[serde(skip_serializing_if = "Option::is_none")]
462 pub le_ecdsa: Option<JsonBinaryData>,
463 #[serde(skip_serializing_if = "Option::is_none")]
465 pub le_unsigned: Option<JsonBinaryData>,
466}
467
468#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, ToSchema)]
469#[serde(rename_all = "camelCase")]
470pub struct ParsedPayload {
471 #[serde(with = "crate::serde_str::timestamp")]
473 #[schema(value_type = String)]
474 pub timestamp_us: TimestampUs,
475 pub price_feeds: Vec<ParsedFeedPayload>,
477}
478
479#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, ToSchema)]
481#[serde(rename_all = "camelCase")]
482pub struct ParsedFeedPayload {
483 pub price_feed_id: PriceFeedId,
485 #[serde(skip_serializing_if = "Option::is_none")]
489 #[serde(with = "crate::serde_str::option_price")]
490 #[serde(default)]
491 #[schema(value_type = Option<String>)]
492 pub price: Option<Price>,
493 #[serde(skip_serializing_if = "Option::is_none")]
497 #[serde(with = "crate::serde_str::option_price")]
498 #[serde(default)]
499 #[schema(value_type = Option<String>)]
500 pub best_bid_price: Option<Price>,
501 #[serde(skip_serializing_if = "Option::is_none")]
505 #[serde(with = "crate::serde_str::option_price")]
506 #[serde(default)]
507 #[schema(value_type = Option<String>)]
508 pub best_ask_price: Option<Price>,
509 #[serde(skip_serializing_if = "Option::is_none")]
512 #[serde(default)]
513 pub publisher_count: Option<u16>,
514 #[serde(skip_serializing_if = "Option::is_none")]
519 #[serde(default)]
520 pub exponent: Option<i16>,
521 #[serde(skip_serializing_if = "Option::is_none")]
525 #[serde(default)]
526 pub confidence: Option<Price>,
527 #[serde(skip_serializing_if = "Option::is_none")]
532 #[serde(default)]
533 pub funding_rate: Option<Rate>,
534 #[serde(skip_serializing_if = "Option::is_none")]
539 #[serde(default)]
540 pub funding_timestamp: Option<TimestampUs>,
541 #[serde(skip_serializing_if = "Option::is_none")]
545 #[serde(default)]
546 pub funding_rate_interval: Option<DurationUs>,
547 #[serde(skip_serializing_if = "Option::is_none")]
550 #[serde(default)]
551 pub market_session: Option<MarketSession>,
552 #[serde(skip_serializing_if = "Option::is_none")]
557 #[serde(with = "crate::serde_str::option_price")]
558 #[serde(default)]
559 #[schema(value_type = Option<String>)]
560 pub ema_price: Option<Price>,
561 #[serde(skip_serializing_if = "Option::is_none")]
566 #[serde(default)]
567 pub ema_confidence: Option<Price>,
568 #[serde(skip_serializing_if = "Option::is_none")]
569 #[serde(default)]
570 pub feed_update_timestamp: Option<TimestampUs>,
571 }
573
574impl ParsedFeedPayload {
575 pub fn new(
576 price_feed_id: PriceFeedId,
577 data: &AggregatedPriceFeedData,
578 properties: &[PriceFeedProperty],
579 ) -> Self {
580 let mut output = Self {
581 price_feed_id,
582 price: None,
583 best_bid_price: None,
584 best_ask_price: None,
585 publisher_count: None,
586 exponent: None,
587 confidence: None,
588 funding_rate: None,
589 funding_timestamp: None,
590 funding_rate_interval: None,
591 market_session: None,
592 ema_price: None,
593 ema_confidence: None,
594 feed_update_timestamp: None,
595 };
596 for &property in properties {
597 match property {
598 PriceFeedProperty::Price => {
599 output.price = data.price;
600 }
601 PriceFeedProperty::BestBidPrice => {
602 output.best_bid_price = data.best_bid_price;
603 }
604 PriceFeedProperty::BestAskPrice => {
605 output.best_ask_price = data.best_ask_price;
606 }
607 PriceFeedProperty::PublisherCount => {
608 output.publisher_count = Some(data.publisher_count);
609 }
610 PriceFeedProperty::Exponent => {
611 output.exponent = Some(data.exponent);
612 }
613 PriceFeedProperty::Confidence => {
614 output.confidence = data.confidence;
615 }
616 PriceFeedProperty::FundingRate => {
617 output.funding_rate = data.funding_rate;
618 }
619 PriceFeedProperty::FundingTimestamp => {
620 output.funding_timestamp = data.funding_timestamp;
621 }
622 PriceFeedProperty::FundingRateInterval => {
623 output.funding_rate_interval = data.funding_rate_interval;
624 }
625 PriceFeedProperty::MarketSession => {
626 output.market_session = Some(data.market_session);
627 }
628 PriceFeedProperty::EmaPrice => {
629 output.ema_price = data.ema_price;
630 }
631 PriceFeedProperty::EmaConfidence => {
632 output.ema_confidence = data.ema_confidence;
633 }
634 PriceFeedProperty::FeedUpdateTimestamp => {
635 output.feed_update_timestamp = data.feed_update_timestamp;
636 }
637 }
638 }
639 output
640 }
641
642 pub fn new_full(
643 price_feed_id: PriceFeedId,
644 exponent: Option<i16>,
645 data: &AggregatedPriceFeedData,
646 ) -> Self {
647 Self {
648 price_feed_id,
649 price: data.price,
650 best_bid_price: data.best_bid_price,
651 best_ask_price: data.best_ask_price,
652 publisher_count: Some(data.publisher_count),
653 exponent,
654 confidence: data.confidence,
655 funding_rate: data.funding_rate,
656 funding_timestamp: data.funding_timestamp,
657 funding_rate_interval: data.funding_rate_interval,
658 market_session: Some(data.market_session),
659 ema_price: data.ema_price,
660 ema_confidence: data.ema_confidence,
661 feed_update_timestamp: data.feed_update_timestamp,
662 }
663 }
664}
665
666#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, ToSchema)]
668#[serde(tag = "type")]
669#[serde(rename_all = "camelCase")]
670pub enum WsRequest {
671 Subscribe(SubscribeRequest),
672 Unsubscribe(UnsubscribeRequest),
673}
674
675#[derive(
676 Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize, ToSchema,
677)]
678pub struct SubscriptionId(pub u64);
679
680#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, ToSchema)]
688#[serde(rename_all = "camelCase")]
689pub struct SubscribeRequest {
690 pub subscription_id: SubscriptionId,
693 #[serde(flatten)]
695 pub params: SubscriptionParams,
696}
697
698#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, ToSchema)]
705#[serde(rename_all = "camelCase")]
706pub struct UnsubscribeRequest {
707 pub subscription_id: SubscriptionId,
709}
710
711#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, From, ToSchema)]
713#[serde(tag = "type")]
714#[serde(rename_all = "camelCase")]
715pub enum WsResponse {
716 Error(ErrorResponse),
717 Subscribed(SubscribedResponse),
718 SubscribedWithInvalidFeedIdsIgnored(SubscribedWithInvalidFeedIdsIgnoredResponse),
719 Unsubscribed(UnsubscribedResponse),
720 SubscriptionError(SubscriptionErrorResponse),
721 StreamUpdated(StreamUpdatedResponse),
722}
723
724#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, ToSchema)]
726#[serde(rename_all = "camelCase")]
727pub struct SubscribedResponse {
728 pub subscription_id: SubscriptionId,
729}
730
731#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, ToSchema)]
732#[serde(rename_all = "camelCase")]
733pub struct InvalidFeedSubscriptionDetails {
734 pub unknown_ids: Vec<PriceFeedId>,
736 pub unknown_symbols: Vec<String>,
738 pub unsupported_channels: Vec<PriceFeedId>,
740 pub unstable: Vec<PriceFeedId>,
742}
743
744#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, ToSchema)]
747#[serde(rename_all = "camelCase")]
748pub struct SubscribedWithInvalidFeedIdsIgnoredResponse {
749 pub subscription_id: SubscriptionId,
751 pub subscribed_feed_ids: Vec<PriceFeedId>,
753 pub ignored_invalid_feed_ids: InvalidFeedSubscriptionDetails,
755}
756
757#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, ToSchema)]
759#[serde(rename_all = "camelCase")]
760pub struct UnsubscribedResponse {
761 pub subscription_id: SubscriptionId,
763}
764
765#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, ToSchema)]
768#[serde(rename_all = "camelCase")]
769pub struct SubscriptionErrorResponse {
770 pub subscription_id: SubscriptionId,
772 pub error: String,
774}
775
776#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, ToSchema)]
779#[serde(rename_all = "camelCase")]
780pub struct ErrorResponse {
781 pub error: String,
783}
784
785#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, ToSchema)]
788#[serde(rename_all = "camelCase")]
789pub struct StreamUpdatedResponse {
790 pub subscription_id: SubscriptionId,
792 #[serde(flatten)]
794 pub payload: JsonUpdate,
795}
796
797fn validate_price_feed_ids_or_symbols(
799 price_feed_ids: &Option<Vec<PriceFeedId>>,
800 symbols: &Option<Vec<String>>,
801) -> Result<(), &'static str> {
802 if price_feed_ids.is_none() && symbols.is_none() {
803 return Err("either price feed ids or symbols must be specified");
804 }
805 if price_feed_ids.is_some() && symbols.is_some() {
806 return Err("either price feed ids or symbols must be specified, not both");
807 }
808 Ok(())
809}
810
811fn validate_optional_nonempty_vec_has_unique_elements<T>(
812 vec: &Option<Vec<T>>,
813 empty_msg: &'static str,
814 duplicate_msg: &'static str,
815) -> Result<(), &'static str>
816where
817 T: Eq + std::hash::Hash,
818{
819 if let Some(items) = vec {
820 if items.is_empty() {
821 return Err(empty_msg);
822 }
823 if !items.iter().all_unique() {
824 return Err(duplicate_msg);
825 }
826 }
827 Ok(())
828}
829
830fn validate_properties(properties: &[PriceFeedProperty]) -> Result<(), &'static str> {
831 if properties.is_empty() {
832 return Err("no properties specified");
833 }
834 if !properties.iter().all_unique() {
835 return Err("duplicate properties specified");
836 }
837 Ok(())
838}
839
840fn validate_formats(formats: &[Format]) -> Result<(), &'static str> {
841 if !formats.iter().all_unique() {
842 return Err("duplicate formats or chains specified");
843 }
844 Ok(())
845}
846
847#[derive(
848 Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, Hash, From, ToSchema, Default,
849)]
850#[serde(rename_all = "camelCase")]
851#[schema(example = "regular")]
852pub enum MarketSession {
853 #[default]
854 Regular,
855 PreMarket,
856 PostMarket,
857 OverNight,
858 Closed,
859}
860
861#[derive(
862 Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, Hash, From, ToSchema, Default,
863)]
864#[serde(rename_all = "camelCase")]
865#[schema(example = "open")]
866pub enum TradingStatus {
867 #[default]
868 Open,
869 Closed,
870 Halted,
871 CorpAction,
872}
873
874impl From<MarketSession> for i16 {
875 fn from(s: MarketSession) -> i16 {
876 match s {
877 MarketSession::Regular => 0,
878 MarketSession::PreMarket => 1,
879 MarketSession::PostMarket => 2,
880 MarketSession::OverNight => 3,
881 MarketSession::Closed => 4,
882 }
883 }
884}
885
886impl TryFrom<i16> for MarketSession {
887 type Error = anyhow::Error;
888
889 fn try_from(value: i16) -> Result<MarketSession, Self::Error> {
890 match value {
891 0 => Ok(MarketSession::Regular),
892 1 => Ok(MarketSession::PreMarket),
893 2 => Ok(MarketSession::PostMarket),
894 3 => Ok(MarketSession::OverNight),
895 4 => Ok(MarketSession::Closed),
896 _ => Err(anyhow::anyhow!("invalid MarketSession value: {}", value)),
897 }
898 }
899}