use crate::{ffi, mem};
#[derive(Debug, thiserror::Error)]
pub enum WsSendError {
#[error("ws send: per-invocation rate limit reached")]
SendLimitReached,
#[error("ws send: message too large")]
MessageTooLarge,
#[error("ws send: write failed")]
SendError,
#[error("ws send: unknown error code {0}")]
Unknown(i32),
}
impl WsSendError {
fn from_code(code: i32) -> Self {
match code {
1 => Self::SendLimitReached,
2 => Self::MessageTooLarge,
3 => Self::SendError,
_ => Self::Unknown(code),
}
}
}
pub fn send(data: &[u8]) -> Result<(), WsSendError> {
let (data_ptr, data_len) = mem::host_arg_bytes(data);
let code = unsafe { ffi::ws_send(data_ptr, data_len) };
if code == 0 {
Ok(())
} else {
Err(WsSendError::from_code(code))
}
}
pub fn send_text(text: &str) -> Result<(), WsSendError> {
send(text.as_bytes())
}
pub fn close(code: u16) {
unsafe { ffi::ws_close_conn(code as i32) }
}
pub fn conn_id() -> String {
unsafe { mem::read_packed_string(ffi::ws_conn_id()) }.unwrap_or_default()
}
pub fn event_type() -> String {
unsafe { mem::read_packed_string(ffi::ws_event_type()) }.unwrap_or_default()
}
pub fn event_data() -> Vec<u8> {
unsafe { mem::read_packed_bytes(ffi::ws_event_data()) }.unwrap_or_default()
}
pub fn event_text() -> String {
String::from_utf8_lossy(&event_data()).into_owned()
}
pub fn close_code() -> u16 {
unsafe { ffi::ws_close_code() as u16 }
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ffi::test_host;
#[test]
fn send_records_payload() {
test_host::reset();
send(b"hello").unwrap();
let sends = test_host::read_mock(|m| m.ws_sends.clone());
assert_eq!(sends, vec![b"hello".to_vec()]);
}
#[test]
fn send_text_records_utf8_bytes() {
test_host::reset();
send_text("héllo").unwrap();
let sends = test_host::read_mock(|m| m.ws_sends.clone());
assert_eq!(sends, vec!["héllo".as_bytes().to_vec()]);
}
#[test]
fn send_maps_error_codes() {
for (code, expected) in [
(1, WsSendError::SendLimitReached),
(2, WsSendError::MessageTooLarge),
(3, WsSendError::SendError),
] {
test_host::reset();
test_host::with_mock(|m| m.ws_send_error = code);
let err = send(b"x").unwrap_err();
assert!(
std::mem::discriminant(&err) == std::mem::discriminant(&expected),
"code {} mismatch",
code,
);
}
}
#[test]
fn send_unknown_error_code() {
test_host::reset();
test_host::with_mock(|m| m.ws_send_error = 99);
match send(b"x").unwrap_err() {
WsSendError::Unknown(99) => {}
other => panic!("expected Unknown(99), got {:?}", other),
}
}
#[test]
fn close_records_code() {
test_host::reset();
close(1000);
close(1011);
assert_eq!(
test_host::read_mock(|m| m.ws_closes.clone()),
vec![1000, 1011]
);
}
#[test]
fn conn_id_returns_host_value() {
test_host::reset();
test_host::with_mock(|m| m.ws_conn_id = Some("conn-abc-123".into()));
assert_eq!(conn_id(), "conn-abc-123");
}
#[test]
fn conn_id_empty_when_unset() {
test_host::reset();
assert_eq!(conn_id(), "");
}
#[test]
fn event_type_open_message_close() {
for ty in ["open", "message", "close"] {
test_host::reset();
test_host::with_mock(|m| m.ws_event_type = Some(ty.into()));
assert_eq!(event_type(), ty);
}
}
#[test]
fn event_data_returns_payload_bytes() {
test_host::reset();
test_host::with_mock(|m| m.ws_event_data = Some(vec![1, 2, 3, 4]));
assert_eq!(event_data(), vec![1, 2, 3, 4]);
}
#[test]
fn event_data_empty_for_open_close() {
test_host::reset();
assert!(event_data().is_empty());
}
#[test]
fn event_text_decodes_utf8() {
test_host::reset();
test_host::with_mock(|m| m.ws_event_data = Some("héllo".as_bytes().to_vec()));
assert_eq!(event_text(), "héllo");
}
#[test]
fn close_code_returns_host_value() {
test_host::reset();
test_host::with_mock(|m| m.ws_close_code = 1006);
assert_eq!(close_code(), 1006);
}
}