pyth_lazer_protocol/
api.rs

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    // Either price feed ids or symbols must be specified.
22    #[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    // "chains" was renamed to "formats". "chains" is still supported for compatibility.
28    #[serde(alias = "chains")]
29    pub formats: Vec<Format>,
30    #[serde(default)]
31    pub json_binary_encoding: JsonBinaryEncoding,
32    /// If `true`, the stream update will contain a JSON object containing
33    /// all data of the update.
34    #[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    // Either price feed ids or symbols must be specified.
92    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    /// If `true`, the stream update will contain a JSON object containing
100    /// all data of the update.
101    #[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    /// Deliver stream updates as JSON text messages.
190    #[default]
191    Json,
192    /// Deliver stream updates as binary messages.
193    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    // Either price feed ids or symbols must be specified.
309    pub price_feed_ids: Option<Vec<PriceFeedId>>,
310    #[schema(default)]
311    pub symbols: Option<Vec<String>>,
312    pub properties: Vec<PriceFeedProperty>,
313    // "chains" was renamed to "formats". "chains" is still supported for compatibility.
314    #[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    /// If `true`, the stream update will contain a `parsed` JSON field containing
321    /// all data of the update.
322    #[serde(default = "default_parsed")]
323    pub parsed: bool,
324    pub channel: Channel,
325    // "ignoreInvalidFeedIds" was renamed to "ignoreInvalidFeeds". "ignoreInvalidFeedIds" is still supported for compatibility.
326    #[serde(default, alias = "ignoreInvalidFeedIds")]
327    pub ignore_invalid_feeds: bool,
328    // Market sessions to filter price feeds by. Default to [Regular] for backward compatibility.
329    #[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    /// Present unless `parsed = false` is specified in subscription params.
390    #[serde(skip_serializing_if = "Option::is_none")]
391    pub parsed: Option<ParsedPayload>,
392    /// Only present if `Evm` is present in `formats` in subscription params.
393    #[serde(skip_serializing_if = "Option::is_none")]
394    pub evm: Option<JsonBinaryData>,
395    /// Only present if `Solana` is present in `formats` in subscription params.
396    #[serde(skip_serializing_if = "Option::is_none")]
397    pub solana: Option<JsonBinaryData>,
398    /// Only present if `LeEcdsa` is present in `formats` in subscription params.
399    #[serde(skip_serializing_if = "Option::is_none")]
400    pub le_ecdsa: Option<JsonBinaryData>,
401    /// Only present if `LeUnsigned` is present in `formats` in subscription params.
402    #[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    // More fields may be added later.
446    #[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}
453
454impl ParsedFeedPayload {
455    pub fn new(
456        price_feed_id: PriceFeedId,
457        data: &AggregatedPriceFeedData,
458        properties: &[PriceFeedProperty],
459    ) -> Self {
460        let mut output = Self {
461            price_feed_id,
462            price: None,
463            best_bid_price: None,
464            best_ask_price: None,
465            publisher_count: None,
466            exponent: None,
467            confidence: None,
468            funding_rate: None,
469            funding_timestamp: None,
470            funding_rate_interval: None,
471            market_session: None,
472        };
473        for &property in properties {
474            match property {
475                PriceFeedProperty::Price => {
476                    output.price = data.price;
477                }
478                PriceFeedProperty::BestBidPrice => {
479                    output.best_bid_price = data.best_bid_price;
480                }
481                PriceFeedProperty::BestAskPrice => {
482                    output.best_ask_price = data.best_ask_price;
483                }
484                PriceFeedProperty::PublisherCount => {
485                    output.publisher_count = Some(data.publisher_count);
486                }
487                PriceFeedProperty::Exponent => {
488                    output.exponent = Some(data.exponent);
489                }
490                PriceFeedProperty::Confidence => {
491                    output.confidence = data.confidence;
492                }
493                PriceFeedProperty::FundingRate => {
494                    output.funding_rate = data.funding_rate;
495                }
496                PriceFeedProperty::FundingTimestamp => {
497                    output.funding_timestamp = data.funding_timestamp;
498                }
499                PriceFeedProperty::FundingRateInterval => {
500                    output.funding_rate_interval = data.funding_rate_interval;
501                }
502                PriceFeedProperty::MarketSession => {
503                    output.market_session = Some(data.market_session);
504                }
505            }
506        }
507        output
508    }
509
510    pub fn new_full(
511        price_feed_id: PriceFeedId,
512        exponent: Option<i16>,
513        data: &AggregatedPriceFeedData,
514    ) -> Self {
515        Self {
516            price_feed_id,
517            price: data.price,
518            best_bid_price: data.best_bid_price,
519            best_ask_price: data.best_ask_price,
520            publisher_count: Some(data.publisher_count),
521            exponent,
522            confidence: data.confidence,
523            funding_rate: data.funding_rate,
524            funding_timestamp: data.funding_timestamp,
525            funding_rate_interval: data.funding_rate_interval,
526            market_session: Some(data.market_session),
527        }
528    }
529}
530
531/// A request sent from the client to the server.
532#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, ToSchema)]
533#[serde(tag = "type")]
534#[serde(rename_all = "camelCase")]
535pub enum WsRequest {
536    Subscribe(SubscribeRequest),
537    Unsubscribe(UnsubscribeRequest),
538}
539
540#[derive(
541    Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize, ToSchema,
542)]
543pub struct SubscriptionId(pub u64);
544
545#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, ToSchema)]
546#[serde(rename_all = "camelCase")]
547pub struct SubscribeRequest {
548    pub subscription_id: SubscriptionId,
549    #[serde(flatten)]
550    pub params: SubscriptionParams,
551}
552
553#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, ToSchema)]
554#[serde(rename_all = "camelCase")]
555pub struct UnsubscribeRequest {
556    pub subscription_id: SubscriptionId,
557}
558
559/// A JSON response sent from the server to the client.
560#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, From, ToSchema)]
561#[serde(tag = "type")]
562#[serde(rename_all = "camelCase")]
563pub enum WsResponse {
564    Error(ErrorResponse),
565    Subscribed(SubscribedResponse),
566    SubscribedWithInvalidFeedIdsIgnored(SubscribedWithInvalidFeedIdsIgnoredResponse),
567    Unsubscribed(UnsubscribedResponse),
568    SubscriptionError(SubscriptionErrorResponse),
569    StreamUpdated(StreamUpdatedResponse),
570}
571
572/// Sent from the server after a successul subscription.
573#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, ToSchema)]
574#[serde(rename_all = "camelCase")]
575pub struct SubscribedResponse {
576    pub subscription_id: SubscriptionId,
577}
578
579#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, ToSchema)]
580#[serde(rename_all = "camelCase")]
581pub struct InvalidFeedSubscriptionDetails {
582    pub unknown_ids: Vec<PriceFeedId>,
583    pub unknown_symbols: Vec<String>,
584    pub unsupported_channels: Vec<PriceFeedId>,
585    pub unstable: Vec<PriceFeedId>,
586}
587
588#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, ToSchema)]
589#[serde(rename_all = "camelCase")]
590pub struct SubscribedWithInvalidFeedIdsIgnoredResponse {
591    pub subscription_id: SubscriptionId,
592    pub subscribed_feed_ids: Vec<PriceFeedId>,
593    pub ignored_invalid_feed_ids: InvalidFeedSubscriptionDetails,
594}
595
596#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, ToSchema)]
597#[serde(rename_all = "camelCase")]
598pub struct UnsubscribedResponse {
599    pub subscription_id: SubscriptionId,
600}
601
602/// Sent from the server if the requested subscription or unsubscription request
603/// could not be fulfilled.
604#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, ToSchema)]
605#[serde(rename_all = "camelCase")]
606pub struct SubscriptionErrorResponse {
607    pub subscription_id: SubscriptionId,
608    pub error: String,
609}
610
611/// Sent from the server if an internal error occured while serving data for an existing subscription,
612/// or a client request sent a bad request.
613#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, ToSchema)]
614#[serde(rename_all = "camelCase")]
615pub struct ErrorResponse {
616    pub error: String,
617}
618
619/// Sent from the server when new data is available for an existing subscription
620/// (only if `delivery_format == Json`).
621#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, ToSchema)]
622#[serde(rename_all = "camelCase")]
623pub struct StreamUpdatedResponse {
624    pub subscription_id: SubscriptionId,
625    #[serde(flatten)]
626    pub payload: JsonUpdate,
627}
628
629// Common validation functions
630fn validate_price_feed_ids_or_symbols(
631    price_feed_ids: &Option<Vec<PriceFeedId>>,
632    symbols: &Option<Vec<String>>,
633) -> Result<(), &'static str> {
634    if price_feed_ids.is_none() && symbols.is_none() {
635        return Err("either price feed ids or symbols must be specified");
636    }
637    if price_feed_ids.is_some() && symbols.is_some() {
638        return Err("either price feed ids or symbols must be specified, not both");
639    }
640    Ok(())
641}
642
643fn validate_optional_nonempty_vec_has_unique_elements<T>(
644    vec: &Option<Vec<T>>,
645    empty_msg: &'static str,
646    duplicate_msg: &'static str,
647) -> Result<(), &'static str>
648where
649    T: Eq + std::hash::Hash,
650{
651    if let Some(ref items) = vec {
652        if items.is_empty() {
653            return Err(empty_msg);
654        }
655        if !items.iter().all_unique() {
656            return Err(duplicate_msg);
657        }
658    }
659    Ok(())
660}
661
662fn validate_properties(properties: &[PriceFeedProperty]) -> Result<(), &'static str> {
663    if properties.is_empty() {
664        return Err("no properties specified");
665    }
666    if !properties.iter().all_unique() {
667        return Err("duplicate properties specified");
668    }
669    Ok(())
670}
671
672fn validate_formats(formats: &[Format]) -> Result<(), &'static str> {
673    if !formats.iter().all_unique() {
674        return Err("duplicate formats or chains specified");
675    }
676    Ok(())
677}
678
679#[derive(
680    Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, Hash, From, ToSchema, Default,
681)]
682#[serde(rename_all = "camelCase")]
683#[schema(example = "regular")]
684pub enum MarketSession {
685    #[default]
686    Regular,
687    PreMarket,
688    PostMarket,
689    OverNight,
690    Closed,
691}
692
693impl From<MarketSession> for i16 {
694    fn from(s: MarketSession) -> i16 {
695        match s {
696            MarketSession::Regular => 0,
697            MarketSession::PreMarket => 1,
698            MarketSession::PostMarket => 2,
699            MarketSession::OverNight => 3,
700            MarketSession::Closed => 4,
701        }
702    }
703}
704
705impl TryFrom<i16> for MarketSession {
706    type Error = anyhow::Error;
707
708    fn try_from(value: i16) -> Result<MarketSession, Self::Error> {
709        match value {
710            0 => Ok(MarketSession::Regular),
711            1 => Ok(MarketSession::PreMarket),
712            2 => Ok(MarketSession::PostMarket),
713            3 => Ok(MarketSession::OverNight),
714            4 => Ok(MarketSession::Closed),
715            _ => Err(anyhow::anyhow!("invalid MarketSession value: {}", value)),
716        }
717    }
718}