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