use ruststream::asyncapi::{Binding, Bindings};
use serde::Serialize;
use crate::endpoint::{Endpoint, Side};
const EXTENSION: &str = "x-ruststream-zeromq";
#[derive(Debug, Clone, Copy)]
pub(crate) enum SocketPair {
PushPull,
PubSub,
DealerRouter,
}
impl SocketPair {
const fn as_str(self) -> &'static str {
match self {
Self::PushPull => "PUSH/PULL",
Self::PubSub => "PUB/SUB",
Self::DealerRouter => "DEALER/ROUTER",
}
}
const fn addresses_by_name(self) -> bool {
match self {
Self::PushPull | Self::PubSub => true,
Self::DealerRouter => false,
}
}
}
#[derive(Serialize)]
struct ServerBody {
transport: &'static str,
endpoint: String,
role: &'static str,
}
#[derive(Serialize)]
struct ChannelBody {
#[serde(rename = "socketPair")]
socket_pair: &'static str,
#[serde(rename = "nameFrame", skip_serializing_if = "Option::is_none")]
name_frame: Option<String>,
}
fn transport(endpoint: &Endpoint) -> &'static str {
if endpoint.address().starts_with("ipc://") {
"ipc"
} else {
"tcp"
}
}
pub(crate) fn server(endpoint: &Endpoint) -> Bindings {
let body = ServerBody {
transport: transport(endpoint),
endpoint: endpoint.host(),
role: match endpoint.side() {
Side::Bind => "bind",
Side::Connect => "connect",
},
};
wrap(&body)
}
pub(crate) fn channel(pair: SocketPair, destination: &str) -> Bindings {
wrap(&ChannelBody {
socket_pair: pair.as_str(),
name_frame: pair.addresses_by_name().then(|| destination.to_owned()),
})
}
fn wrap<T: Serialize>(body: &T) -> Bindings {
Binding::extension(EXTENSION, body)
.map_or_else(|_| Bindings::new(), |b| Bindings::new().with(b))
}
#[cfg(test)]
mod tests {
use serde_json::Value;
use super::*;
use crate::ZmqEndpoint;
#[test]
fn the_server_binding_reports_the_transport_and_the_role() {
let json = serde_json::to_value(server(
&ZmqEndpoint::bind("tcp://0.0.0.0:5555").into_inner(),
))
.expect("the binding serializes");
let body = &json[EXTENSION];
assert_eq!(body["transport"], "tcp");
assert_eq!(body["endpoint"], "0.0.0.0:5555");
assert_eq!(body["role"], "bind");
}
#[test]
fn the_server_binding_drops_userinfo_with_the_scheme() {
let endpoint = ZmqEndpoint::connect("tcp://user:hunter2@broker:5555").into_inner();
let json = serde_json::to_string(&server(&endpoint)).expect("the binding serializes");
assert!(
!json.contains("hunter2"),
"the binding carries a password: {json}"
);
assert!(
json.contains("broker:5555"),
"the binding lost the coordinate: {json}"
);
}
#[test]
fn an_ipc_endpoint_reports_its_socket_path() {
let json = serde_json::to_value(server(
&ZmqEndpoint::connect("ipc:///tmp/orders").into_inner(),
))
.expect("the binding serializes");
assert_eq!(json[EXTENSION]["transport"], "ipc");
assert_eq!(json[EXTENSION]["endpoint"], "/tmp/orders");
assert_eq!(json[EXTENSION]["role"], "connect");
}
#[test]
fn every_pattern_names_its_socket_pair() {
for (pair, expected) in [
(SocketPair::PushPull, "PUSH/PULL"),
(SocketPair::PubSub, "PUB/SUB"),
(SocketPair::DealerRouter, "DEALER/ROUTER"),
] {
let json =
serde_json::to_value(channel(pair, "orders")).expect("the binding serializes");
assert_eq!(json[EXTENSION]["socketPair"], expected);
}
}
#[test]
fn the_one_way_patterns_report_the_destination_as_their_name_frame() {
for pair in [SocketPair::PushPull, SocketPair::PubSub] {
let json =
serde_json::to_value(channel(pair, "orders")).expect("the binding serializes");
assert_eq!(json[EXTENSION]["nameFrame"], "orders");
}
}
#[test]
fn a_responder_reports_no_name_frame() {
let json = serde_json::to_value(channel(SocketPair::DealerRouter, "reply"))
.expect("the binding serializes");
assert_eq!(json[EXTENSION]["nameFrame"], Value::Null);
}
}