use std::num::NonZeroU32;
use crate::config::ConfigError;
use crate::session::{
DecimalNumber, RequestedBufferSize, RequestedMaxFrequency, Snapshot as WireSnapshot,
SubscriptionMode as WireMode, SubscriptionSpec,
};
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[repr(u8)]
pub enum SubscriptionMode {
Raw,
Merge,
Distinct,
Command,
}
impl SubscriptionMode {
#[must_use]
#[inline]
pub const fn as_str(self) -> &'static str {
match self {
Self::Raw => "RAW",
Self::Merge => "MERGE",
Self::Distinct => "DISTINCT",
Self::Command => "COMMAND",
}
}
pub(crate) const fn to_wire(self) -> WireMode {
match self {
Self::Raw => WireMode::Raw,
Self::Merge => WireMode::Merge,
Self::Distinct => WireMode::Distinct,
Self::Command => WireMode::Command,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct ItemGroup {
text: String,
listed: bool,
}
impl ItemGroup {
#[must_use = "a checked item group does nothing unless it is used"]
pub fn try_new(group: impl Into<String>) -> Result<Self, ConfigError> {
let group = group.into();
if group.trim().is_empty() {
return Err(ConfigError::EmptyItemGroup);
}
if group.chars().any(char::is_control) {
return Err(ConfigError::ItemGroupControlCharacter);
}
Ok(Self {
text: group,
listed: false,
})
}
#[must_use = "a checked item group does nothing unless it is used"]
pub fn from_items<I, S>(items: I) -> Result<Self, ConfigError>
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
let names: Vec<String> = items.into_iter().map(Into::into).collect();
if names.is_empty() {
return Err(ConfigError::EmptyItemGroup);
}
for name in &names {
if name.is_empty() || name.chars().any(char::is_whitespace) {
return Err(ConfigError::ItemNameHasWhitespace { name: name.clone() });
}
if name.chars().any(char::is_control) {
return Err(ConfigError::ItemGroupControlCharacter);
}
}
Ok(Self {
text: names.join(" "),
listed: true,
})
}
#[must_use]
#[inline]
pub fn as_str(&self) -> &str {
&self.text
}
#[must_use]
pub fn names(&self) -> Vec<&str> {
if self.listed {
self.text.split_whitespace().collect()
} else {
Vec::new()
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct FieldSchema {
text: String,
listed: bool,
}
impl FieldSchema {
#[must_use = "a checked field schema does nothing unless it is used"]
pub fn try_new(schema: impl Into<String>) -> Result<Self, ConfigError> {
let schema = schema.into();
if schema.trim().is_empty() {
return Err(ConfigError::EmptyFieldSchema);
}
if schema.chars().any(char::is_control) {
return Err(ConfigError::FieldSchemaControlCharacter);
}
Ok(Self {
text: schema,
listed: false,
})
}
#[must_use = "a checked field schema does nothing unless it is used"]
pub fn from_fields<I, S>(fields: I) -> Result<Self, ConfigError>
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
let names: Vec<String> = fields.into_iter().map(Into::into).collect();
if names.is_empty() {
return Err(ConfigError::EmptyFieldSchema);
}
for name in &names {
if name.is_empty() || name.chars().any(char::is_whitespace) {
return Err(ConfigError::FieldNameHasWhitespace { name: name.clone() });
}
if name.chars().any(char::is_control) {
return Err(ConfigError::FieldSchemaControlCharacter);
}
}
Ok(Self {
text: names.join(" "),
listed: true,
})
}
#[must_use]
#[inline]
pub fn as_str(&self) -> &str {
&self.text
}
#[must_use]
pub fn names(&self) -> Vec<&str> {
if self.listed {
self.text.split_whitespace().collect()
} else {
Vec::new()
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
#[repr(u8)]
pub enum Snapshot {
#[default]
Off,
On,
Length(NonZeroU32),
}
impl Snapshot {
#[must_use]
#[inline]
pub const fn is_requested(self) -> bool {
!matches!(self, Self::Off)
}
const fn to_wire(self) -> WireSnapshot {
match self {
Self::Off => WireSnapshot::Off,
Self::On => WireSnapshot::On,
Self::Length(n) => WireSnapshot::Length(n),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
pub enum MaxFrequency {
#[default]
Unlimited,
Unfiltered,
Limited(FrequencyLimit),
}
impl MaxFrequency {
pub fn limited(updates_per_second: impl Into<String>) -> Result<Self, ConfigError> {
let text = updates_per_second.into();
DecimalNumber::try_new(text.clone())
.map(|number| Self::Limited(FrequencyLimit(number)))
.map_err(|_| ConfigError::InvalidFrequency { value: text })
}
fn to_wire(&self) -> RequestedMaxFrequency {
match self {
Self::Unlimited => RequestedMaxFrequency::Unlimited,
Self::Unfiltered => RequestedMaxFrequency::Unfiltered,
Self::Limited(limit) => RequestedMaxFrequency::Limited(limit.0.clone()),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct FrequencyLimit(DecimalNumber);
impl FrequencyLimit {
#[must_use]
#[inline]
pub fn as_str(&self) -> &str {
self.0.as_str()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
pub enum BufferSize {
#[default]
ServerDecides,
Unlimited,
Events(NonZeroU32),
}
impl BufferSize {
const fn to_wire(self) -> Option<RequestedBufferSize> {
match self {
Self::ServerDecides => None,
Self::Unlimited => Some(RequestedBufferSize::Unlimited),
Self::Events(n) => Some(RequestedBufferSize::Events(n)),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Subscription {
mode: SubscriptionMode,
items: ItemGroup,
fields: FieldSchema,
data_adapter: Option<String>,
selector: Option<String>,
snapshot: Snapshot,
max_frequency: MaxFrequency,
buffer_size: BufferSize,
}
impl Subscription {
#[must_use = "builders do nothing unless the subscription is used"]
pub fn new(mode: SubscriptionMode, items: ItemGroup, fields: FieldSchema) -> Self {
Self {
mode,
items,
fields,
data_adapter: None,
selector: None,
snapshot: Snapshot::default(),
max_frequency: MaxFrequency::default(),
buffer_size: BufferSize::default(),
}
}
#[must_use = "builders do nothing unless the subscription is used"]
pub fn with_data_adapter(mut self, name: impl Into<String>) -> Self {
self.data_adapter = Some(name.into());
self
}
#[must_use = "builders do nothing unless the subscription is used"]
pub fn with_selector(mut self, name: impl Into<String>) -> Self {
self.selector = Some(name.into());
self
}
#[must_use = "builders do nothing unless the subscription is used"]
pub const fn with_snapshot(mut self, snapshot: Snapshot) -> Self {
self.snapshot = snapshot;
self
}
#[must_use = "builders do nothing unless the subscription is used"]
pub fn with_max_frequency(mut self, frequency: MaxFrequency) -> Self {
self.max_frequency = frequency;
self
}
#[must_use = "builders do nothing unless the subscription is used"]
pub const fn with_buffer_size(mut self, size: BufferSize) -> Self {
self.buffer_size = size;
self
}
#[must_use]
#[inline]
pub const fn mode(&self) -> SubscriptionMode {
self.mode
}
#[must_use]
#[inline]
pub const fn items(&self) -> &ItemGroup {
&self.items
}
#[must_use]
#[inline]
pub const fn fields(&self) -> &FieldSchema {
&self.fields
}
#[must_use]
#[inline]
pub const fn snapshot(&self) -> Snapshot {
self.snapshot
}
pub fn validate(&self) -> Result<(), ConfigError> {
let filtered = !matches!(self.mode, SubscriptionMode::Raw);
if matches!(self.snapshot, Snapshot::Length(_)) && self.mode != SubscriptionMode::Distinct {
return Err(ConfigError::SnapshotLengthNeedsDistinct {
mode: self.mode.as_str(),
});
}
if self.snapshot.is_requested() && !filtered {
return Err(ConfigError::SnapshotNeedsAStatefulMode);
}
if !matches!(self.max_frequency, MaxFrequency::Unlimited) && !filtered {
return Err(ConfigError::FrequencyNeedsAFilteredMode);
}
if !matches!(self.buffer_size, BufferSize::ServerDecides) {
if !matches!(
self.mode,
SubscriptionMode::Merge | SubscriptionMode::Distinct
) {
return Err(ConfigError::BufferSizeNeedsMergeOrDistinct {
mode: self.mode.as_str(),
});
}
if matches!(self.max_frequency, MaxFrequency::Unfiltered) {
return Err(ConfigError::BufferSizeWithUnfilteredFrequency);
}
}
Ok(())
}
pub(crate) fn declared_items(&self) -> Vec<String> {
self.items.names().into_iter().map(str::to_owned).collect()
}
pub(crate) fn declared_fields(&self) -> Vec<String> {
self.fields.names().into_iter().map(str::to_owned).collect()
}
pub(crate) fn into_spec(self) -> SubscriptionSpec {
let declared_items = self.declared_items();
let declared_fields = self.declared_fields();
SubscriptionSpec {
declared_items,
declared_fields,
group: self.items.text,
schema: self.fields.text,
mode: self.mode.to_wire(),
data_adapter: self.data_adapter,
selector: self.selector,
requested_buffer_size: self.buffer_size.to_wire(),
requested_max_frequency: match self.max_frequency {
MaxFrequency::Unlimited => None,
ref other => Some(other.to_wire()),
},
snapshot: match self.snapshot {
Snapshot::Off => None,
other => Some(other.to_wire()),
},
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn group() -> ItemGroup {
match ItemGroup::from_items(["item1", "item2"]) {
Ok(group) => group,
Err(error) => unreachable!("the fixture group is valid: {error}"),
}
}
fn schema() -> FieldSchema {
match FieldSchema::from_fields(["last_price", "time"]) {
Ok(schema) => schema,
Err(error) => unreachable!("the fixture schema is valid: {error}"),
}
}
#[test]
fn test_item_group_joins_a_list_with_spaces() {
assert!(matches!(
ItemGroup::from_items(["a", "b", "c"]),
Ok(group) if group.as_str() == "a b c"
));
}
#[test]
fn test_item_group_rejects_an_empty_list() {
let empty: [&str; 0] = [];
assert!(matches!(
ItemGroup::from_items(empty),
Err(ConfigError::EmptyItemGroup)
));
}
#[test]
fn test_item_group_rejects_a_name_with_whitespace() {
assert!(matches!(
ItemGroup::from_items(["ok", "not ok"]),
Err(ConfigError::ItemNameHasWhitespace { name }) if name == "not ok"
));
}
#[test]
fn test_item_group_rejects_an_empty_name_in_a_list() {
assert!(matches!(
ItemGroup::from_items(["ok", ""]),
Err(ConfigError::ItemNameHasWhitespace { .. })
));
}
#[test]
fn test_item_group_rejects_an_empty_group_name() {
assert!(matches!(
ItemGroup::try_new(" "),
Err(ConfigError::EmptyItemGroup)
));
}
#[test]
fn test_item_group_rejects_a_control_character() {
assert!(matches!(
ItemGroup::try_new("a\rb"),
Err(ConfigError::ItemGroupControlCharacter)
));
}
#[test]
fn test_field_schema_rejects_every_bad_shape() {
let empty: [&str; 0] = [];
assert!(matches!(
FieldSchema::from_fields(empty),
Err(ConfigError::EmptyFieldSchema)
));
assert!(matches!(
FieldSchema::from_fields(["a b"]),
Err(ConfigError::FieldNameHasWhitespace { .. })
));
assert!(matches!(
FieldSchema::try_new(""),
Err(ConfigError::EmptyFieldSchema)
));
assert!(matches!(
FieldSchema::try_new("a\nb"),
Err(ConfigError::FieldSchemaControlCharacter)
));
}
#[test]
fn test_field_schema_lists_its_names() {
assert_eq!(schema().names(), vec!["last_price", "time"]);
}
#[test]
fn test_max_frequency_accepts_a_decimal_number() {
assert!(MaxFrequency::limited("2").is_ok());
assert!(MaxFrequency::limited("0.5").is_ok());
}
#[test]
fn test_max_frequency_rejects_anything_that_is_not_one() {
for bad in ["2,0", ".5", "5.", "1e3", "-1", ""] {
assert!(
matches!(
MaxFrequency::limited(bad),
Err(ConfigError::InvalidFrequency { .. })
),
"{bad} was accepted"
);
}
}
#[test]
fn test_snapshot_knows_when_it_asks_for_one() {
assert!(!Snapshot::Off.is_requested());
assert!(Snapshot::On.is_requested());
assert!(Snapshot::Length(NonZeroU32::MIN).is_requested());
}
#[test]
fn test_subscription_defaults_omit_every_optional_parameter() {
let spec = Subscription::new(SubscriptionMode::Merge, group(), schema()).into_spec();
assert_eq!(spec.group, "item1 item2");
assert_eq!(spec.schema, "last_price time");
assert_eq!(spec.mode, WireMode::Merge);
assert_eq!(spec.data_adapter, None);
assert_eq!(spec.selector, None);
assert_eq!(spec.snapshot, None);
assert_eq!(spec.requested_max_frequency, None);
assert_eq!(spec.requested_buffer_size, None);
}
#[test]
fn test_subscription_carries_every_option_it_was_given() {
let frequency = match MaxFrequency::limited("2.5") {
Ok(frequency) => frequency,
Err(error) => unreachable!("the fixture frequency is valid: {error}"),
};
let spec = Subscription::new(SubscriptionMode::Distinct, group(), schema())
.with_data_adapter("QUOTE_ADAPTER")
.with_selector("only_big")
.with_snapshot(Snapshot::Length(NonZeroU32::MIN))
.with_max_frequency(frequency)
.with_buffer_size(BufferSize::Unlimited)
.into_spec();
assert_eq!(spec.mode, WireMode::Distinct);
assert_eq!(spec.data_adapter.as_deref(), Some("QUOTE_ADAPTER"));
assert_eq!(spec.selector.as_deref(), Some("only_big"));
assert_eq!(spec.snapshot, Some(WireSnapshot::Length(NonZeroU32::MIN)));
assert_eq!(
spec.requested_buffer_size,
Some(RequestedBufferSize::Unlimited)
);
assert!(matches!(
spec.requested_max_frequency,
Some(RequestedMaxFrequency::Limited(_))
));
}
fn subscription(mode: SubscriptionMode) -> Subscription {
Subscription::new(mode, group(), schema())
}
#[test]
fn test_a_plain_subscription_validates_in_every_mode() {
for mode in [
SubscriptionMode::Raw,
SubscriptionMode::Merge,
SubscriptionMode::Distinct,
SubscriptionMode::Command,
] {
assert!(
subscription(mode).validate().is_ok(),
"{} was rejected",
mode.as_str()
);
}
}
#[test]
fn test_a_snapshot_length_is_refused_outside_distinct_mode() {
for mode in [
SubscriptionMode::Raw,
SubscriptionMode::Merge,
SubscriptionMode::Command,
] {
let built = subscription(mode).with_snapshot(Snapshot::Length(NonZeroU32::MIN));
assert!(
matches!(
built.validate(),
Err(ConfigError::SnapshotLengthNeedsDistinct { mode: named }) if named == mode.as_str()
),
"{} accepted a snapshot length",
mode.as_str()
);
}
assert!(
subscription(SubscriptionMode::Distinct)
.with_snapshot(Snapshot::Length(NonZeroU32::MIN))
.validate()
.is_ok()
);
}
#[test]
fn test_a_snapshot_is_refused_in_raw_mode() {
assert!(matches!(
subscription(SubscriptionMode::Raw)
.with_snapshot(Snapshot::On)
.validate(),
Err(ConfigError::SnapshotNeedsAStatefulMode)
));
assert!(
subscription(SubscriptionMode::Raw)
.with_snapshot(Snapshot::Off)
.validate()
.is_ok()
);
}
#[test]
fn test_a_frequency_is_refused_in_raw_mode() {
let Ok(limited) = MaxFrequency::limited("2.0") else {
unreachable!("the fixture frequency is valid");
};
for frequency in [MaxFrequency::Unfiltered, limited] {
assert!(
matches!(
subscription(SubscriptionMode::Raw)
.with_max_frequency(frequency.clone())
.validate(),
Err(ConfigError::FrequencyNeedsAFilteredMode)
),
"{frequency:?} was accepted with RAW"
);
}
assert!(
subscription(SubscriptionMode::Raw)
.with_max_frequency(MaxFrequency::Unlimited)
.validate()
.is_ok()
);
}
#[test]
fn test_a_buffer_size_is_refused_outside_merge_and_distinct() {
for mode in [SubscriptionMode::Raw, SubscriptionMode::Command] {
assert!(
matches!(
subscription(mode)
.with_buffer_size(BufferSize::Unlimited)
.validate(),
Err(ConfigError::BufferSizeNeedsMergeOrDistinct { mode: named }) if named == mode.as_str()
),
"{} accepted a buffer size",
mode.as_str()
);
}
for mode in [SubscriptionMode::Merge, SubscriptionMode::Distinct] {
assert!(
subscription(mode)
.with_buffer_size(BufferSize::Events(NonZeroU32::MIN))
.validate()
.is_ok(),
"{} rejected a buffer size",
mode.as_str()
);
}
}
#[test]
fn test_a_buffer_size_is_refused_alongside_an_unfiltered_frequency() {
assert!(matches!(
subscription(SubscriptionMode::Merge)
.with_max_frequency(MaxFrequency::Unfiltered)
.with_buffer_size(BufferSize::Unlimited)
.validate(),
Err(ConfigError::BufferSizeWithUnfilteredFrequency)
));
assert!(
subscription(SubscriptionMode::Merge)
.with_max_frequency(MaxFrequency::Unfiltered)
.validate()
.is_ok()
);
}
#[test]
fn test_subscription_mode_spells_itself_the_way_the_protocol_does() {
assert_eq!(SubscriptionMode::Raw.as_str(), "RAW");
assert_eq!(SubscriptionMode::Merge.as_str(), "MERGE");
assert_eq!(SubscriptionMode::Distinct.as_str(), "DISTINCT");
assert_eq!(SubscriptionMode::Command.as_str(), "COMMAND");
}
}