use std::num::NonZeroU32;
use std::time::Duration;
use crate::config::ConfigError;
use crate::error::ServerError;
use crate::session::{
MessageResult as WireResult, MessageSequence, OutgoingMessage, SequenceName as WireSequenceName,
};
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct SequenceName(WireSequenceName);
impl SequenceName {
#[must_use = "a checked sequence name does nothing unless it is used"]
pub fn try_new(name: impl Into<String>) -> Result<Self, ConfigError> {
let name = name.into();
WireSequenceName::try_new(name.clone())
.map(Self)
.map_err(|_| ConfigError::InvalidSequenceName { name })
}
#[must_use]
#[inline]
pub fn as_str(&self) -> &str {
self.0.as_str()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Message {
inner: OutgoingMessage,
}
impl Message {
#[must_use = "builders do nothing unless the message is sent"]
pub fn new(text: impl Into<String>, progressive: NonZeroU32) -> Self {
Self {
inner: OutgoingMessage::numbered(text, progressive),
}
}
#[must_use = "builders do nothing unless the message is sent"]
pub fn fire_and_forget(text: impl Into<String>) -> Self {
Self {
inner: OutgoingMessage::fire_and_forget(text),
}
}
#[must_use = "builders do nothing unless the message is sent"]
pub fn in_sequence(mut self, sequence: SequenceName) -> Self {
self.inner = self.inner.in_sequence(sequence.0);
self
}
pub fn with_max_wait(mut self, wait: Duration) -> Result<Self, ConfigError> {
let millis = u64::try_from(wait.as_millis())
.map_err(|_| ConfigError::DurationTooLarge { field: "max_wait" })?;
self.inner.max_wait_millis = Some(millis);
Ok(self)
}
#[must_use]
#[inline]
pub fn text(&self) -> &str {
&self.inner.message
}
pub fn validate(&self) -> Result<(), ConfigError> {
let sequenced = self.inner.sequence.is_some();
if sequenced && self.inner.msg_prog.is_none() {
return Err(ConfigError::SequencedMessageNeedsAProgressive);
}
if self.inner.max_wait_millis.is_some() && !sequenced {
return Err(ConfigError::MaxWaitNeedsASequence);
}
Ok(())
}
pub(crate) fn into_outgoing(self) -> OutgoingMessage {
self.inner
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum MessageResult {
Delivered {
response: String,
},
Failed(ServerError),
Refused(ServerError),
NotSent {
reason: String,
},
}
impl From<WireResult> for MessageResult {
fn from(result: WireResult) -> Self {
match result {
WireResult::Done { response } => Self::Delivered { response },
WireResult::Failed { cause } => Self::Failed(cause.into()),
WireResult::Refused { cause } => Self::Refused(cause.into()),
WireResult::NotSent { reason } => Self::NotSent { reason },
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct MessageOutcome {
pub sequence: Option<String>,
pub progressive: Option<u64>,
pub result: MessageResult,
}
impl MessageOutcome {
pub(crate) fn new(
sequence: MessageSequence,
progressive: Option<u64>,
result: WireResult,
) -> Self {
Self {
sequence: match sequence {
MessageSequence::Named(name) => Some(name),
MessageSequence::Unspecified => None,
},
progressive,
result: result.into(),
}
}
#[must_use]
#[inline]
pub const fn is_delivered(&self) -> bool {
matches!(self.result, MessageResult::Delivered { .. })
}
#[cfg(feature = "test-util")]
#[must_use]
pub(crate) const fn from_parts(
sequence: Option<String>,
progressive: Option<u64>,
result: MessageResult,
) -> Self {
Self {
sequence,
progressive,
result,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::session::ServerCause;
#[test]
fn test_sequence_name_accepts_an_alphanumeric_identifier() {
assert!(matches!(
SequenceName::try_new("ORDERS_1"),
Ok(name) if name.as_str() == "ORDERS_1"
));
}
#[test]
fn test_sequence_name_rejects_the_reserved_identifier() {
assert!(matches!(
SequenceName::try_new("UNORDERED_MESSAGES"),
Err(ConfigError::InvalidSequenceName { name }) if name == "UNORDERED_MESSAGES"
));
}
#[test]
fn test_sequence_name_rejects_an_empty_or_punctuated_name() {
assert!(matches!(
SequenceName::try_new(""),
Err(ConfigError::InvalidSequenceName { .. })
));
assert!(matches!(
SequenceName::try_new("with space"),
Err(ConfigError::InvalidSequenceName { .. })
));
assert!(matches!(
SequenceName::try_new("dash-ed"),
Err(ConfigError::InvalidSequenceName { .. })
));
}
#[test]
fn test_fire_and_forget_carries_no_progressive() {
let message = Message::fire_and_forget("hello");
assert_eq!(message.text(), "hello");
assert_eq!(message.into_outgoing().msg_prog, None);
}
#[test]
fn test_a_numbered_message_keeps_the_callers_number() {
let outgoing = Message::new("hello", NonZeroU32::MIN).into_outgoing();
assert_eq!(outgoing.msg_prog, Some(NonZeroU32::MIN));
}
#[test]
fn test_max_wait_rejects_a_duration_that_does_not_fit_milliseconds() {
assert!(matches!(
Message::fire_and_forget("x").with_max_wait(Duration::MAX),
Err(ConfigError::DurationTooLarge { field: "max_wait" })
));
}
fn sequence() -> SequenceName {
match SequenceName::try_new("ORDERS") {
Ok(name) => name,
Err(error) => unreachable!("the fixture sequence name is valid: {error}"),
}
}
#[test]
fn test_a_fire_and_forget_message_cannot_be_sequenced() {
assert!(matches!(
Message::fire_and_forget("BUY 100")
.in_sequence(sequence())
.validate(),
Err(ConfigError::SequencedMessageNeedsAProgressive)
));
}
#[test]
fn test_a_max_wait_needs_a_sequence() {
let built = Message::new("BUY 100", NonZeroU32::MIN).with_max_wait(Duration::from_secs(1));
match built {
Ok(message) => assert!(matches!(
message.validate(),
Err(ConfigError::MaxWaitNeedsASequence)
)),
Err(error) => panic!("a one-second wait is expressible: {error}"),
}
}
#[test]
fn test_a_sequenced_numbered_message_with_a_wait_is_valid() {
let built = Message::new("BUY 100", NonZeroU32::MIN)
.in_sequence(sequence())
.with_max_wait(Duration::from_secs(1));
match built {
Ok(message) => assert!(message.validate().is_ok()),
Err(error) => panic!("a one-second wait is expressible: {error}"),
}
}
#[test]
fn test_the_plain_shapes_are_valid() {
assert!(Message::fire_and_forget("hello").validate().is_ok());
assert!(Message::new("hello", NonZeroU32::MIN).validate().is_ok());
assert!(
Message::new("hello", NonZeroU32::MIN)
.in_sequence(sequence())
.validate()
.is_ok()
);
}
#[test]
fn test_a_zero_max_wait_is_a_boundary_not_an_error() {
let built = Message::new("BUY 100", NonZeroU32::MIN)
.in_sequence(sequence())
.with_max_wait(Duration::ZERO);
match built {
Ok(message) => {
assert!(message.validate().is_ok());
assert_eq!(message.into_outgoing().max_wait_millis, Some(0));
}
Err(error) => panic!("zero is expressible in milliseconds: {error}"),
}
}
#[test]
fn test_outcome_distinguishes_where_a_message_failed() {
let delivered = MessageOutcome::new(
MessageSequence::Named("ORDERS".to_owned()),
Some(1),
WireResult::Done {
response: "ok".to_owned(),
},
);
assert!(delivered.is_delivered());
assert_eq!(delivered.sequence.as_deref(), Some("ORDERS"));
let refused = MessageOutcome::new(
MessageSequence::Unspecified,
None,
WireResult::Refused {
cause: ServerCause {
code: 65,
message: "Syntax error".to_owned(),
},
},
);
assert!(!refused.is_delivered());
assert_eq!(refused.sequence, None);
assert!(matches!(refused.result, MessageResult::Refused(cause) if cause.code() == 65));
let never_sent = MessageOutcome::new(
MessageSequence::Unspecified,
None,
WireResult::NotSent {
reason: "no session".to_owned(),
},
);
assert!(matches!(never_sent.result, MessageResult::NotSent { .. }));
}
}