use std::collections::BTreeMap;
use serde::ser::SerializeMap;
use serde::{Serialize, Serializer};
use serde_json::Value;
use thiserror::Error;
const PROTOCOLS: &[&str] = &[
"amqp",
"amqp1",
"anypointmq",
"googlepubsub",
"http",
"ibmmq",
"jms",
"kafka",
"mercure",
"mqtt",
"mqtt5",
"nats",
"pulsar",
"redis",
"ros2",
"sns",
"solace",
"sqs",
"stomp",
"websockets",
];
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Binding {
key: &'static str,
body: String,
}
impl Binding {
pub fn new<T: Serialize>(
protocol: &'static str,
version: &'static str,
body: &T,
) -> Result<Self, BindingError> {
if !PROTOCOLS.contains(&protocol) {
return Err(BindingError::UnknownProtocol { protocol });
}
let mut value = serde_json::to_value(body)?;
let object = value
.as_object_mut()
.ok_or(BindingError::NotAnObject { key: protocol })?;
object.insert(
"bindingVersion".to_owned(),
Value::String(version.to_owned()),
);
Ok(Self {
key: protocol,
body: serde_json::to_string(&value)?,
})
}
pub fn extension<T: Serialize>(name: &'static str, body: &T) -> Result<Self, BindingError> {
let legal = name.starts_with("x-")
&& name.len() > 2
&& name[2..]
.chars()
.all(|c| c.is_alphanumeric() || matches!(c, '_' | '-' | '.'));
if !legal {
return Err(BindingError::NotAnExtension { name });
}
let value = serde_json::to_value(body)?;
if !value.is_object() {
return Err(BindingError::NotAnObject { key: name });
}
Ok(Self {
key: name,
body: serde_json::to_string(&value)?,
})
}
#[must_use]
pub const fn protocol(&self) -> &'static str {
self.key
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct Bindings(BTreeMap<&'static str, String>);
impl Bindings {
#[must_use]
pub const fn new() -> Self {
Self(BTreeMap::new())
}
#[must_use]
pub fn with(mut self, binding: Binding) -> Self {
self.0.insert(binding.key, binding.body);
self
}
pub(crate) fn fill_from(&mut self, other: &Self) {
for (key, body) in &other.0 {
self.0.entry(key).or_insert_with(|| body.clone());
}
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
}
impl Serialize for Bindings {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
let mut map = serializer.serialize_map(Some(self.0.len()))?;
for (key, body) in &self.0 {
let value = serde_json::from_str::<Value>(body).unwrap_or(Value::Null);
map.serialize_entry(key, &value)?;
}
map.end()
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
#[non_exhaustive]
pub struct SubscriptionBindings {
pub channel: Bindings,
pub operation: Bindings,
pub message: Bindings,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
#[non_exhaustive]
pub struct PublishBindings {
pub channel: Bindings,
pub operation: Bindings,
pub message: Bindings,
}
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum BindingError {
#[error(
"`{protocol}` is not an AsyncAPI protocol key; a transport the specification has no \
binding for goes in an `x-` extension instead (Binding::extension)"
)]
UnknownProtocol {
protocol: &'static str,
},
#[error(
"`{name}` is not an extension name; it must start with `x-` and continue with letters, \
digits, `_`, `-` or `.`"
)]
NotAnExtension {
name: &'static str,
},
#[error("the body of `{key}` must serialize to a JSON object: a binding holds named fields")]
NotAnObject {
key: &'static str,
},
#[error("serializing a binding body failed: {0}")]
Serialize(#[from] serde_json::Error),
}
#[cfg(test)]
mod tests {
use super::*;
#[derive(Serialize)]
struct Group {
#[serde(rename = "groupId")]
group_id: &'static str,
}
#[test]
fn the_core_writes_the_binding_version_so_a_broker_cannot_forget_it() {
let bindings = Bindings::new().with(
Binding::new(
"kafka",
"0.5.0",
&Group {
group_id: "billing",
},
)
.unwrap(),
);
let json = serde_json::to_value(&bindings).unwrap();
assert_eq!(json["kafka"]["groupId"], "billing");
assert_eq!(json["kafka"]["bindingVersion"], "0.5.0");
}
#[test]
fn an_unlisted_protocol_is_refused_at_construction() {
let error = Binding::new("kinesis", "0.1.0", &Group { group_id: "x" }).unwrap_err();
assert!(matches!(
error,
BindingError::UnknownProtocol {
protocol: "kinesis"
}
));
}
#[test]
fn an_extension_needs_the_x_prefix_and_carries_no_binding_version() {
assert!(matches!(
Binding::extension("kinesis", &Group { group_id: "x" }).unwrap_err(),
BindingError::NotAnExtension { name: "kinesis" },
));
assert!(matches!(
Binding::extension("x-", &Group { group_id: "x" }).unwrap_err(),
BindingError::NotAnExtension { name: "x-" },
));
let bindings = Bindings::new()
.with(Binding::extension("x-kinesis", &Group { group_id: "x" }).unwrap());
let json = serde_json::to_value(&bindings).unwrap();
assert_eq!(json["x-kinesis"]["groupId"], "x");
assert!(json["x-kinesis"].get("bindingVersion").is_none());
}
#[test]
fn a_body_that_is_not_an_object_has_no_binding_to_be() {
assert!(matches!(
Binding::new("kafka", "0.5.0", &"a string").unwrap_err(),
BindingError::NotAnObject { key: "kafka" },
));
}
#[test]
fn one_protocol_carries_one_body() {
let bindings = Bindings::new()
.with(Binding::new("kafka", "0.5.0", &Group { group_id: "first" }).unwrap())
.with(Binding::new("kafka", "0.5.0", &Group { group_id: "second" }).unwrap());
let json = serde_json::to_value(&bindings).unwrap();
assert_eq!(json["kafka"]["groupId"], "second");
}
}