use crate::validation::{Context, ValidateWithContext};
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Default)]
#[serde(transparent)]
pub struct Bindings(pub BTreeMap<String, serde_json::Value>);
impl Bindings {
#[must_use]
pub fn get(&self, protocol: &str) -> Option<&serde_json::Value> {
self.0.get(protocol)
}
#[must_use]
pub fn binding_version(&self, protocol: &str) -> Option<&str> {
self.get(protocol)?.get("bindingVersion")?.as_str()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
}
impl ValidateWithContext for Bindings {
fn validate_with_context(&self, ctx: &mut Context) {
for (protocol, value) in &self.0 {
if !value.is_object() {
ctx.error_field(protocol, "binding must be an object");
}
}
}
}
macro_rules! bindings {
($( $name:ident => $kind:literal, $what:literal );+ $(;)?) => {
$(
#[doc = concat!("Bindings declared on ", $what, ".")]
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Default)]
#[serde(transparent)]
pub struct $name(pub Bindings);
impl std::ops::Deref for $name {
type Target = Bindings;
fn deref(&self) -> &Bindings {
&self.0
}
}
impl From<Bindings> for $name {
fn from(bindings: Bindings) -> Self {
Self(bindings)
}
}
impl ValidateWithContext for $name {
fn validate_with_context(&self, ctx: &mut Context) {
self.0.validate_with_context(ctx);
}
}
)+
crate::common::resolve::kinds! {
$( $name => Some($kind), )+
}
};
}
bindings! {
ServerBindings => "serverBindings", "a server";
ChannelBindings => "channelBindings", "a channel";
OperationBindings => "operationBindings", "an operation or an operation trait";
MessageBindings => "messageBindings", "a message or a message trait";
}
#[cfg(test)]
mod tests {
use crate::common::reference::RefOr;
#[test]
fn a_typed_binding_wraps_a_plain_one() {
let plain: Bindings = serde_json::from_value(json!({ "kafka": { "topic": "t" } })).unwrap();
let channel = ChannelBindings::from(plain.clone());
assert_eq!(
serde_json::to_value(&channel).unwrap(),
serde_json::to_value(&plain).unwrap()
);
assert_eq!(channel.binding_version("kafka"), None);
assert!(channel.get("kafka").is_some());
let mut ctx = Context::with_path(EnumSet::empty(), "#.channels.c.bindings");
let bad: RefOr<ChannelBindings> =
serde_json::from_value(json!({ "kafka": "not an object" })).unwrap();
bad.validate_with_context(&mut ctx);
assert_eq!(
ctx.errors.first().map(ToString::to_string),
Some("#.channels.c.bindings.kafka: binding must be an object".to_owned())
);
}
use super::*;
use enumset::EnumSet;
use serde_json::json;
#[test]
fn round_trips_transparently() {
let value = json!({
"kafka": { "topic": "my-topic", "bindingVersion": "0.5.0" },
"ws": { "method": "GET" }
});
let bindings: Bindings = serde_json::from_value(value.clone()).unwrap();
assert_eq!(serde_json::to_value(&bindings).unwrap(), value);
}
#[test]
fn accessors_read_protocol_and_binding_version() {
let bindings: Bindings = serde_json::from_value(json!({
"kafka": { "topic": "t", "bindingVersion": "0.5.0" },
"mqtt": { "qos": 1 }
}))
.unwrap();
assert_eq!(bindings.binding_version("kafka"), Some("0.5.0"));
assert_eq!(bindings.binding_version("mqtt"), None);
assert_eq!(bindings.binding_version("amqp"), None);
assert!(bindings.get("kafka").is_some());
assert!(bindings.get("amqp").is_none());
assert!(!bindings.is_empty());
assert!(Bindings::default().is_empty());
}
#[test]
fn validate_rejects_non_object_binding() {
let bindings: Bindings =
serde_json::from_value(json!({ "kafka": { "topic": "t" }, "ws": "nope" })).unwrap();
let mut ctx = Context::with_path(EnumSet::empty(), "#.channels.user.bindings");
bindings.validate_with_context(&mut ctx);
assert_eq!(ctx.errors.len(), 1);
assert!(ctx.errors[0] == "#.channels.user.bindings.ws: binding must be an object");
}
}