use std::fmt;
use std::str::FromStr;
use serde::{Deserialize, Serialize};
use crate::error::RadionError;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum Channel {
Trading,
Fees,
Oracle,
Resolution,
Lifecycle,
Positions,
Combos,
Transfers,
Accounts,
Wallets,
Markets,
}
pub const CHANNELS: [Channel; 11] = [
Channel::Trading,
Channel::Fees,
Channel::Oracle,
Channel::Resolution,
Channel::Lifecycle,
Channel::Positions,
Channel::Combos,
Channel::Transfers,
Channel::Accounts,
Channel::Wallets,
Channel::Markets,
];
impl Channel {
pub fn as_str(&self) -> &'static str {
match self {
Self::Trading => "trading",
Self::Fees => "fees",
Self::Oracle => "oracle",
Self::Resolution => "resolution",
Self::Lifecycle => "lifecycle",
Self::Positions => "positions",
Self::Combos => "combos",
Self::Transfers => "transfers",
Self::Accounts => "accounts",
Self::Wallets => "wallets",
Self::Markets => "markets",
}
}
}
impl fmt::Display for Channel {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
impl FromStr for Channel {
type Err = RadionError;
fn from_str(value: &str) -> Result<Self, Self::Err> {
CHANNELS
.into_iter()
.find(|channel| channel.as_str() == value)
.ok_or_else(|| RadionError::connection(format!("unknown channel \"{value}\"")))
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum ClobChannel {
Book,
Prices,
LastTrade,
Midpoint,
TickSize,
BestBidAsk,
}
pub const CLOB_CHANNELS: [ClobChannel; 6] = [
ClobChannel::Book,
ClobChannel::Prices,
ClobChannel::LastTrade,
ClobChannel::Midpoint,
ClobChannel::TickSize,
ClobChannel::BestBidAsk,
];
impl ClobChannel {
pub fn as_str(&self) -> &'static str {
match self {
Self::Book => "clob.book",
Self::Prices => "clob.prices",
Self::LastTrade => "clob.last_trade",
Self::Midpoint => "clob.midpoint",
Self::TickSize => "clob.tick_size",
Self::BestBidAsk => "clob.best_bid_ask",
}
}
}
impl fmt::Display for ClobChannel {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
impl FromStr for ClobChannel {
type Err = RadionError;
fn from_str(value: &str) -> Result<Self, Self::Err> {
CLOB_CHANNELS
.into_iter()
.find(|channel| channel.as_str() == value)
.ok_or_else(|| RadionError::connection(format!("unknown clob channel \"{value}\"")))
}
}
const CLOB_PREFIX: &str = "clob.";
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum SubscribableChannel {
Topic(Channel),
Clob(ClobChannel),
}
impl SubscribableChannel {
pub fn topic(&self) -> Option<Channel> {
match self {
Self::Topic(channel) => Some(*channel),
Self::Clob(_) => None,
}
}
pub fn is_clob(&self) -> bool {
matches!(self, Self::Clob(_))
}
pub(crate) fn filter_requirement(&self) -> Option<FilterRequirement> {
match self {
Self::Topic(channel) => filter_requirement(*channel),
Self::Clob(_) => Some(FilterRequirement {
required_any_of: &[FilterKey::TokenIds],
}),
}
}
}
impl fmt::Display for SubscribableChannel {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Topic(channel) => write!(f, "{channel}"),
Self::Clob(channel) => write!(f, "{channel}"),
}
}
}
impl FromStr for SubscribableChannel {
type Err = RadionError;
fn from_str(value: &str) -> Result<Self, Self::Err> {
if value.starts_with(CLOB_PREFIX) {
return value.parse().map(Self::Clob);
}
value.parse().map(Self::Topic)
}
}
impl From<Channel> for SubscribableChannel {
fn from(channel: Channel) -> Self {
Self::Topic(channel)
}
}
impl From<ClobChannel> for SubscribableChannel {
fn from(channel: ClobChannel) -> Self {
Self::Clob(channel)
}
}
impl Serialize for SubscribableChannel {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
serializer.serialize_str(&self.to_string())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FilterKey {
Wallets,
MarketIds,
TokenIds,
MinUsd,
}
impl FilterKey {
fn label(self) -> &'static str {
match self {
Self::Wallets => "wallets",
Self::MarketIds => "market_ids",
Self::TokenIds => "token_ids",
Self::MinUsd => "min_usd",
}
}
}
pub(crate) struct FilterRequirement {
pub required_any_of: &'static [FilterKey],
}
pub(crate) fn filter_requirement(channel: Channel) -> Option<FilterRequirement> {
match channel {
Channel::Markets => Some(FilterRequirement {
required_any_of: &[FilterKey::MarketIds, FilterKey::TokenIds],
}),
Channel::Wallets => Some(FilterRequirement {
required_any_of: &[FilterKey::Wallets],
}),
_ => None,
}
}
pub(crate) fn join_filter_keys(keys: &[FilterKey]) -> String {
keys.iter()
.map(|key| key.label())
.collect::<Vec<_>>()
.join(" or ")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn channel_roundtrips_through_str() {
for channel in CHANNELS {
assert_eq!(channel.as_str().parse::<Channel>().unwrap(), channel);
}
assert_eq!("trading".parse::<Channel>().unwrap(), Channel::Trading);
assert!("nope".parse::<Channel>().is_err());
}
#[test]
fn subscribable_channel_wraps_a_bare_topic() {
let topic: SubscribableChannel = Channel::Trading.into();
assert_eq!(topic, SubscribableChannel::Topic(Channel::Trading));
assert_eq!(topic.to_string(), "trading");
assert_eq!(topic.topic(), Some(Channel::Trading));
assert!(!topic.is_clob());
assert!("mempool.trading".parse::<SubscribableChannel>().is_err());
let parsed: SubscribableChannel = "trading".parse().unwrap();
assert_eq!(parsed, SubscribableChannel::Topic(Channel::Trading));
}
#[test]
fn clob_channel_roundtrips_through_str() {
for channel in CLOB_CHANNELS {
assert_eq!(channel.as_str().parse::<ClobChannel>().unwrap(), channel);
}
assert_eq!(
"clob.book".parse::<ClobChannel>().unwrap(),
ClobChannel::Book
);
assert_eq!(
"clob.best_bid_ask".parse::<ClobChannel>().unwrap(),
ClobChannel::BestBidAsk
);
assert!("clob.nope".parse::<ClobChannel>().is_err());
}
#[test]
fn subscribable_channel_handles_clob_prefix() {
let clob: SubscribableChannel = ClobChannel::LastTrade.into();
assert_eq!(clob, SubscribableChannel::Clob(ClobChannel::LastTrade));
assert_eq!(clob.to_string(), "clob.last_trade");
assert!(clob.is_clob());
assert_eq!(clob.topic(), None);
let parsed: SubscribableChannel = "clob.book".parse().unwrap();
assert_eq!(parsed, SubscribableChannel::Clob(ClobChannel::Book));
}
#[test]
fn filter_requirements_match_docs() {
assert!(filter_requirement(Channel::Markets).is_some());
assert!(filter_requirement(Channel::Wallets).is_some());
assert!(filter_requirement(Channel::Trading).is_none());
for channel in CLOB_CHANNELS {
let requirement = SubscribableChannel::Clob(channel)
.filter_requirement()
.expect("clob requires a filter");
assert_eq!(requirement.required_any_of, &[FilterKey::TokenIds]);
}
}
}