use std::sync::Arc;
use crate::protocol::ProtocolError;
use crate::protocol::request::SubscriptionMode;
use crate::protocol::response::{FilteringMode, MaxFrequency, Notification};
use crate::subscription::item_update::{ItemUpdate, SubscriptionSchema, UpdateKind};
use crate::subscription::update::ItemState;
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
#[non_exhaustive]
pub(crate) enum SubscriptionError {
#[error("`{tag}` arrived before the subscription was activated by `SUBOK`/`SUBCMD`")]
NotActivated {
tag: &'static str,
},
#[error("`{tag}` arrived after the subscription was ended by `UNSUB`")]
Ended {
tag: &'static str,
},
#[error("`{tag}` names item {item_index} of a {item_count}-item subscription")]
ItemOutOfRange {
tag: &'static str,
item_index: u64,
item_count: usize,
},
#[error(
"`SUBCMD` places the {role} field at position {position} of a {field_count}-field schema"
)]
CommandFieldOutOfRange {
role: &'static str,
position: u64,
field_count: usize,
},
#[error(transparent)]
Value(#[from] ProtocolError),
}
#[cold]
#[inline(never)]
fn item_out_of_range(tag: &'static str, item_index: u32, item_count: usize) -> SubscriptionError {
SubscriptionError::ItemOutOfRange {
tag,
item_index: u64::from(item_index),
item_count,
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub(crate) struct CommandFields {
pub(crate) key: usize,
pub(crate) command: usize,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum SubscriptionEvent {
Activated {
item_count: usize,
field_count: usize,
command_fields: Option<CommandFields>,
},
Update(ItemUpdate),
EndOfSnapshot {
item_index: usize,
},
SnapshotCleared {
item_index: usize,
},
Overflow {
item_index: usize,
dropped_count: u64,
},
Reconfigured {
max_frequency: MaxFrequency,
filtering: FilteringMode,
},
Unsubscribed,
}
#[must_use]
pub(crate) const fn classify_update(
snapshot_requested: bool,
mode: SubscriptionMode,
eos_received: bool,
first_notification: bool,
) -> UpdateKind {
if !snapshot_requested {
return UpdateKind::RealTime;
}
match mode {
SubscriptionMode::Raw => UpdateKind::RealTime,
SubscriptionMode::Merge => {
if first_notification {
UpdateKind::Snapshot
} else {
UpdateKind::RealTime
}
}
SubscriptionMode::Distinct | SubscriptionMode::Command => {
if eos_received {
UpdateKind::RealTime
} else {
UpdateKind::Snapshot
}
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
struct ItemTracker {
state: ItemState,
seen_update: bool,
end_of_snapshot: bool,
}
impl ItemTracker {
#[must_use]
fn new(field_count: usize) -> Self {
Self {
state: ItemState::new(field_count),
seen_update: false,
end_of_snapshot: false,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
struct Active {
schema: Arc<SubscriptionSchema>,
items: Vec<ItemTracker>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
enum Activation {
Pending,
Active(Active),
Ended,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct SubscriptionManager {
mode: SubscriptionMode,
snapshot_requested: bool,
declared_items: Vec<String>,
declared_fields: Vec<String>,
activation: Activation,
}
impl SubscriptionManager {
#[must_use]
pub(crate) fn new(
mode: SubscriptionMode,
snapshot_requested: bool,
declared_items: Vec<String>,
declared_fields: Vec<String>,
) -> Self {
Self {
mode,
snapshot_requested,
declared_items,
declared_fields,
activation: Activation::Pending,
}
}
#[allow(dead_code)]
#[must_use]
#[inline]
pub(crate) const fn mode(&self) -> SubscriptionMode {
self.mode
}
#[allow(dead_code)]
#[must_use]
#[inline]
pub(crate) const fn is_active(&self) -> bool {
matches!(self.activation, Activation::Active(_))
}
#[must_use]
#[inline]
pub(crate) const fn is_ended(&self) -> bool {
matches!(self.activation, Activation::Ended)
}
#[allow(dead_code)]
#[must_use]
#[inline]
pub(crate) fn item_count(&self) -> Option<usize> {
match &self.activation {
Activation::Active(active) => Some(active.schema.item_count()),
Activation::Pending | Activation::Ended => None,
}
}
#[allow(dead_code)]
#[must_use]
#[inline]
pub(crate) fn field_count(&self) -> Option<usize> {
match &self.activation {
Activation::Active(active) => Some(active.schema.field_count()),
Activation::Pending | Activation::Ended => None,
}
}
pub(crate) fn handle(
&mut self,
notification: &Notification,
) -> Result<Option<SubscriptionEvent>, SubscriptionError> {
match notification {
Notification::SubscriptionOk {
item_count,
field_count,
..
} => self.activate(*item_count, *field_count, None).map(Some),
Notification::SubscriptionCommandOk {
item_count,
field_count,
key_field_index,
command_field_index,
..
} => self
.activate(
*item_count,
*field_count,
Some((*key_field_index, *command_field_index)),
)
.map(Some),
Notification::Update {
item_index,
raw_values,
..
} => self.apply_update(*item_index, raw_values).map(Some),
Notification::EndOfSnapshot { item_index, .. } => {
let index = self.item_position("EOS", *item_index)?;
if let Activation::Active(active) = &mut self.activation
&& let Some(tracker) = active.items.get_mut(index)
{
tracker.end_of_snapshot = true;
}
Ok(Some(SubscriptionEvent::EndOfSnapshot {
item_index: to_one_based(index),
}))
}
Notification::ClearSnapshot { item_index, .. } => {
let index = self.item_position("CS", *item_index)?;
Ok(Some(SubscriptionEvent::SnapshotCleared {
item_index: to_one_based(index),
}))
}
Notification::Overflow {
item_index,
dropped_count,
..
} => {
let index = self.item_position("OV", *item_index)?;
Ok(Some(SubscriptionEvent::Overflow {
item_index: to_one_based(index),
dropped_count: *dropped_count,
}))
}
Notification::SubscriptionReconfigured {
max_frequency,
filtering,
..
} => {
if self.is_ended() {
return Err(SubscriptionError::Ended { tag: "CONF" });
}
Ok(Some(SubscriptionEvent::Reconfigured {
max_frequency: max_frequency.clone(),
filtering: filtering.clone(),
}))
}
Notification::Unsubscribed { .. } => {
self.activation = Activation::Ended;
Ok(Some(SubscriptionEvent::Unsubscribed))
}
_ => Ok(None),
}
}
fn activate(
&mut self,
item_count: u32,
field_count: u32,
command_fields: Option<(u32, u32)>,
) -> Result<SubscriptionEvent, SubscriptionError> {
let items = to_usize(item_count);
let fields = to_usize(field_count);
let zero_based = match command_fields {
Some((key, command)) => {
let key_index = command_field_position("key", key, fields)?;
let command_index = command_field_position("command", command, fields)?;
Some((key_index, command_index))
}
None => None,
};
let schema = Arc::new(SubscriptionSchema::new(
items,
fields,
&self.declared_items,
&self.declared_fields,
zero_based,
));
let mut trackers = Vec::with_capacity(items);
for _ in 0..items {
trackers.push(ItemTracker::new(fields));
}
self.activation = Activation::Active(Active {
schema,
items: trackers,
});
Ok(SubscriptionEvent::Activated {
item_count: items,
field_count: fields,
command_fields: zero_based.map(|(key, command)| CommandFields {
key: to_one_based(key),
command: to_one_based(command),
}),
})
}
fn apply_update(
&mut self,
item_index: u32,
raw_values: &str,
) -> Result<SubscriptionEvent, SubscriptionError> {
let snapshot_requested = self.snapshot_requested;
let mode = self.mode;
let active = match &mut self.activation {
Activation::Active(active) => active,
Activation::Pending => return Err(SubscriptionError::NotActivated { tag: "U" }),
Activation::Ended => return Err(SubscriptionError::Ended { tag: "U" }),
};
let item_count = active.items.len();
let index = item_position(item_count, "U", item_index)?;
let tracker = active
.items
.get_mut(index)
.ok_or_else(|| item_out_of_range("U", item_index, item_count))?;
let first_notification = !tracker.seen_update;
let kind = classify_update(
snapshot_requested,
mode,
tracker.end_of_snapshot,
first_notification,
);
let outcome = tracker.state.apply(raw_values)?;
tracker.seen_update = true;
let field_count = tracker.state.field_count();
let mut values = Vec::with_capacity(field_count);
for position in 0..field_count {
values.push(tracker.state.field(position).flatten().map(str::to_owned));
}
Ok(SubscriptionEvent::Update(ItemUpdate::new(
Arc::clone(&active.schema),
index,
kind,
values,
outcome.changed_fields().to_vec(),
)))
}
fn item_position(
&self,
tag: &'static str,
item_index: u32,
) -> Result<usize, SubscriptionError> {
match &self.activation {
Activation::Active(active) => item_position(active.items.len(), tag, item_index),
Activation::Pending => Err(SubscriptionError::NotActivated { tag }),
Activation::Ended => Err(SubscriptionError::Ended { tag }),
}
}
}
fn item_position(
item_count: usize,
tag: &'static str,
item_index: u32,
) -> Result<usize, SubscriptionError> {
let index = to_usize(item_index)
.checked_sub(1)
.ok_or_else(|| item_out_of_range(tag, item_index, item_count))?;
if index >= item_count {
return Err(item_out_of_range(tag, item_index, item_count));
}
Ok(index)
}
fn command_field_position(
role: &'static str,
position: u32,
field_count: usize,
) -> Result<usize, SubscriptionError> {
let out_of_range = || SubscriptionError::CommandFieldOutOfRange {
role,
position: u64::from(position),
field_count,
};
let index = to_usize(position).checked_sub(1).ok_or_else(out_of_range)?;
if index >= field_count {
return Err(out_of_range());
}
Ok(index)
}
#[must_use]
#[inline]
fn to_usize(value: u32) -> usize {
usize::try_from(value).unwrap_or(usize::MAX)
}
#[must_use]
#[inline]
const fn to_one_based(index: usize) -> usize {
index + 1
}
#[cfg(test)]
mod tests {
use super::*;
use crate::subscription::item_update::{FieldValue, ItemCommand};
fn names(values: &[&str]) -> Vec<String> {
values.iter().map(|value| (*value).to_owned()).collect()
}
fn subok(item_count: u32, field_count: u32) -> Notification {
Notification::SubscriptionOk {
subscription_id: 3,
item_count,
field_count,
}
}
fn subcmd(item_count: u32, field_count: u32, key: u32, command: u32) -> Notification {
Notification::SubscriptionCommandOk {
subscription_id: 3,
item_count,
field_count,
key_field_index: key,
command_field_index: command,
}
}
fn update(item_index: u32, raw_values: &str) -> Notification {
Notification::Update {
subscription_id: 3,
item_index,
raw_values: raw_values.to_owned(),
}
}
fn eos(item_index: u32) -> Notification {
Notification::EndOfSnapshot {
subscription_id: 3,
item_index,
}
}
fn clear_snapshot(item_index: u32) -> Notification {
Notification::ClearSnapshot {
subscription_id: 3,
item_index,
}
}
fn overflow(item_index: u32, dropped_count: u64) -> Notification {
Notification::Overflow {
subscription_id: 3,
item_index,
dropped_count,
}
}
fn conf() -> Notification {
Notification::SubscriptionReconfigured {
subscription_id: 3,
max_frequency: MaxFrequency::Limited {
updates_per_second: "3.0".to_owned(),
},
filtering: FilteringMode::Filtered,
}
}
fn unsub() -> Notification {
Notification::Unsubscribed { subscription_id: 3 }
}
fn expect_update(manager: &mut SubscriptionManager, notification: &Notification) -> ItemUpdate {
match manager.handle(notification) {
Ok(Some(SubscriptionEvent::Update(update))) => update,
other => panic!("expected an update event, got {other:?}"),
}
}
fn quote_manager(snapshot_requested: bool) -> SubscriptionManager {
let mut manager = SubscriptionManager::new(
SubscriptionMode::Merge,
snapshot_requested,
names(&["item1"]),
names(&[
"timestamp",
"price",
"change",
"minimum",
"maximum",
"bid",
"ask",
"open",
"close",
"status",
]),
);
assert!(manager.handle(&subok(1, 10)).is_ok());
manager
}
#[test]
fn test_classify_row_1_no_snapshot_requested_is_always_real_time_s2_4() {
for mode in [
SubscriptionMode::Raw,
SubscriptionMode::Merge,
SubscriptionMode::Distinct,
SubscriptionMode::Command,
] {
for eos_received in [false, true] {
for first in [false, true] {
assert_eq!(
classify_update(false, mode, eos_received, first),
UpdateKind::RealTime,
"mode {mode:?}, eos {eos_received}, first {first}"
);
}
}
}
}
#[test]
fn test_classify_row_2_raw_is_always_real_time_s2_4() {
for first in [false, true] {
assert_eq!(
classify_update(true, SubscriptionMode::Raw, false, first),
UpdateKind::RealTime
);
}
}
#[test]
fn test_classify_row_3_merge_first_notification_is_snapshot_s2_4() {
assert_eq!(
classify_update(true, SubscriptionMode::Merge, false, true),
UpdateKind::Snapshot
);
}
#[test]
fn test_classify_row_4_merge_later_notification_is_real_time_s2_4() {
assert_eq!(
classify_update(true, SubscriptionMode::Merge, false, false),
UpdateKind::RealTime
);
}
#[test]
fn test_classify_row_5_distinct_after_eos_is_real_time_s2_4() {
for first in [false, true] {
assert_eq!(
classify_update(true, SubscriptionMode::Distinct, true, first),
UpdateKind::RealTime
);
}
}
#[test]
fn test_classify_row_6_distinct_before_eos_is_snapshot_s2_4() {
for first in [false, true] {
assert_eq!(
classify_update(true, SubscriptionMode::Distinct, false, first),
UpdateKind::Snapshot
);
}
}
#[test]
fn test_classify_row_7_command_after_eos_is_real_time_s2_4() {
for first in [false, true] {
assert_eq!(
classify_update(true, SubscriptionMode::Command, true, first),
UpdateKind::RealTime
);
}
}
#[test]
fn test_classify_row_8_command_before_eos_is_snapshot_s2_4() {
for first in [false, true] {
assert_eq!(
classify_update(true, SubscriptionMode::Command, false, first),
UpdateKind::Snapshot
);
}
}
#[test]
fn test_merge_first_update_is_snapshot_then_real_time() {
let mut manager = quote_manager(true);
let first = expect_update(
&mut manager,
&update(1, "20:00:33|3.04|0.0|2.41|3.67|3.03|3.04|#|#|$"),
);
assert!(first.is_snapshot());
let second = expect_update(
&mut manager,
&update(1, "20:00:54|3.07|0.98|||3.06|3.07|||Suspended"),
);
assert_eq!(second.kind(), UpdateKind::RealTime);
}
#[test]
fn test_merge_without_snapshot_request_is_never_snapshot() {
let mut manager = quote_manager(false);
let first = expect_update(
&mut manager,
&update(1, "20:00:33|3.04|0.0|2.41|3.67|3.03|3.04|#|#|$"),
);
assert_eq!(first.kind(), UpdateKind::RealTime);
}
#[test]
fn test_merge_first_notification_is_tracked_per_item() {
let mut manager = SubscriptionManager::new(
SubscriptionMode::Merge,
true,
names(&["a", "b"]),
names(&["v"]),
);
assert!(manager.handle(&subok(2, 1)).is_ok());
assert!(expect_update(&mut manager, &update(1, "1")).is_snapshot());
assert!(expect_update(&mut manager, &update(2, "2")).is_snapshot());
assert!(!expect_update(&mut manager, &update(1, "3")).is_snapshot());
}
#[test]
fn test_distinct_snapshot_ends_at_eos() {
let mut manager = SubscriptionManager::new(
SubscriptionMode::Distinct,
true,
names(&["chat"]),
names(&["line"]),
);
assert!(manager.handle(&subok(1, 1)).is_ok());
assert!(expect_update(&mut manager, &update(1, "first")).is_snapshot());
assert!(expect_update(&mut manager, &update(1, "second")).is_snapshot());
assert_eq!(
manager.handle(&eos(1)),
Ok(Some(SubscriptionEvent::EndOfSnapshot { item_index: 1 }))
);
assert!(!expect_update(&mut manager, &update(1, "third")).is_snapshot());
}
#[test]
fn test_eos_is_tracked_per_item() {
let mut manager = SubscriptionManager::new(
SubscriptionMode::Distinct,
true,
names(&["a", "b"]),
names(&["v"]),
);
assert!(manager.handle(&subok(2, 1)).is_ok());
assert!(manager.handle(&eos(1)).is_ok());
assert!(!expect_update(&mut manager, &update(1, "x")).is_snapshot());
assert!(expect_update(&mut manager, &update(2, "y")).is_snapshot());
}
#[test]
fn test_raw_updates_are_real_time_even_before_any_eos() {
let mut manager =
SubscriptionManager::new(SubscriptionMode::Raw, true, names(&["tick"]), names(&["v"]));
assert!(manager.handle(&subok(1, 1)).is_ok());
assert!(!expect_update(&mut manager, &update(1, "x")).is_snapshot());
}
#[test]
fn test_subok_announces_the_shape_s3_1() {
let mut manager =
SubscriptionManager::new(SubscriptionMode::Merge, false, Vec::new(), Vec::new());
assert!(!manager.is_active());
assert_eq!(manager.field_count(), None);
assert_eq!(
manager.handle(&subok(1, 10)),
Ok(Some(SubscriptionEvent::Activated {
item_count: 1,
field_count: 10,
command_fields: None,
}))
);
assert!(manager.is_active());
assert_eq!(manager.item_count(), Some(1));
assert_eq!(manager.field_count(), Some(10));
assert_eq!(manager.mode(), SubscriptionMode::Merge);
}
#[test]
fn test_subcmd_announces_key_and_command_positions_s3_2() {
let mut manager =
SubscriptionManager::new(SubscriptionMode::Command, true, Vec::new(), Vec::new());
assert_eq!(
manager.handle(&subcmd(1, 10, 1, 2)),
Ok(Some(SubscriptionEvent::Activated {
item_count: 1,
field_count: 10,
command_fields: Some(CommandFields { key: 1, command: 2 }),
}))
);
}
#[test]
fn test_update_before_activation_is_an_error() {
let mut manager =
SubscriptionManager::new(SubscriptionMode::Merge, false, Vec::new(), Vec::new());
assert_eq!(
manager.handle(&update(1, "a|b")),
Err(SubscriptionError::NotActivated { tag: "U" })
);
assert_eq!(
manager.handle(&eos(1)),
Err(SubscriptionError::NotActivated { tag: "EOS" })
);
}
#[test]
fn test_reactivation_rebuilds_the_item_state() {
let mut manager = quote_manager(true);
let _ = expect_update(
&mut manager,
&update(1, "20:00:33|3.04|0.0|2.41|3.67|3.03|3.04|#|#|$"),
);
assert!(manager.handle(&subok(1, 10)).is_ok());
let after = expect_update(
&mut manager,
&update(1, "20:00:34|3.05|0.1|2.41|3.67|3.03|3.04|#|#|$"),
);
assert!(after.is_snapshot());
assert_eq!(after.changed_count(), 10);
}
#[test]
fn test_subcmd_with_out_of_range_key_is_an_error() {
let mut manager =
SubscriptionManager::new(SubscriptionMode::Command, true, Vec::new(), Vec::new());
assert_eq!(
manager.handle(&subcmd(1, 3, 4, 2)),
Err(SubscriptionError::CommandFieldOutOfRange {
role: "key",
position: 4,
field_count: 3,
})
);
assert_eq!(
manager.handle(&subcmd(1, 3, 0, 2)),
Err(SubscriptionError::CommandFieldOutOfRange {
role: "key",
position: 0,
field_count: 3,
})
);
assert_eq!(
manager.handle(&subcmd(1, 3, 1, 9)),
Err(SubscriptionError::CommandFieldOutOfRange {
role: "command",
position: 9,
field_count: 3,
})
);
}
#[test]
fn test_item_index_outside_the_announced_range_is_an_error() {
let mut manager = quote_manager(true);
assert!(matches!(
manager.handle(&update(0, "a")),
Err(SubscriptionError::ItemOutOfRange { .. })
));
assert!(matches!(
manager.handle(&update(2, "a")),
Err(SubscriptionError::ItemOutOfRange { .. })
));
assert!(matches!(
manager.handle(&eos(2)),
Err(SubscriptionError::ItemOutOfRange { tag: "EOS", .. })
));
assert!(matches!(
manager.handle(&clear_snapshot(7)),
Err(SubscriptionError::ItemOutOfRange { tag: "CS", .. })
));
assert!(matches!(
manager.handle(&overflow(7, 1)),
Err(SubscriptionError::ItemOutOfRange { tag: "OV", .. })
));
}
#[test]
fn test_unchanged_fields_are_absent_from_changed_fields_s2_5() {
let mut manager = quote_manager(true);
let _ = expect_update(
&mut manager,
&update(1, "20:00:33|3.04|0.0|2.41|3.67|3.03|3.04|#|#|$"),
);
let second = expect_update(
&mut manager,
&update(1, "20:00:54|3.07|0.98|||3.06|3.07|||Suspended"),
);
let changed: Vec<&str> = second.changed_fields().map(|field| field.name()).collect();
assert_eq!(
changed,
vec!["timestamp", "price", "change", "bid", "ask", "status"]
);
assert!(!second.is_field_changed_by_name("minimum"));
assert_eq!(
second.field_by_name("minimum"),
Some(FieldValue::Text("2.41"))
);
}
#[test]
fn test_caret_run_of_unchanged_fields_s2_5() {
let mut manager = quote_manager(true);
let _ = expect_update(
&mut manager,
&update(1, "20:00:33|3.04|0.0|2.41|3.67|3.03|3.04|#|#|$"),
);
let fourth = expect_update(&mut manager, &update(1, "20:04:40|^4|3.02|3.03|||"));
let changed: Vec<usize> = fourth
.changed_fields()
.map(|field| field.position())
.collect();
assert_eq!(changed, vec![1, 6, 7]);
assert_eq!(
fourth.field_by_name("price"),
Some(FieldValue::Text("3.04"))
);
}
#[test]
fn test_null_and_empty_survive_to_the_caller() {
let mut manager = quote_manager(true);
let first = expect_update(
&mut manager,
&update(1, "20:00:33|3.04|0.0|2.41|3.67|3.03|3.04|#|#|$"),
);
assert_eq!(first.field_by_name("open"), Some(FieldValue::Null));
assert_eq!(first.field_by_name("close"), Some(FieldValue::Null));
assert_eq!(first.field_by_name("status"), Some(FieldValue::Text("")));
assert_ne!(first.field_by_name("open"), first.field_by_name("status"));
assert!(first.is_field_changed_by_name("open"));
assert!(first.is_field_changed_by_name("status"));
assert_eq!(first.field_by_name("volume"), None);
}
#[test]
fn test_a_malformed_value_list_is_reported_and_changes_nothing() {
let mut manager = quote_manager(true);
let _ = expect_update(
&mut manager,
&update(1, "20:00:33|3.04|0.0|2.41|3.67|3.03|3.04|#|#|$"),
);
assert!(matches!(
manager.handle(&update(1, "a|b")),
Err(SubscriptionError::Value(ProtocolError::FieldValue { .. }))
));
let next = expect_update(&mut manager, &update(1, "^10"));
assert_eq!(next.field_by_name("price"), Some(FieldValue::Text("3.04")));
assert!(!next.is_snapshot());
}
fn portfolio_manager() -> SubscriptionManager {
let mut manager = SubscriptionManager::new(
SubscriptionMode::Command,
true,
names(&["portfolio1"]),
names(&["key", "command", "qty"]),
);
assert!(manager.handle(&subcmd(1, 3, 1, 2)).is_ok());
manager
}
#[test]
fn test_command_mode_add_update_delete() {
let mut manager = portfolio_manager();
let added = expect_update(&mut manager, &update(1, "AAPL|ADD|100"));
assert!(added.is_command_mode());
assert!(
added.is_snapshot(),
"no `EOS` yet, so still snapshot [§2.4]"
);
assert_eq!(added.key(), Some(FieldValue::Text("AAPL")));
assert_eq!(added.command(), Some(ItemCommand::Add));
assert_eq!(added.field_by_name("qty"), Some(FieldValue::Text("100")));
assert!(manager.handle(&eos(1)).is_ok());
let changed = expect_update(&mut manager, &update(1, "|UPDATE|120"));
assert!(!changed.is_snapshot());
assert_eq!(changed.key(), Some(FieldValue::Text("AAPL")));
assert_eq!(changed.command(), Some(ItemCommand::Update));
assert!(!changed.is_field_changed_by_name("key"));
assert_eq!(changed.field_by_name("qty"), Some(FieldValue::Text("120")));
let deleted = expect_update(&mut manager, &update(1, "|DELETE|"));
assert_eq!(deleted.command(), Some(ItemCommand::Delete));
assert_eq!(deleted.key(), Some(FieldValue::Text("AAPL")));
let after = expect_update(&mut manager, &update(1, "MSFT|ADD|5"));
assert_eq!(after.key(), Some(FieldValue::Text("MSFT")));
assert_eq!(after.field_by_name("qty"), Some(FieldValue::Text("5")));
}
#[test]
fn test_command_mode_null_command_field() {
let mut manager = portfolio_manager();
let update = expect_update(&mut manager, &update(1, "AAPL|#|100"));
assert_eq!(update.command(), None);
assert_eq!(update.key(), Some(FieldValue::Text("AAPL")));
}
#[test]
fn test_eos_does_not_disturb_field_values_s3_5() {
let mut manager = quote_manager(true);
let _ = expect_update(
&mut manager,
&update(1, "20:00:33|3.04|0.0|2.41|3.67|3.03|3.04|#|#|$"),
);
assert_eq!(
manager.handle(&eos(1)),
Ok(Some(SubscriptionEvent::EndOfSnapshot { item_index: 1 }))
);
let next = expect_update(&mut manager, &update(1, "^10"));
assert_eq!(next.field_by_name("price"), Some(FieldValue::Text("3.04")));
}
#[test]
fn test_clear_snapshot_is_reported_and_keeps_the_decoding_baseline_s3_6() {
let mut manager = SubscriptionManager::new(
SubscriptionMode::Distinct,
true,
names(&["chat"]),
names(&["line", "author"]),
);
assert!(manager.handle(&subok(1, 2)).is_ok());
let _ = expect_update(&mut manager, &update(1, "hello|ana"));
assert_eq!(
manager.handle(&clear_snapshot(1)),
Ok(Some(SubscriptionEvent::SnapshotCleared { item_index: 1 }))
);
let next = expect_update(&mut manager, &update(1, "bye|"));
assert_eq!(next.field_by_name("author"), Some(FieldValue::Text("ana")));
assert!(!next.is_field_changed_by_name("author"));
}
#[test]
fn test_overflow_is_reported_verbatim_s3_7() {
let mut manager = quote_manager(false);
assert_eq!(
manager.handle(&overflow(1, 5)),
Ok(Some(SubscriptionEvent::Overflow {
item_index: 1,
dropped_count: 5,
}))
);
}
#[test]
fn test_overflow_does_not_touch_item_state_s3_7() {
let mut manager = quote_manager(true);
let _ = expect_update(
&mut manager,
&update(1, "20:00:33|3.04|0.0|2.41|3.67|3.03|3.04|#|#|$"),
);
assert!(manager.handle(&overflow(1, 3)).is_ok());
let next = expect_update(&mut manager, &update(1, "^10"));
assert_eq!(next.field_by_name("price"), Some(FieldValue::Text("3.04")));
assert!(next.changed_fields().next().is_none());
}
#[test]
fn test_conf_is_accepted_before_activation_s3_8() {
let mut manager =
SubscriptionManager::new(SubscriptionMode::Merge, false, Vec::new(), Vec::new());
assert_eq!(
manager.handle(&conf()),
Ok(Some(SubscriptionEvent::Reconfigured {
max_frequency: MaxFrequency::Limited {
updates_per_second: "3.0".to_owned(),
},
filtering: FilteringMode::Filtered,
}))
);
assert!(!manager.is_active());
}
#[test]
fn test_unsub_ends_the_subscription_s3_4() {
let mut manager = quote_manager(true);
assert_eq!(
manager.handle(&unsub()),
Ok(Some(SubscriptionEvent::Unsubscribed))
);
assert!(manager.is_ended());
assert_eq!(
manager.handle(&update(1, "a")),
Err(SubscriptionError::Ended { tag: "U" })
);
assert_eq!(
manager.handle(&conf()),
Err(SubscriptionError::Ended { tag: "CONF" })
);
}
#[test]
fn test_non_subscription_notifications_are_ignored() {
let mut manager = quote_manager(true);
assert_eq!(manager.handle(&Notification::Probe), Ok(None));
assert_eq!(
manager.handle(&Notification::Sync {
elapsed_seconds: 120
}),
Ok(None)
);
assert_eq!(
manager.handle(&Notification::ServerName {
name: "Lightstreamer HTTP Server".to_owned()
}),
Ok(None)
);
}
#[test]
fn test_undeclared_names_report_positions() {
let mut manager =
SubscriptionManager::new(SubscriptionMode::Merge, false, Vec::new(), Vec::new());
assert!(manager.handle(&subok(2, 2)).is_ok());
let update = expect_update(&mut manager, &update(2, "a|b"));
assert_eq!(update.item_index(), 2);
assert_eq!(update.item_name(), "2");
assert_eq!(update.declared_item_name(), None);
assert_eq!(update.field_name(1), Some("1"));
}
#[test]
fn test_declared_names_reach_the_update() {
let mut manager = quote_manager(true);
let first = expect_update(
&mut manager,
&update(1, "20:00:33|3.04|0.0|2.41|3.67|3.03|3.04|#|#|$"),
);
assert_eq!(first.item_name(), "item1");
assert_eq!(first.field_name(10), Some("status"));
assert_eq!(first.field_position("status"), Some(10));
assert_eq!(first.field_position("nope"), None);
}
}