use std::str::FromStr;
use crate::protocol::ProtocolError;
use crate::protocol::escaping::percent_decode;
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum Notification {
Update {
subscription_id: u32,
item_index: u32,
raw_values: String,
},
SubscriptionOk {
subscription_id: u32,
item_count: u32,
field_count: u32,
},
SubscriptionCommandOk {
subscription_id: u32,
item_count: u32,
field_count: u32,
key_field_index: u32,
command_field_index: u32,
},
Unsubscribed {
subscription_id: u32,
},
EndOfSnapshot {
subscription_id: u32,
item_index: u32,
},
ClearSnapshot {
subscription_id: u32,
item_index: u32,
},
Overflow {
subscription_id: u32,
item_index: u32,
dropped_count: u64,
},
SubscriptionReconfigured {
subscription_id: u32,
max_frequency: MaxFrequency,
filtering: FilteringMode,
},
MessageDone {
sequence: MessageSequence,
prog: u64,
response: String,
},
MessageFailed {
sequence: MessageSequence,
prog: u64,
code: i64,
message: String,
},
ConstraintsChanged {
bandwidth: Bandwidth,
},
Sync {
elapsed_seconds: u64,
},
ClientIp {
address: String,
},
ServerName {
name: String,
},
Progressive {
progressive: u64,
},
NoOp {
preamble: String,
},
Probe,
Loop {
expected_delay_millis: u64,
},
End {
cause_code: i64,
cause_message: String,
},
ConnectionOk {
session_id: String,
request_limit_bytes: u64,
keep_alive_millis: u64,
control_link: Option<String>,
},
ConnectionError {
code: i64,
message: String,
},
RequestOk {
request_id: Option<String>,
},
RequestError {
request_id: String,
code: i64,
message: String,
},
Error {
code: i64,
message: String,
},
WsOk,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum MaxFrequency {
Limited {
updates_per_second: String,
},
Unlimited,
Unrecognized {
literal: String,
},
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum FilteringMode {
Filtered,
Unfiltered,
Unrecognized {
literal: String,
},
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum Bandwidth {
Limited {
kbps: String,
},
Unlimited,
Unmanaged,
Unrecognized {
literal: String,
},
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum MessageSequence {
Unspecified,
Named(String),
}
#[must_use = "a parsed line carries the server's instruction; dropping it loses it"]
pub(crate) fn parse_line(line: &str) -> Result<Notification, ProtocolError> {
let line = strip_terminator(line);
let (tag, rest) = match line.split_once(',') {
Some((tag, rest)) => (tag, Some(rest)),
None => (line, None),
};
match tag {
"U" => {
let [subscription_id, item_index, raw_values] = split_fixed::<3>(tag, rest)?;
Ok(Notification::Update {
subscription_id: number(tag, "subscription-ID", subscription_id)?,
item_index: number(tag, "item", item_index)?,
raw_values: raw_values.to_owned(),
})
}
"SUBOK" => {
let [subscription_id, item_count, field_count] = split_fixed::<3>(tag, rest)?;
Ok(Notification::SubscriptionOk {
subscription_id: number(tag, "subscription-ID", subscription_id)?,
item_count: number(tag, "num-items", item_count)?,
field_count: number(tag, "num-fields", field_count)?,
})
}
"SUBCMD" => {
let [
subscription_id,
item_count,
field_count,
key_field,
command_field,
] = split_fixed::<5>(tag, rest)?;
Ok(Notification::SubscriptionCommandOk {
subscription_id: number(tag, "subscription-ID", subscription_id)?,
item_count: number(tag, "num-items", item_count)?,
field_count: number(tag, "num-fields", field_count)?,
key_field_index: number(tag, "key-field", key_field)?,
command_field_index: number(tag, "command-field", command_field)?,
})
}
"UNSUB" => {
let [subscription_id] = split_fixed::<1>(tag, rest)?;
Ok(Notification::Unsubscribed {
subscription_id: number(tag, "subscription-ID", subscription_id)?,
})
}
"EOS" => {
let [subscription_id, item_index] = split_fixed::<2>(tag, rest)?;
Ok(Notification::EndOfSnapshot {
subscription_id: number(tag, "subscription-ID", subscription_id)?,
item_index: number(tag, "item", item_index)?,
})
}
"CS" => {
let [subscription_id, item_index] = split_fixed::<2>(tag, rest)?;
Ok(Notification::ClearSnapshot {
subscription_id: number(tag, "subscription-ID", subscription_id)?,
item_index: number(tag, "item", item_index)?,
})
}
"OV" => {
let [subscription_id, item_index, overflow_size] = split_fixed::<3>(tag, rest)?;
Ok(Notification::Overflow {
subscription_id: number(tag, "subscription-ID", subscription_id)?,
item_index: number(tag, "item", item_index)?,
dropped_count: number(tag, "overflow-size", overflow_size)?,
})
}
"CONF" => {
let [subscription_id, max_frequency, filtering] = split_fixed::<3>(tag, rest)?;
Ok(Notification::SubscriptionReconfigured {
subscription_id: number(tag, "subscription-ID", subscription_id)?,
max_frequency: parse_max_frequency(&decode(max_frequency)?),
filtering: parse_filtering(decode(filtering)?),
})
}
"MSGDONE" => {
let [sequence, prog, response] = split_fixed::<3>(tag, rest)?;
Ok(Notification::MessageDone {
sequence: parse_sequence(decode(sequence)?),
prog: number(tag, "prog", prog)?,
response: decode(response)?,
})
}
"MSGFAIL" => {
let [sequence, prog, code, message] = split_fixed::<4>(tag, rest)?;
Ok(Notification::MessageFailed {
sequence: parse_sequence(decode(sequence)?),
prog: number(tag, "prog", prog)?,
code: number(tag, "error-code", code)?,
message: decode(message)?,
})
}
"CONS" => {
let [bandwidth] = split_fixed::<1>(tag, rest)?;
Ok(Notification::ConstraintsChanged {
bandwidth: parse_bandwidth(&decode(bandwidth)?),
})
}
"SYNC" => {
let [seconds] = split_fixed::<1>(tag, rest)?;
Ok(Notification::Sync {
elapsed_seconds: number(tag, "seconds-since-initial-header", seconds)?,
})
}
"CLIENTIP" => {
let [address] = split_fixed::<1>(tag, rest)?;
Ok(Notification::ClientIp {
address: decode(address)?,
})
}
"SERVNAME" => {
let [name] = split_fixed::<1>(tag, rest)?;
Ok(Notification::ServerName {
name: decode(name)?,
})
}
"PROG" => {
let [progressive] = split_fixed::<1>(tag, rest)?;
Ok(Notification::Progressive {
progressive: number(tag, "progressive", progressive)?,
})
}
"NOOP" => {
let [preamble] = split_fixed::<1>(tag, rest)?;
Ok(Notification::NoOp {
preamble: decode(preamble)?,
})
}
"PROBE" => {
let [] = split_fixed::<0>(tag, rest)?;
Ok(Notification::Probe)
}
"LOOP" => {
let [delay] = split_fixed::<1>(tag, rest)?;
Ok(Notification::Loop {
expected_delay_millis: number(tag, "expected-delay", delay)?,
})
}
"END" => {
let [cause_code, cause_message] = split_fixed::<2>(tag, rest)?;
Ok(Notification::End {
cause_code: number(tag, "cause-code", cause_code)?,
cause_message: decode(cause_message)?,
})
}
"CONOK" => {
let [session_id, request_limit, keep_alive, control_link] =
split_fixed::<4>(tag, rest)?;
let control_link = decode(control_link)?;
Ok(Notification::ConnectionOk {
session_id: decode(session_id)?,
request_limit_bytes: number(tag, "request-limit", request_limit)?,
keep_alive_millis: number(tag, "keep-alive", keep_alive)?,
control_link: if control_link == "*" {
None
} else {
Some(control_link)
},
})
}
"CONERR" => {
let [code, message] = split_fixed::<2>(tag, rest)?;
Ok(Notification::ConnectionError {
code: number(tag, "error-code", code)?,
message: decode(message)?,
})
}
"REQOK" => match rest {
None => Ok(Notification::RequestOk { request_id: None }),
Some(_) => {
let [request_id] = split_fixed::<1>(tag, rest)?;
Ok(Notification::RequestOk {
request_id: Some(decode(request_id)?),
})
}
},
"REQERR" => {
let [request_id, code, message] = split_fixed::<3>(tag, rest)?;
Ok(Notification::RequestError {
request_id: decode(request_id)?,
code: number(tag, "error-code", code)?,
message: decode(message)?,
})
}
"ERROR" => {
let [code, message] = split_fixed::<2>(tag, rest)?;
Ok(Notification::Error {
code: number(tag, "error-code", code)?,
message: decode(message)?,
})
}
"WSOK" => {
let [] = split_fixed::<0>(tag, rest)?;
Ok(Notification::WsOk)
}
_ => Err(ProtocolError::UnknownTag {
tag: tag.to_owned(),
line: line.to_owned(),
}),
}
}
#[inline]
fn strip_terminator(line: &str) -> &str {
match line.strip_suffix("\r\n") {
Some(trimmed) => trimmed,
None => match line.strip_suffix('\n') {
Some(trimmed) => trimmed,
None => line.strip_suffix('\r').unwrap_or(line),
},
}
}
fn split_fixed<'a, const N: usize>(
tag: &str,
rest: Option<&'a str>,
) -> Result<[&'a str; N], ProtocolError> {
let Some(rest) = rest else {
if N == 0 {
return Ok([""; N]);
}
return Err(argument_count(tag, N, 0));
};
if N == 0 {
return Err(argument_count(tag, 0, rest.split(',').count()));
}
let mut arguments = [""; N];
let mut parts = rest.splitn(N, ',');
for (index, slot) in arguments.iter_mut().enumerate() {
match parts.next() {
Some(part) => *slot = part,
None => return Err(argument_count(tag, N, index)),
}
}
Ok(arguments)
}
#[inline]
fn decode(argument: &str) -> Result<String, ProtocolError> {
Ok(percent_decode(argument)?.into_owned())
}
#[inline]
fn number<T: FromStr>(tag: &str, argument: &'static str, value: &str) -> Result<T, ProtocolError> {
value
.parse::<T>()
.map_err(|_| not_numeric(tag, argument, value))
}
#[must_use]
fn parse_max_frequency(argument: &str) -> MaxFrequency {
if argument == "unlimited" {
MaxFrequency::Unlimited
} else if is_decimal_number(argument) {
MaxFrequency::Limited {
updates_per_second: argument.to_owned(),
}
} else {
MaxFrequency::Unrecognized {
literal: argument.to_owned(),
}
}
}
#[must_use]
fn parse_bandwidth(argument: &str) -> Bandwidth {
match argument {
"unlimited" => Bandwidth::Unlimited,
"unmanaged" => Bandwidth::Unmanaged,
_ if is_decimal_number(argument) => Bandwidth::Limited {
kbps: argument.to_owned(),
},
_ => Bandwidth::Unrecognized {
literal: argument.to_owned(),
},
}
}
#[must_use]
fn is_decimal_number(argument: &str) -> bool {
crate::protocol::request::DecimalNumber::try_new(argument).is_ok()
}
#[must_use]
fn parse_filtering(argument: String) -> FilteringMode {
match argument.as_str() {
"filtered" => FilteringMode::Filtered,
"unfiltered" => FilteringMode::Unfiltered,
_ => FilteringMode::Unrecognized { literal: argument },
}
}
#[must_use]
fn parse_sequence(argument: String) -> MessageSequence {
if argument == "*" {
MessageSequence::Unspecified
} else {
MessageSequence::Named(argument)
}
}
#[cold]
#[inline(never)]
fn argument_count(tag: &str, expected: usize, found: usize) -> ProtocolError {
ProtocolError::ArgumentCount {
tag: tag.to_owned(),
expected,
found,
}
}
#[cold]
#[inline(never)]
fn not_numeric(tag: &str, argument: &'static str, value: &str) -> ProtocolError {
ProtocolError::NotNumeric {
tag: tag.to_owned(),
argument,
value: value.to_owned(),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_line_update_spec_example_ok() {
let line = "U,3,1,20:00:33|3.04|0.0|2.41|3.67|3.03|3.04|#|#|$";
assert_eq!(
parse_line(line),
Ok(Notification::Update {
subscription_id: 3,
item_index: 1,
raw_values: "20:00:33|3.04|0.0|2.41|3.67|3.03|3.04|#|#|$".to_owned(),
})
);
}
#[test]
fn test_parse_line_update_unchanged_run_ok() {
assert_eq!(
parse_line("U,3,1,20:04:40|^4|3.02|3.03|||"),
Ok(Notification::Update {
subscription_id: 3,
item_index: 1,
raw_values: "20:04:40|^4|3.02|3.03|||".to_owned(),
})
);
}
#[test]
fn test_parse_line_update_raw_values_preserved_byte_for_byte() {
let values = "20:00:54|^P[{\"op\":\"replace\",\"path\":\"/text\",\"value\":\"aa%7Cbb%7Ccc\"},{\"op\":\"add\",\"path\":\"/attributes/1\",\"value\":\"bold\"}]";
let line = format!("U,5,1,{values}");
assert_eq!(
parse_line(&line),
Ok(Notification::Update {
subscription_id: 5,
item_index: 1,
raw_values: values.to_owned(),
})
);
}
#[test]
fn test_parse_line_update_empty_value_list_ok() {
assert_eq!(
parse_line("U,5,1,20:04:16|"),
Ok(Notification::Update {
subscription_id: 5,
item_index: 1,
raw_values: "20:04:16|".to_owned(),
})
);
}
#[test]
fn test_parse_line_update_missing_values_argument_is_error() {
assert_eq!(
parse_line("U,3,1"),
Err(ProtocolError::ArgumentCount {
tag: "U".to_owned(),
expected: 3,
found: 2,
})
);
}
#[test]
fn test_parse_line_update_non_numeric_subscription_id_is_error() {
assert_eq!(
parse_line("U,x,1,a|b"),
Err(ProtocolError::NotNumeric {
tag: "U".to_owned(),
argument: "subscription-ID",
value: "x".to_owned(),
})
);
}
#[test]
fn test_parse_line_subok_spec_example_ok() {
assert_eq!(
parse_line("SUBOK,3,1,10"),
Ok(Notification::SubscriptionOk {
subscription_id: 3,
item_count: 1,
field_count: 10,
})
);
}
#[test]
fn test_parse_line_subcmd_spec_example_ok() {
assert_eq!(
parse_line("SUBCMD,3,1,10,1,2"),
Ok(Notification::SubscriptionCommandOk {
subscription_id: 3,
item_count: 1,
field_count: 10,
key_field_index: 1,
command_field_index: 2,
})
);
}
#[test]
fn test_parse_line_subcmd_too_few_arguments_is_error() {
assert_eq!(
parse_line("SUBCMD,3,1,10"),
Err(ProtocolError::ArgumentCount {
tag: "SUBCMD".to_owned(),
expected: 5,
found: 3,
})
);
}
#[test]
fn test_parse_line_unsub_spec_example_ok() {
assert_eq!(
parse_line("UNSUB,3"),
Ok(Notification::Unsubscribed { subscription_id: 3 })
);
}
#[test]
fn test_parse_line_eos_spec_example_ok() {
assert_eq!(
parse_line("EOS,3,1"),
Ok(Notification::EndOfSnapshot {
subscription_id: 3,
item_index: 1,
})
);
}
#[test]
fn test_parse_line_cs_spec_example_ok() {
assert_eq!(
parse_line("CS,3,1"),
Ok(Notification::ClearSnapshot {
subscription_id: 3,
item_index: 1,
})
);
}
#[test]
fn test_parse_line_ov_spec_example_ok() {
assert_eq!(
parse_line("OV,3,1,5"),
Ok(Notification::Overflow {
subscription_id: 3,
item_index: 1,
dropped_count: 5,
})
);
}
#[test]
fn test_parse_line_conf_spec_example_ok() {
assert_eq!(
parse_line("CONF,3,3.0,filtered"),
Ok(Notification::SubscriptionReconfigured {
subscription_id: 3,
max_frequency: MaxFrequency::Limited {
updates_per_second: "3.0".to_owned(),
},
filtering: FilteringMode::Filtered,
})
);
}
#[test]
fn test_parse_line_conf_unlimited_unfiltered_ok() {
assert_eq!(
parse_line("CONF,7,unlimited,unfiltered"),
Ok(Notification::SubscriptionReconfigured {
subscription_id: 7,
max_frequency: MaxFrequency::Unlimited,
filtering: FilteringMode::Unfiltered,
})
);
}
#[test]
fn test_parse_line_conf_unrecognized_filtering_is_preserved() {
assert_eq!(
parse_line("CONF,7,1.5,whatever"),
Ok(Notification::SubscriptionReconfigured {
subscription_id: 7,
max_frequency: MaxFrequency::Limited {
updates_per_second: "1.5".to_owned(),
},
filtering: FilteringMode::Unrecognized {
literal: "whatever".to_owned(),
},
})
);
}
#[test]
fn test_parse_max_frequency_accepts_only_the_two_specified_alternatives() {
for text in ["3.0", "3", "0", "0.5", "12345.678", "007"] {
assert_eq!(
parse_max_frequency(text),
MaxFrequency::Limited {
updates_per_second: text.to_owned(),
},
"{text:?} is a decimal number"
);
}
assert_eq!(parse_max_frequency("unlimited"), MaxFrequency::Unlimited);
}
#[test]
fn test_parse_max_frequency_does_not_pass_off_garbage_as_a_limit() {
for text in [
"",
"+",
"-3",
"+3.0",
" 3.0",
"3.0 ",
"3e2",
"3,0",
"3.",
".5",
"3.0x",
"UNLIMITED",
"unfiltered",
"adaptive",
] {
assert_eq!(
parse_max_frequency(text),
MaxFrequency::Unrecognized {
literal: text.to_owned(),
},
"{text:?} must not be reported as a frequency limit"
);
}
}
#[test]
fn test_parse_max_frequency_keeps_an_overlong_number_verbatim() {
let long = "1".repeat(200);
assert_eq!(
parse_max_frequency(&long),
MaxFrequency::Limited {
updates_per_second: long.clone(),
}
);
}
#[test]
fn test_parse_line_conf_with_an_unreadable_frequency() {
assert_eq!(
parse_line("CONF,7,banana,filtered"),
Ok(Notification::SubscriptionReconfigured {
subscription_id: 7,
max_frequency: MaxFrequency::Unrecognized {
literal: "banana".to_owned(),
},
filtering: FilteringMode::Filtered,
})
);
}
#[test]
fn test_parse_line_msgdone_spec_example_ok() {
assert_eq!(
parse_line("MSGDONE,Orders_Sequence,3,Processed with ID 32652506"),
Ok(Notification::MessageDone {
sequence: MessageSequence::Named("Orders_Sequence".to_owned()),
prog: 3,
response: "Processed with ID 32652506".to_owned(),
})
);
}
#[test]
fn test_parse_line_msgdone_unspecified_sequence_ok() {
assert_eq!(
parse_line("MSGDONE,*,1,"),
Ok(Notification::MessageDone {
sequence: MessageSequence::Unspecified,
prog: 1,
response: String::new(),
})
);
}
#[test]
fn test_parse_line_msgfail_spec_example_ok() {
assert_eq!(
parse_line(
"MSGFAIL,Orders_Sequence,4,38,The specified progressive number has been skipped by timeout"
),
Ok(Notification::MessageFailed {
sequence: MessageSequence::Named("Orders_Sequence".to_owned()),
prog: 4,
code: 38,
message: "The specified progressive number has been skipped by timeout".to_owned(),
})
);
}
#[test]
fn test_parse_line_msgfail_message_with_commas_is_not_split() {
assert_eq!(
parse_line("MSGFAIL,Seq,4,38,rejected, retry later, or give up"),
Ok(Notification::MessageFailed {
sequence: MessageSequence::Named("Seq".to_owned()),
prog: 4,
code: 38,
message: "rejected, retry later, or give up".to_owned(),
})
);
}
#[test]
fn test_parse_line_msgfail_adapter_supplied_negative_code_ok() {
assert_eq!(
parse_line("MSGFAIL,*,1,-7,application specific"),
Ok(Notification::MessageFailed {
sequence: MessageSequence::Unspecified,
prog: 1,
code: -7,
message: "application specific".to_owned(),
})
);
}
#[test]
fn test_parse_line_cons_spec_example_ok() {
assert_eq!(
parse_line("CONS,50"),
Ok(Notification::ConstraintsChanged {
bandwidth: Bandwidth::Limited {
kbps: "50".to_owned()
},
})
);
}
#[test]
fn test_parse_line_cons_unlimited_and_unmanaged_ok() {
assert_eq!(
parse_line("CONS,unlimited"),
Ok(Notification::ConstraintsChanged {
bandwidth: Bandwidth::Unlimited
})
);
assert_eq!(
parse_line("CONS,unmanaged"),
Ok(Notification::ConstraintsChanged {
bandwidth: Bandwidth::Unmanaged
})
);
}
#[test]
fn test_parse_line_cons_with_an_unreadable_bandwidth() {
assert_eq!(
parse_line("CONS,"),
Ok(Notification::ConstraintsChanged {
bandwidth: Bandwidth::Unrecognized {
literal: String::new(),
},
})
);
for text in ["-50", "50kbps", "5e3", "50,0", "UNMANAGED", "throttled"] {
assert_eq!(
parse_line(&format!("CONS,{text}")),
Ok(Notification::ConstraintsChanged {
bandwidth: Bandwidth::Unrecognized {
literal: text.to_owned(),
},
}),
"{text:?} must not be reported as a bandwidth limit"
);
}
assert_eq!(
parse_line("CONS,50.5"),
Ok(Notification::ConstraintsChanged {
bandwidth: Bandwidth::Limited {
kbps: "50.5".to_owned(),
},
})
);
}
#[test]
fn test_parse_line_sync_spec_example_ok() {
assert_eq!(
parse_line("SYNC,120"),
Ok(Notification::Sync {
elapsed_seconds: 120
})
);
}
#[test]
fn test_parse_line_clientip_ipv4_spec_example_ok() {
assert_eq!(
parse_line("CLIENTIP,127.0.0.1"),
Ok(Notification::ClientIp {
address: "127.0.0.1".to_owned()
})
);
}
#[test]
fn test_parse_line_clientip_ipv6_spec_example_ok() {
assert_eq!(
parse_line("CLIENTIP,::1"),
Ok(Notification::ClientIp {
address: "::1".to_owned()
})
);
}
#[test]
fn test_parse_line_servname_spec_example_ok() {
assert_eq!(
parse_line("SERVNAME,Lightstreamer HTTP Server"),
Ok(Notification::ServerName {
name: "Lightstreamer HTTP Server".to_owned(),
})
);
}
#[test]
fn test_parse_line_servname_percent_encoded_argument_is_decoded() {
assert_eq!(
parse_line("SERVNAME,Acme%2C%20Inc.%20Server"),
Ok(Notification::ServerName {
name: "Acme, Inc. Server".to_owned(),
})
);
}
#[test]
fn test_parse_line_prog_spec_example_ok() {
assert_eq!(
parse_line("PROG,24576"),
Ok(Notification::Progressive { progressive: 24576 })
);
}
#[test]
fn test_parse_line_noop_spec_example_ok() {
assert_eq!(
parse_line("NOOP,sending placeholder data"),
Ok(Notification::NoOp {
preamble: "sending placeholder data".to_owned(),
})
);
}
#[test]
fn test_parse_line_probe_spec_example_ok() {
assert_eq!(parse_line("PROBE"), Ok(Notification::Probe));
}
#[test]
fn test_parse_line_probe_with_argument_is_error() {
assert_eq!(
parse_line("PROBE,1"),
Err(ProtocolError::ArgumentCount {
tag: "PROBE".to_owned(),
expected: 0,
found: 1,
})
);
}
#[test]
fn test_parse_line_loop_spec_example_ok() {
assert_eq!(
parse_line("LOOP,5000"),
Ok(Notification::Loop {
expected_delay_millis: 5000
})
);
}
#[test]
fn test_parse_line_end_spec_example_ok() {
assert_eq!(
parse_line("END,8,Session count limit reached"),
Ok(Notification::End {
cause_code: 8,
cause_message: "Session count limit reached".to_owned(),
})
);
}
#[test]
fn test_parse_line_end_cause_message_with_commas_is_not_split() {
assert_eq!(
parse_line("END,31,Destroy invoked by client, no retry"),
Ok(Notification::End {
cause_code: 31,
cause_message: "Destroy invoked by client, no retry".to_owned(),
})
);
}
#[test]
fn test_parse_line_end_single_argument_is_error() {
assert_eq!(
parse_line("END,8"),
Err(ProtocolError::ArgumentCount {
tag: "END".to_owned(),
expected: 2,
found: 1,
})
);
}
#[test]
fn test_parse_line_conok_spec_example_ok() {
assert_eq!(
parse_line("CONOK,S73d162c183916f0dT2729905,50000,5000,*"),
Ok(Notification::ConnectionOk {
session_id: "S73d162c183916f0dT2729905".to_owned(),
request_limit_bytes: 50000,
keep_alive_millis: 5000,
control_link: None,
})
);
}
#[test]
fn test_parse_line_conok_with_control_link_ok() {
assert_eq!(
parse_line("CONOK,S1,50000,5000,push2.example.com:443"),
Ok(Notification::ConnectionOk {
session_id: "S1".to_owned(),
request_limit_bytes: 50000,
keep_alive_millis: 5000,
control_link: Some("push2.example.com:443".to_owned()),
})
);
}
#[test]
fn test_parse_line_conerr_spec_example_ok() {
assert_eq!(
parse_line("CONERR,2,Requested Adapter Set not available"),
Ok(Notification::ConnectionError {
code: 2,
message: "Requested Adapter Set not available".to_owned(),
})
);
}
#[test]
fn test_parse_line_conerr_empty_message_ok() {
assert_eq!(
parse_line("CONERR,-3,"),
Ok(Notification::ConnectionError {
code: -3,
message: String::new(),
})
);
}
#[test]
fn test_parse_line_reqok_spec_example_ok() {
assert_eq!(
parse_line("REQOK,1"),
Ok(Notification::RequestOk {
request_id: Some("1".to_owned()),
})
);
}
#[test]
fn test_parse_line_reqok_bare_heartbeat_form_ok() {
assert_eq!(
parse_line("REQOK"),
Ok(Notification::RequestOk { request_id: None })
);
}
#[test]
fn test_parse_line_reqok_alphanumeric_request_id_ok() {
assert_eq!(
parse_line("REQOK,a7f3"),
Ok(Notification::RequestOk {
request_id: Some("a7f3".to_owned()),
})
);
}
#[test]
fn test_parse_line_reqerr_three_arguments_ok() {
assert_eq!(
parse_line("REQERR,7,19,Specified subscription not found"),
Ok(Notification::RequestError {
request_id: "7".to_owned(),
code: 19,
message: "Specified subscription not found".to_owned(),
})
);
}
#[test]
fn test_parse_line_reqerr_spec_example_is_rejected_as_malformed() {
assert_eq!(
parse_line("REQERR,19,Specified subscription not found"),
Err(ProtocolError::ArgumentCount {
tag: "REQERR".to_owned(),
expected: 3,
found: 2,
})
);
}
#[test]
fn test_parse_line_reqerr_message_with_commas_is_not_split() {
assert_eq!(
parse_line("REQERR,7,65,Malformed request, check LS_op, then retry"),
Ok(Notification::RequestError {
request_id: "7".to_owned(),
code: 65,
message: "Malformed request, check LS_op, then retry".to_owned(),
})
);
}
#[test]
fn test_parse_line_reqerr_too_few_arguments_is_error() {
assert_eq!(
parse_line("REQERR,19"),
Err(ProtocolError::ArgumentCount {
tag: "REQERR".to_owned(),
expected: 3,
found: 1,
})
);
}
#[test]
fn test_parse_line_error_spec_example_ok() {
assert_eq!(
parse_line("ERROR,68,Internal error during request processing"),
Ok(Notification::Error {
code: 68,
message: "Internal error during request processing".to_owned(),
})
);
}
#[test]
fn test_parse_line_wsok_spec_example_ok() {
assert_eq!(parse_line("WSOK"), Ok(Notification::WsOk));
}
#[test]
fn test_parse_line_unknown_tag_is_reported_not_dropped() {
assert_eq!(
parse_line("MPNREG,devid,adapter"),
Err(ProtocolError::UnknownTag {
tag: "MPNREG".to_owned(),
line: "MPNREG,devid,adapter".to_owned(),
})
);
}
#[test]
fn test_parse_line_unknown_bare_tag_is_reported() {
assert_eq!(
parse_line("FUTURE"),
Err(ProtocolError::UnknownTag {
tag: "FUTURE".to_owned(),
line: "FUTURE".to_owned(),
})
);
}
#[test]
fn test_parse_line_empty_line_is_unknown_tag() {
assert_eq!(
parse_line(""),
Err(ProtocolError::UnknownTag {
tag: String::new(),
line: String::new(),
})
);
}
#[test]
fn test_parse_line_unknown_tag_preserves_the_whole_line() {
let line = "FUTURE,1,two,three,with,commas,and%20an%20escape";
assert_eq!(
parse_line(line),
Err(ProtocolError::UnknownTag {
tag: "FUTURE".to_owned(),
line: line.to_owned(),
})
);
assert_eq!(
parse_line("FUTURE,a,b\r\n"),
Err(ProtocolError::UnknownTag {
tag: "FUTURE".to_owned(),
line: "FUTURE,a,b".to_owned(),
})
);
assert_eq!(
parse_line("FUTURE,"),
Err(ProtocolError::UnknownTag {
tag: "FUTURE".to_owned(),
line: "FUTURE,".to_owned(),
})
);
}
#[test]
fn test_parse_line_trailing_crlf_is_tolerated() {
assert_eq!(parse_line("PROBE\r\n"), Ok(Notification::Probe));
assert_eq!(
parse_line("EOS,3,1\n"),
Ok(Notification::EndOfSnapshot {
subscription_id: 3,
item_index: 1,
})
);
assert_eq!(
parse_line("EOS,3,1\r"),
Ok(Notification::EndOfSnapshot {
subscription_id: 3,
item_index: 1,
})
);
}
#[test]
fn test_parse_line_trailing_terminator_stripped_only_once() {
assert_eq!(
parse_line("U,3,1,a|b\n\r\n"),
Ok(Notification::Update {
subscription_id: 3,
item_index: 1,
raw_values: "a|b\n".to_owned(),
})
);
}
#[test]
fn test_parse_line_numeric_overflow_is_not_numeric_error() {
assert_eq!(
parse_line("UNSUB,99999999999999999999"),
Err(ProtocolError::NotNumeric {
tag: "UNSUB".to_owned(),
argument: "subscription-ID",
value: "99999999999999999999".to_owned(),
})
);
}
#[test]
fn test_parse_line_empty_numeric_argument_is_not_numeric_error() {
assert_eq!(
parse_line("UNSUB,"),
Err(ProtocolError::NotNumeric {
tag: "UNSUB".to_owned(),
argument: "subscription-ID",
value: String::new(),
})
);
}
}