use crate::common::pointer;
use crate::common::reference::{RefOr, Reference};
use crate::common::resolve::{Resolution, Terminus, classify_unresolved, follow_tracked};
use crate::v3_1::channel::Channel;
use crate::v3_1::components::Components;
use crate::v3_1::info::Info;
use crate::v3_1::operation::{Operation, OperationReply};
use crate::v3_1::server::Server;
use crate::v3_1::version::Version;
use crate::validation::{Context, Error, Validate, ValidateWithContext, ValidationOptions};
use enumset::EnumSet;
use serde::de::DeserializeOwned;
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Default)]
pub struct Document {
pub asyncapi: Version,
#[serde(skip_serializing_if = "Option::is_none")]
pub id: Option<String>,
pub info: Info,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub servers: BTreeMap<String, RefOr<Server>>,
#[serde(rename = "defaultContentType", skip_serializing_if = "Option::is_none")]
pub default_content_type: Option<String>,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub channels: BTreeMap<String, RefOr<Channel>>,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub operations: BTreeMap<String, RefOr<Operation>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub components: Option<Components>,
#[serde(flatten)]
#[serde(with = "crate::common::extensions")]
#[serde(skip_serializing_if = "Option::is_none")]
pub extensions: Option<BTreeMap<String, serde_json::Value>>,
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum Origin {
Root,
Components,
}
struct ResolvedChannel<'a> {
at: Terminus,
channel: Option<&'a Channel>,
}
impl Document {
fn resolve<'a, T>(
&'a self,
reference: &Reference,
field: &str,
inline: Option<&'a BTreeMap<String, RefOr<T>>>,
components: Option<&'a BTreeMap<String, RefOr<T>>>,
) -> (Option<Terminus>, Resolution<'a, T>)
where
T: DeserializeOwned,
{
if reference.is_external() {
return match Terminus::parse(&reference.reference) {
Some(terminus) => (Some(terminus), Resolution::Opaque),
None => (None, Resolution::Unrecognized),
};
}
let Some(local) = reference.local_pointer() else {
return (None, Resolution::Unrecognized);
};
let Some(path) = pointer::tokens(local) else {
return (None, Resolution::Unrecognized);
};
let lookup = |path: &[String]| match path {
[c, this, key] if c == "components" && this == field => {
components.and_then(|map| map.get(key))
}
[this, key] if this == field => inline.and_then(|map| map.get(key)),
_ => None,
};
let (terminal, resolution) = match lookup(&path) {
Some(entry) => follow_tracked(self, path, entry, field, lookup),
None => (
Terminus {
resource: String::new(),
at: path,
},
classify_unresolved(self, local, field),
),
};
(Some(terminal), resolution)
}
fn check_location(reference: &Reference, field: &str, origin: Origin) -> Option<String> {
if origin == Origin::Components {
return None;
}
if reference.is_external() {
return Some(format!("must point into the root `{field}` object"));
}
let path = reference.local_pointer().and_then(pointer::tokens)?;
let rooted = matches!(path.as_slice(), [this, _] if this == field);
(!rooted).then(|| format!("must point into the root `{field}` object"))
}
fn components_map<'a, T>(
&'a self,
pick: impl Fn(&'a Components) -> &'a BTreeMap<String, RefOr<T>>,
) -> Option<&'a BTreeMap<String, RefOr<T>>> {
self.components.as_ref().map(pick)
}
fn validate_channel_servers(&self, ctx: &mut Context, channel: &Channel, origin: Origin) {
for (i, server) in channel.servers.iter().enumerate() {
let problem = Self::check_location(server, "servers", origin).or_else(|| {
let (_, resolution) = self.resolve(
server,
"servers",
Some(&self.servers),
self.components_map(|c| &c.servers),
);
resolution.problem().map(ToOwned::to_owned)
});
if let Some(problem) = problem {
ctx.in_index("servers", i, |ctx| {
ctx.error_field("$ref", format!("server `{}` {problem}", server.reference));
});
}
}
}
fn validate_operation_wiring(&self, ctx: &mut Context, operation: &Operation, origin: Origin) {
let channel = self.check_channel_ref(ctx, "channel", &operation.channel, origin);
self.check_message_refs(ctx, "messages", &operation.messages, channel.as_ref());
let Some(reply) = &operation.reply else {
return;
};
ctx.in_field("reply", |ctx| match reply {
RefOr::Reference(reference) => {
let (_, resolution) = self.resolve(
reference,
"replies",
None,
self.components_map(|c| &c.replies),
);
if let Some(problem) = resolution.problem() {
ctx.error_field("$ref", format!("reply `{}` {problem}", reference.reference));
}
}
RefOr::Item(reply) => self.validate_reply_wiring(ctx, reply, origin),
});
}
fn validate_reply_wiring(&self, ctx: &mut Context, reply: &OperationReply, origin: Origin) {
let channel = reply
.channel
.as_ref()
.and_then(|reference| self.check_channel_ref(ctx, "channel", reference, origin));
self.check_message_refs(ctx, "messages", &reply.messages, channel.as_ref());
if reply.address.is_some()
&& let Some(channel) = channel.as_ref().and_then(|resolved| resolved.channel)
&& let Some(Some(address)) = channel.address.as_ref()
{
ctx.error_field(
"address",
format!(
"requires the channel's `address` to be `null` or absent, but it is `{address}`"
),
);
}
}
fn check_channel_ref<'a>(
&'a self,
ctx: &mut Context,
field: &str,
reference: &Reference,
origin: Origin,
) -> Option<ResolvedChannel<'a>> {
let mut report = |problem: &str| {
ctx.in_field(field, |ctx| {
ctx.error_field(
"$ref",
format!("channel `{}` {problem}", reference.reference),
);
});
};
if let Some(problem) = Self::check_location(reference, "channels", origin) {
report(&problem);
return None;
}
let (at, resolution) = self.resolve(
reference,
"channels",
Some(&self.channels),
self.components_map(|c| &c.channels),
);
if let Some(problem) = resolution.problem() {
report(problem);
return None;
}
at.map(|at| ResolvedChannel {
at,
channel: resolution.found(),
})
}
fn check_message_refs(
&self,
ctx: &mut Context,
field: &str,
messages: &[Reference],
channel: Option<&ResolvedChannel<'_>>,
) {
let Some(resolved) = channel else { return };
for (i, message) in messages.iter().enumerate() {
if message.reference.is_empty() {
continue;
}
let report = |ctx: &mut Context, reason: String| {
ctx.in_index(field, i, |ctx| {
ctx.error_field("$ref", format!("message `{}` {reason}", message.reference));
});
};
let Some(named) = Terminus::parse(&message.reference) else {
report(ctx, "is not a usable JSON Pointer".to_owned());
continue;
};
let Some(key) = resolved.at.child_key("messages", &named) else {
report(ctx, format!("must point at a message of `{}`", resolved.at));
continue;
};
let Some(channel) = resolved.channel else {
continue;
};
if !channel.messages.contains_key(key) {
report(ctx, "is not one of the channel's `messages`".to_owned());
}
}
}
fn validate_components_wiring(&self, ctx: &mut Context, components: &Components) {
for (name, channel) in &components.channels {
if let Some(channel) = channel.item() {
ctx.in_key("channels", name, |ctx| {
self.validate_channel_servers(ctx, channel, Origin::Components);
});
}
}
for (name, operation) in &components.operations {
if let Some(operation) = operation.item() {
ctx.in_key("operations", name, |ctx| {
self.validate_operation_wiring(ctx, operation, Origin::Components);
});
}
}
for (name, reply) in &components.replies {
if let Some(reply) = reply.item() {
ctx.in_key("replies", name, |ctx| {
self.validate_reply_wiring(ctx, reply, Origin::Components);
});
}
}
}
fn validate_inner(&self, options: EnumSet<ValidationOptions>) -> Result<(), Error> {
let mut ctx = Context::for_document(options, self);
if let Some(id) = &self.id {
ctx.require_non_empty("id", id);
}
if let Some(content_type) = &self.default_content_type {
ctx.require_non_empty("defaultContentType", content_type);
}
ctx.in_field("info", |ctx| self.info.validate_with_context(ctx));
ctx.validate_map_keys("servers", &self.servers);
for (name, server) in &self.servers {
ctx.in_key("servers", name, |ctx| server.validate_with_context(ctx));
}
ctx.validate_map_keys("channels", &self.channels);
for (name, channel) in &self.channels {
ctx.in_key("channels", name, |ctx| {
if let Some(channel) = channel.item() {
self.validate_channel_servers(ctx, channel, Origin::Root);
}
channel.validate_with_context(ctx);
});
}
ctx.validate_map_keys("operations", &self.operations);
for (name, operation) in &self.operations {
ctx.in_key("operations", name, |ctx| {
if let Some(operation) = operation.item() {
self.validate_operation_wiring(ctx, operation, Origin::Root);
}
operation.validate_with_context(ctx);
});
}
if let Some(components) = &self.components {
ctx.in_field("components", |ctx| {
self.validate_components_wiring(ctx, components);
components.validate_with_context(ctx);
});
}
ctx.into_result()
}
}
impl Validate for Document {
fn validate(&self, options: EnumSet<ValidationOptions>) -> Result<(), Error> {
self.validate_inner(options)
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
fn minimal() -> serde_json::Value {
json!({
"asyncapi": "3.1.0",
"info": { "title": "Streetlights", "version": "1.0.0" }
})
}
fn wired() -> serde_json::Value {
json!({
"asyncapi": "3.1.0",
"info": { "title": "T", "version": "1" },
"servers": { "production": { "host": "broker:9092", "protocol": "kafka" } },
"channels": {
"userSignedUp": {
"address": "user/signedup",
"servers": [ { "$ref": "#/servers/production" } ],
"messages": { "signup": { "name": "UserSignedUp" } }
}
},
"operations": {
"receiveSignups": {
"action": "receive",
"channel": { "$ref": "#/channels/userSignedUp" },
"messages": [ { "$ref": "#/channels/userSignedUp/messages/signup" } ]
}
}
})
}
fn wired_from_components() -> serde_json::Value {
let mut value = wired();
let operation = value["operations"]["receiveSignups"].take();
value["operations"] = json!({});
value["components"] = json!({ "operations": { "receiveSignups": operation } });
value
}
fn errors_for(value: serde_json::Value) -> Vec<String> {
let doc: Document = serde_json::from_value(value).unwrap();
match doc.validate(EnumSet::empty()) {
Ok(()) => Vec::new(),
Err(err) => err.errors.iter().map(ToString::to_string).collect(),
}
}
#[test]
fn minimal_document_parses_validates_and_round_trips() {
let doc: Document = serde_json::from_value(minimal()).unwrap();
assert_eq!(doc.asyncapi, Version::V3_1_0());
doc.validate(EnumSet::empty()).expect("valid");
assert_eq!(serde_json::to_value(&doc).unwrap(), minimal());
}
#[test]
fn fully_wired_document_validates() {
let errors = errors_for(wired());
assert!(errors.is_empty(), "got: {errors:?}");
}
#[test]
fn rejects_other_spec_versions_at_parse_time() {
for version in ["2.6.0", "3.0.0"] {
let mut value = minimal();
value["asyncapi"] = json!(version);
assert!(
serde_json::from_value::<Document>(value).is_err(),
"{version} must not parse as v3.1"
);
}
}
#[test]
fn empty_id_and_default_content_type_are_reported() {
let mut value = minimal();
value["id"] = json!("");
value["defaultContentType"] = json!("");
let errors = errors_for(value);
assert!(errors.iter().any(|e| e == "#.id: must not be empty"));
assert!(
errors
.iter()
.any(|e| e == "#.defaultContentType: must not be empty")
);
}
#[test]
fn operation_channel_must_be_declared() {
let mut value = wired();
value["operations"]["receiveSignups"]["channel"] = json!({ "$ref": "#/channels/nope" });
let errors = errors_for(value);
assert!(
errors.iter().any(|e| e
== "#.operations.receiveSignups.channel.$ref: channel `#/channels/nope` names nothing in this document"),
"got: {errors:?}"
);
}
#[test]
fn operation_channel_must_point_at_a_channel() {
let mut value = wired();
value["operations"]["receiveSignups"]["channel"] =
json!({ "$ref": "#/servers/production" });
let errors = errors_for(value);
assert!(
errors
.iter()
.any(|e| e.contains("must point into the root `channels` object")),
"got: {errors:?}"
);
let mut value = wired_from_components();
value["components"]["operations"]["receiveSignups"]["channel"] =
json!({ "$ref": "#/servers/production" });
let errors = errors_for(value);
assert!(
errors
.iter()
.any(|e| e.contains("does not point at an object of the expected kind")),
"got: {errors:?}"
);
}
#[test]
fn operation_messages_must_belong_to_the_channel() {
let mut value = wired();
value["operations"]["receiveSignups"]["messages"] =
json!([ { "$ref": "#/channels/userSignedUp/messages/other" } ]);
let errors = errors_for(value);
assert!(
errors
.iter()
.any(|e| e.contains("is not one of the channel's `messages`")),
"got: {errors:?}"
);
}
#[test]
fn operation_messages_must_come_from_its_own_channel() {
let mut value = wired();
value["channels"]["other"] = json!({
"address": "other",
"messages": { "ping": { "name": "Ping" } }
});
value["operations"]["receiveSignups"]["messages"] =
json!([ { "$ref": "#/channels/other/messages/ping" } ]);
let errors = errors_for(value);
assert!(
errors.iter().any(|e| e.contains(
"message `#/channels/other/messages/ping` must point at a message of `#/channels/userSignedUp`"
)),
"got: {errors:?}"
);
}
#[test]
fn a_pointer_at_a_value_of_the_wrong_shape_is_not_opaque() {
let mut scalar = wired_from_components();
scalar["x-note"] = json!("just a string");
scalar["components"]["operations"]["receiveSignups"]["channel"] =
json!({ "$ref": "#/x-note" });
assert!(
errors_for(scalar)
.iter()
.any(|e| e.contains("does not point at an object of the expected kind")),
);
let mut info = wired_from_components();
info["components"]["operations"]["receiveSignups"]["channel"] = json!({ "$ref": "#/info" });
assert!(
errors_for(info)
.iter()
.any(|e| e.contains("does not point at an object of the expected kind")),
);
for container in ["#", "#/channels", "#/components", "#/components/channels"] {
let mut value = wired_from_components();
value["components"]["operations"]["receiveSignups"]["channel"] =
json!({ "$ref": container });
let errors = errors_for(value);
assert!(
errors
.iter()
.any(|e| e.contains("does not point at an object of the expected kind")),
"`{container}` got: {errors:?}"
);
}
let mut shaped = wired_from_components();
shaped["x-shared-channel"] = json!({ "address": "shared" });
shaped["components"]["operations"]["receiveSignups"]["channel"] =
json!({ "$ref": "#/x-shared-channel" });
shaped["components"]["operations"]["receiveSignups"]["messages"] = json!([]);
assert!(errors_for(shaped).is_empty());
}
#[test]
fn message_pointers_are_decoded_before_they_are_compared() {
let mut value = wired();
value["channels"]["userSignedUp"]["messages"] =
json!({ "sign-up": { "name": "UserSignedUp" } });
value["operations"]["receiveSignups"]["messages"] =
json!([ { "$ref": "#/channels/userSignedUp/messages/sign%2Dup" } ]);
assert!(errors_for(value).is_empty());
let mut value = wired();
value["channels"] = json!({
"user-signed": { "messages": { "sign-up": { "name": "UserSignedUp" } } }
});
value["operations"]["receiveSignups"]["channel"] =
json!({ "$ref": "#/channels/user%2Dsigned" });
value["operations"]["receiveSignups"]["messages"] =
json!([ { "$ref": "#/channels/user-signed/messages/sign%2Dup" } ]);
assert!(errors_for(value).is_empty());
}
#[test]
fn a_channels_message_is_followed_to_its_end() {
let mut value = wired();
value["channels"]["userSignedUp"]["messages"] =
json!({ "signup": { "$ref": "#/components/messages/ghost" } });
let errors = errors_for(value);
assert!(
errors
.iter()
.any(|e| e.contains("names nothing in this document")),
"got: {errors:?}"
);
let mut value = wired();
value["channels"]["userSignedUp"]["messages"] =
json!({ "signup": { "$ref": "#/components/messages/real" } });
value["components"] = json!({ "messages": { "real": { "name": "UserSignedUp" } } });
assert!(errors_for(value).is_empty());
let mut value = wired();
value["channels"]["userSignedUp"]["messages"] =
json!({ "signup": { "$ref": "#/components/messages/alias" } });
value["operations"]["receiveSignups"]["messages"] =
json!([ { "$ref": "#/components/messages/alias" } ]);
value["components"] =
json!({ "messages": { "alias": { "$ref": "#/components/messages/ghost" } } });
assert!(
errors_for(value)
.iter()
.any(|e| e.contains("names nothing in this document")),
);
}
#[test]
fn a_channel_alias_is_the_channel_it_names() {
let mut value = wired();
value["channels"]["alias"] = json!({ "$ref": "#/channels/userSignedUp" });
value["operations"]["receiveSignups"]["channel"] = json!({ "$ref": "#/channels/alias" });
assert!(errors_for(value).is_empty());
let mut value = wired();
value["channels"]["alias"] = json!({ "$ref": "#/channels/userSignedUp" });
value["channels"]["other"] = json!({ "messages": { "m": { "name": "M" } } });
value["operations"]["receiveSignups"]["channel"] = json!({ "$ref": "#/channels/alias" });
value["operations"]["receiveSignups"]["messages"] =
json!([ { "$ref": "#/channels/other/messages/m" } ]);
assert!(
errors_for(value)
.iter()
.any(|e| e.contains("must point at a message of `#/channels/userSignedUp`")),
);
}
#[test]
fn reusable_channels_and_operations_are_wired_too() {
let mut value = wired();
value["components"] = json!({
"channels": { "reusable": { "servers": [ { "$ref": "#/servers/missing" } ] } }
});
assert!(
errors_for(value).iter().any(|e| e
== "#.components.channels.reusable.servers[0].$ref: server `#/servers/missing` names nothing in this document"),
);
let mut value = wired();
value["components"] = json!({
"operations": {
"reusable": { "action": "send", "channel": { "$ref": "#/channels/missing" } }
}
});
assert!(
errors_for(value).iter().any(|e| e
== "#.components.operations.reusable.channel.$ref: channel `#/channels/missing` names nothing in this document"),
);
}
#[test]
fn messages_are_found_wherever_the_channel_lives() {
let mut value = wired_from_components();
value["components"]["operations"]["receiveSignups"]["channel"] =
json!({ "$ref": "#/components/channels/reusable" });
value["components"]["operations"]["receiveSignups"]["messages"] =
json!([ { "$ref": "#/components/channels/reusable/messages/m" } ]);
value["components"]["channels"] =
json!({ "reusable": { "messages": { "m": { "name": "M" } } } });
assert!(errors_for(value).is_empty());
let mut value = wired();
value["channels"]["other"] = json!({ "messages": { "m": { "name": "M" } } });
value["channels"]["userSignedUp"]["messages"] =
json!({ "signup": { "$ref": "#/channels/other/messages/m" } });
assert!(errors_for(value).is_empty());
let mut value = wired();
value["channels"]["userSignedUp"]["messages"] =
json!({ "signup": { "$ref": "#/components/channels/reusable/messages/m" } });
value["components"] = json!({
"channels": { "reusable": { "messages": { "m": { "name": "M" } } } }
});
assert!(errors_for(value).is_empty());
let mut value = wired();
value["channels"]["userSignedUp"]["messages"] =
json!({ "signup": { "$ref": "#/channels/other/messages/m" } });
assert!(
errors_for(value)
.iter()
.any(|e| e.contains("names nothing in this document")),
);
let mut value = wired();
value["channels"]["userSignedUp"]["messages"] = json!({ "signup": { "$ref": "#/info" } });
assert!(
errors_for(value)
.iter()
.any(|e| e.contains("does not point at an object of the expected kind")),
);
}
#[test]
fn a_message_pointer_that_is_not_a_pointer_is_reported() {
let mut value = wired();
value["operations"]["receiveSignups"]["messages"] =
json!([ { "$ref": "#/channels/bad~2escape/messages/m" } ]);
assert!(
errors_for(value)
.iter()
.any(|e| e.contains("is not a usable JSON Pointer")),
);
}
#[test]
fn message_checks_stop_where_the_channel_does() {
let mut value = wired();
value["channels"]["elsewhere"] = json!({ "$ref": "./other.yaml#/channels/userSignedUp" });
value["operations"]["receiveSignups"]["channel"] =
json!({ "$ref": "#/channels/elsewhere" });
value["operations"]["receiveSignups"]["messages"] =
json!([ { "$ref": "./other.yaml#/channels/userSignedUp/messages/m" } ]);
assert_eq!(errors_for(value), Vec::<String>::new());
let mut value = wired();
value["channels"]["elsewhere"] = json!({ "$ref": "./other.yaml#/channels/userSignedUp" });
value["operations"]["receiveSignups"]["channel"] =
json!({ "$ref": "#/channels/elsewhere" });
value["operations"]["receiveSignups"]["messages"] =
json!([ { "$ref": "#/channels/userSignedUp/messages/signup" } ]);
assert!(errors_for(value).iter().any(|e| {
e.contains("must point at a message of `other.yaml#/channels/userSignedUp`")
}),);
}
#[test]
fn reusable_entries_that_are_themselves_refs_are_left_alone() {
let mut value = wired();
value["components"] = json!({
"channels": { "alias": { "$ref": "#/channels/userSignedUp" } },
"operations": { "alias": { "$ref": "#/operations/receiveSignups" } },
"messages": {
"real": { "name": "M" },
"alias": { "$ref": "#/components/messages/real" }
},
"operationTraits": {
"real": { "title": "T" },
"alias": { "$ref": "#/components/operationTraits/real" }
},
"messageTraits": {
"real": { "title": "T" },
"alias": { "$ref": "#/components/messageTraits/real" }
}
});
assert_eq!(errors_for(value), Vec::<String>::new());
}
#[test]
fn unresolvable_reference_shapes_are_each_reported() {
let mut empty = wired();
empty["operations"]["receiveSignups"]["channel"] = json!({ "$ref": "" });
let errors = errors_for(empty);
assert!(
errors
.iter()
.any(|e| e.contains("is not a usable JSON Pointer"))
);
let mut outside = wired_from_components();
outside["components"]["operations"]["receiveSignups"]["channel"] =
json!({ "$ref": "./other.yaml#bad" });
assert!(
errors_for(outside)
.iter()
.any(|e| e.contains("is not a usable JSON Pointer"))
);
let mut aliased = wired();
aliased["channels"]["alias"] = json!({ "$ref": "./other.yaml#bad" });
aliased["operations"]["receiveSignups"]["channel"] = json!({ "$ref": "#/channels/alias" });
aliased["operations"]["receiveSignups"]["messages"] = json!([]);
assert!(
errors_for(aliased)
.iter()
.any(|e| e.contains("is not a usable JSON Pointer"))
);
let mut malformed = wired();
malformed["operations"]["receiveSignups"]["channel"] =
json!({ "$ref": "#/channels/bad~2escape" });
assert!(
errors_for(malformed)
.iter()
.any(|e| e.contains("is not a usable JSON Pointer"))
);
let mut missing_component = wired_from_components();
missing_component["components"]["operations"]["receiveSignups"]["channel"] =
json!({ "$ref": "#/components/channels/nope" });
let errors = errors_for(missing_component);
assert!(
errors.iter().any(|e| e
.contains("channel `#/components/channels/nope` names nothing in this document")),
"got: {errors:?}"
);
let mut wrong_kind = wired_from_components();
wrong_kind["components"]["operations"]["receiveSignups"]["channel"] =
json!({ "$ref": "#/channels/userSignedUp/messages/signup" });
wrong_kind["components"]["operations"]["receiveSignups"]["messages"] = json!([]);
assert!(
errors_for(wrong_kind)
.iter()
.any(|e| e.contains("does not point at an object of the expected kind")),
);
}
#[test]
fn only_unresolvable_message_refs_are_skipped() {
let mut value = wired();
value["operations"]["receiveSignups"]["messages"] = json!([ { "$ref": "" } ]);
let errors = errors_for(value);
assert!(
!errors.iter().any(|e| e.contains("must point at a message")),
"got: {errors:?}"
);
let mut value = wired();
value["operations"]["receiveSignups"]["messages"] =
json!([ { "$ref": "./messages.yaml#/signup" } ]);
let errors = errors_for(value);
assert!(
errors.iter().any(|e| e.contains(
"message `./messages.yaml#/signup` must point at a message of `#/channels/userSignedUp`"
)),
"got: {errors:?}"
);
}
#[test]
fn local_message_refs_of_the_wrong_kind_are_reported() {
let mut value = wired();
value["operations"]["receiveSignups"]["messages"] =
json!([ { "$ref": "#/components/schemas/notAMessage" } ]);
let errors = errors_for(value);
assert!(
errors.iter().any(|e| e.contains("must point at a message")),
"got: {errors:?}"
);
}
#[test]
fn operation_messages_may_not_name_components_directly() {
let mut value = wired();
value["channels"]["userSignedUp"]["messages"] =
json!({ "signup": { "$ref": "#/components/messages/signup" } });
value["operations"]["receiveSignups"]["messages"] =
json!([ { "$ref": "#/components/messages/signup" } ]);
value["components"] = json!({ "messages": { "signup": { "name": "Signup" } } });
let errors = errors_for(value);
assert!(
errors.iter().any(|e| e.contains(
"message `#/components/messages/signup` must point at a message of `#/channels/userSignedUp`"
)),
"got: {errors:?}"
);
}
#[test]
fn a_reusable_message_is_named_through_the_channel_that_lists_it() {
let value = json!({
"asyncapi": "3.1.0",
"info": { "title": "T", "version": "1" },
"channels": {
"user": {
"address": "user",
"messages": { "signup": { "$ref": "#/components/messages/signup" } }
}
},
"operations": {
"send": {
"action": "send",
"channel": { "$ref": "#/channels/user" },
"messages": [ { "$ref": "#/channels/user/messages/signup" } ]
}
},
"components": { "messages": { "signup": { "name": "Signup" } } }
});
assert!(errors_for(value.clone()).is_empty());
let mut detached = value;
detached["channels"]["user"]["messages"] = json!({});
let errors = errors_for(detached);
assert!(
errors
.iter()
.any(|e| e.contains("is not one of the channel's `messages`")),
"got: {errors:?}"
);
}
#[test]
fn channel_servers_must_be_declared() {
let mut value = wired();
value["channels"]["userSignedUp"]["servers"] = json!([ { "$ref": "#/servers/staging" } ]);
let errors = errors_for(value);
assert!(
errors.iter().any(|e| e
== "#.channels.userSignedUp.servers[0].$ref: server `#/servers/staging` names nothing in this document"),
"got: {errors:?}"
);
let mut wrong_kind = wired();
wrong_kind["channels"]["userSignedUp"]["servers"] =
json!([ { "$ref": "#/channels/userSignedUp" } ]);
let errors = errors_for(wrong_kind);
assert!(
errors
.iter()
.any(|e| e.contains("must point into the root `servers` object")),
"got: {errors:?}"
);
let mut wrong_kind = wired();
wrong_kind["components"] = json!({
"channels": {
"reusable": { "servers": [ { "$ref": "#/channels/userSignedUp" } ] }
}
});
let errors = errors_for(wrong_kind);
assert!(
errors
.iter()
.any(|e| e.contains("does not point at an object of the expected kind")),
"got: {errors:?}"
);
}
#[test]
fn components_channels_and_servers_resolve_too() {
let value = json!({
"asyncapi": "3.1.0",
"info": { "title": "T", "version": "1" },
"components": {
"operations": {
"send": {
"action": "send",
"channel": { "$ref": "#/components/channels/user" }
}
},
"servers": { "prod": { "host": "h", "protocol": "kafka" } },
"channels": {
"user": { "address": "user", "servers": [ { "$ref": "#/components/servers/prod" } ] }
}
}
});
assert!(errors_for(value).is_empty());
}
#[test]
fn root_objects_may_only_reference_the_root() {
let mut value = wired();
value["channels"]["userSignedUp"]["servers"] =
json!([ { "$ref": "#/components/servers/s" } ]);
value["components"] = json!({ "servers": { "s": { "host": "h", "protocol": "kafka" } } });
assert!(
errors_for(value).iter().any(|e| e
== "#.channels.userSignedUp.servers[0].$ref: server `#/components/servers/s` must point into the root `servers` object"),
);
let mut value = wired();
value["operations"]["receiveSignups"]["channel"] =
json!({ "$ref": "#/components/channels/c" });
value["operations"]["receiveSignups"]["messages"] = json!([]);
value["components"] = json!({ "channels": { "c": { "address": "c" } } });
assert!(
errors_for(value).iter().any(|e| e
== "#.operations.receiveSignups.channel.$ref: channel `#/components/channels/c` must point into the root `channels` object"),
);
let mut value = wired();
value["operations"]["receiveSignups"]["reply"] =
json!({ "channel": { "$ref": "#/components/channels/c" } });
value["components"] = json!({ "channels": { "c": { "address": "c" } } });
assert!(
errors_for(value).iter().any(|e| e
== "#.operations.receiveSignups.reply.channel.$ref: channel `#/components/channels/c` must point into the root `channels` object"),
);
}
#[test]
fn a_dangling_alias_is_reported_even_if_nothing_uses_it() {
for (field, alias) in [
("servers", "#/servers/ghost"),
("channels", "#/channels/ghost"),
("operations", "#/operations/ghost"),
] {
let mut value = wired();
value[field]["unused"] = json!({ "$ref": alias });
let errors = errors_for(value);
assert!(
errors.iter().any(|e| e
== &format!("#.{field}.unused.$ref: `{alias}` names nothing in this document")),
"got: {errors:?}"
);
}
let mut value = wired();
value["components"] = json!({
"servers": { "unused": { "$ref": "#/servers/ghost" } },
"channels": { "unused": { "$ref": "#/channels/ghost" } },
"operations": { "unused": { "$ref": "#/operations/ghost" } },
"messages": { "unused": { "$ref": "#/components/messages/ghost" } },
"replies": { "unused": { "$ref": "#/components/replies/ghost" } },
"securitySchemes": { "unused": { "$ref": "#/components/securitySchemes/ghost" } }
});
let errors = errors_for(value);
for field in [
"servers",
"channels",
"operations",
"messages",
"replies",
"securitySchemes",
] {
assert!(
errors.iter().any(
|e| e.starts_with(&format!("#.components.{field}.unused.$ref:"))
&& e.contains("names nothing in this document")
),
"{field} got: {errors:?}"
);
}
}
#[test]
fn a_declared_alias_may_not_name_another_kind() {
let mut value = wired();
value["components"] = json!({
"channels": { "alias": { "$ref": "#/components/messages/m" } },
"messages": { "m": { "name": "M" } }
});
assert!(
errors_for(value).iter().any(|e| e
== "#.components.channels.alias.$ref: `#/components/messages/m` does not point at an object of the expected kind"),
);
let mut value = wired();
value["components"] = json!({ "messages": { "alias": { "$ref": "#/info" } } });
assert!(
errors_for(value).iter().any(|e| e
== "#.components.messages.alias.$ref: `#/info` does not point at an object of the expected kind"),
);
}
#[test]
fn replies_are_wired_wherever_they_are_declared() {
let mut value = wired();
value["operations"]["receiveSignups"]["reply"] =
json!({ "$ref": "#/components/replies/shared" });
value["components"] = json!({
"replies": { "shared": { "channel": { "$ref": "#/channels/missing" } } }
});
let errors = errors_for(value);
assert!(
errors.iter().any(|e| e
== "#.components.replies.shared.channel.$ref: channel `#/channels/missing` names nothing in this document"),
"got: {errors:?}"
);
let mut value = wired();
value["operations"]["receiveSignups"]["reply"] =
json!({ "$ref": "#/components/replies/ghost" });
let errors = errors_for(value);
assert!(
errors.iter().any(|e| e
== "#.operations.receiveSignups.reply.$ref: reply `#/components/replies/ghost` names nothing in this document"),
"got: {errors:?}"
);
}
#[test]
fn a_channel_is_more_than_its_key() {
let mut value = wired();
value["operations"] = json!({});
value["channels"]["events"] = json!({ "messages": { "m": { "name": "Root" } } });
value["components"] = json!({
"channels": { "events": { "messages": { "m": { "name": "Reusable" } } } },
"operations": {
"send": {
"action": "send",
"channel": { "$ref": "#/components/channels/events" },
"messages": [ { "$ref": "#/channels/events/messages/m" } ]
}
}
});
let errors = errors_for(value);
assert!(
errors
.iter()
.any(|e| e.contains("must point at a message of `#/components/channels/events`")),
"got: {errors:?}"
);
}
#[test]
fn a_nested_reference_is_followed_without_being_used() {
let value = json!({
"asyncapi": "3.1.0",
"info": { "title": "T", "version": "1" },
"channels": {
"c": { "messages": { "m": { "$ref": "#/components/messages/missing" } } }
}
});
assert_eq!(
errors_for(value),
vec![
"#.channels.c.messages.m.$ref: `#/components/messages/missing` names nothing in this document"
]
);
let value = json!({
"asyncapi": "3.1.0",
"info": { "title": "T", "version": "1" },
"channels": { "c": { "messages": { "m": { "$ref": "#/info/title/deeper" } } } }
});
assert_eq!(
errors_for(value),
vec![
"#.channels.c.messages.m.$ref: `#/info/title/deeper` names nothing in this document"
]
);
let value = json!({
"asyncapi": "3.1.0",
"info": { "title": "T", "version": "1" },
"servers": {
"s": {
"host": "h",
"protocol": "kafka",
"variables": { "v": { "$ref": "#/components/serverVariables/missing" } }
}
},
"channels": { "c": { "parameters": { "p": { "$ref": "#/components/parameters/missing" } } } }
});
let errors = errors_for(value);
assert!(
errors.iter().any(|e| e
== "#.servers.s.variables.v.$ref: `#/components/serverVariables/missing` names nothing in this document"),
"got: {errors:?}"
);
assert!(
errors.iter().any(|e| e
== "#.channels.c.parameters.p.$ref: `#/components/parameters/missing` names nothing in this document"),
"got: {errors:?}"
);
}
#[test]
fn a_wired_reference_is_reported_once() {
let mut value = wired();
value["operations"]["receiveSignups"]["channel"] = json!({ "$ref": "#/channels/nope" });
value["operations"]["receiveSignups"]["messages"] = json!([]);
assert_eq!(
errors_for(value),
vec![
"#.operations.receiveSignups.channel.$ref: channel `#/channels/nope` names nothing in this document"
]
);
}
#[test]
fn a_reference_out_of_the_document_is_still_out_of_the_root() {
let mut value = wired();
value["channels"]["userSignedUp"]["servers"] =
json!([ { "$ref": "./other.yaml#/servers/s" } ]);
assert!(
errors_for(value).iter().any(|e| e
== "#.channels.userSignedUp.servers[0].$ref: server `./other.yaml#/servers/s` must point into the root `servers` object"),
);
let mut value = wired();
value["operations"]["receiveSignups"]["channel"] =
json!({ "$ref": "./other.yaml#/channels/c" });
value["operations"]["receiveSignups"]["messages"] = json!([]);
assert!(
errors_for(value).iter().any(|e| e
== "#.operations.receiveSignups.channel.$ref: channel `./other.yaml#/channels/c` must point into the root `channels` object"),
);
}
#[test]
fn a_reply_address_needs_a_channel_without_one() {
let mut value = wired();
value["channels"]["replies"] = json!({ "address": "reply-topic" });
value["operations"]["receiveSignups"]["reply"] = json!({
"channel": { "$ref": "#/channels/replies" },
"address": { "location": "$message.header#/replyTo" }
});
assert!(
errors_for(value).iter().any(|e| e
== "#.operations.receiveSignups.reply.address: requires the channel's `address` to be `null` or absent, but it is `reply-topic`"),
);
for address in [json!(null), json!("ABSENT")] {
let mut value = wired();
let mut channel = json!({});
if address != json!("ABSENT") {
channel["address"] = address.clone();
}
value["channels"]["replies"] = channel;
value["operations"]["receiveSignups"]["reply"] = json!({
"channel": { "$ref": "#/channels/replies" },
"address": { "location": "$message.header#/replyTo" }
});
assert_eq!(
errors_for(value),
Vec::<String>::new(),
"address {address:?}"
);
}
let mut value = wired();
value["channels"]["replies"] = json!({ "address": "reply-topic" });
value["operations"]["receiveSignups"]["reply"] =
json!({ "channel": { "$ref": "#/channels/replies" } });
assert_eq!(errors_for(value), Vec::<String>::new());
}
#[test]
fn application_data_is_not_searched_for_references() {
let mut value = wired();
value["channels"]["userSignedUp"]["messages"]["signup"]["examples"] = json!([
{ "name": "one", "payload": { "$ref": "#/business/id" } }
]);
assert_eq!(errors_for(value), Vec::<String>::new());
let mut value = wired();
value["channels"]["userSignedUp"]["messages"]["signup"]["payload"] = json!({
"schemaFormat": "application/vnd.apache.avro;version=1.9.0",
"schema": { "type": "record", "fields": { "$ref": "#/dialect/type" } }
});
assert_eq!(errors_for(value), Vec::<String>::new());
let mut value = wired();
value["channels"]["userSignedUp"]["bindings"] =
json!({ "kafka": { "topic": { "$ref": "#/broker/topic" } } });
assert_eq!(errors_for(value), Vec::<String>::new());
}
#[test]
fn a_channel_in_another_document_keeps_its_messages_there() {
let mut value = wired_from_components();
value["components"]["operations"]["receiveSignups"]["channel"] =
json!({ "$ref": "./other.yaml#/channels/c" });
value["components"]["operations"]["receiveSignups"]["messages"] =
json!([ { "$ref": "./other.yaml#/channels/c/messages/m" } ]);
assert_eq!(errors_for(value), Vec::<String>::new());
let mut value = wired_from_components();
value["components"]["operations"]["receiveSignups"]["channel"] =
json!({ "$ref": "./other.yaml#/channels/c" });
value["components"]["operations"]["receiveSignups"]["messages"] =
json!([ { "$ref": "#/channels/userSignedUp/messages/signup" } ]);
assert!(
errors_for(value)
.iter()
.any(|e| e.contains("must point at a message of `other.yaml#/channels/c`")),
);
}
#[test]
fn an_extension_does_not_declare_kinds() {
let mut value = wired_from_components();
value["x-store"] = json!({ "messages": { "c": { "address": "stored" } } });
value["components"]["operations"]["receiveSignups"]["channel"] =
json!({ "$ref": "#/x-store/messages/c" });
value["components"]["operations"]["receiveSignups"]["messages"] = json!([]);
assert_eq!(errors_for(value), Vec::<String>::new());
}
#[test]
fn every_reference_the_model_holds_is_followed() {
let mut value = wired();
value["info"]["tags"] = json!([ { "$ref": "#/components/tags/ghost" } ]);
assert!(errors_for(value).iter().any(|e| e
== "#.info.tags[0].$ref: `#/components/tags/ghost` names nothing in this document"),);
let mut value = wired();
value["channels"]["userSignedUp"]["messages"]["signup"]["payload"] = json!({
"type": "object",
"properties": { "p": { "$ref": "#/components/schemas/ghost" } }
});
assert!(
errors_for(value).iter().any(|e| e
== "#.channels.userSignedUp.messages.signup.payload.properties.p.$ref: `#/components/schemas/ghost` names nothing in this document"),
);
let mut value = wired();
value["channels"]["userSignedUp"]["messages"]["signup"]["traits"] =
json!([ { "headers": { "$ref": "#/components/schemas/ghost" } } ]);
assert!(
errors_for(value).iter().any(|e| e
== "#.channels.userSignedUp.messages.signup.traits[0].headers.$ref: `#/components/schemas/ghost` names nothing in this document"),
);
}
#[test]
fn the_same_resource_spelled_differently_is_the_same_resource() {
let mut value = wired_from_components();
value["components"]["operations"]["receiveSignups"]["channel"] =
json!({ "$ref": "./channels.yaml#/c" });
value["components"]["operations"]["receiveSignups"]["messages"] =
json!([ { "$ref": "channels.yaml#/c/messages/m" } ]);
assert_eq!(errors_for(value), Vec::<String>::new());
let mut value = wired_from_components();
value["components"]["operations"]["receiveSignups"]["channel"] =
json!({ "$ref": "./channels.yaml#/c" });
value["components"]["operations"]["receiveSignups"]["messages"] =
json!([ { "$ref": "../channels.yaml#/c/messages/m" } ]);
assert!(
errors_for(value)
.iter()
.any(|e| e.contains("must point at a message of `channels.yaml#/c`")),
);
}
#[test]
fn an_extension_inside_the_document_declares_nothing_either() {
let mut value = wired_from_components();
value["components"]["x-store"] = json!({ "messages": { "c": { "address": "stored" } } });
value["components"]["operations"]["receiveSignups"]["channel"] =
json!({ "$ref": "#/components/x-store/messages/c" });
value["components"]["operations"]["receiveSignups"]["messages"] = json!([]);
assert_eq!(errors_for(value), Vec::<String>::new());
let mut value = wired();
value["channels"]["x-thing"] = json!({ "messages": { "m": { "name": "M" } } });
value["operations"]["receiveSignups"]["channel"] = json!({ "$ref": "#/channels/x-thing" });
value["operations"]["receiveSignups"]["messages"] =
json!([ { "$ref": "#/channels/x-thing/messages/m" } ]);
assert_eq!(errors_for(value), Vec::<String>::new());
}
#[test]
fn an_alias_is_followed_before_its_kind_is_judged() {
let mut value = wired();
value["x-shared"] = json!({ "tags": [ { "$ref": "#/components/tags/real" } ] });
value["servers"]["production"]["tags"] = json!([ { "$ref": "#/x-shared/tags/0" } ]);
value["components"] = json!({ "tags": { "real": { "name": "real" } } });
assert_eq!(errors_for(value), Vec::<String>::new());
let mut value = wired();
value["x-shared"] = json!({ "tags": [ { "$ref": "#/components/schemas/notATag" } ] });
value["servers"]["production"]["tags"] = json!([ { "$ref": "#/x-shared/tags/0" } ]);
value["components"] = json!({ "schemas": { "notATag": { "type": "object" } } });
assert!(
errors_for(value)
.iter()
.any(|e| e.contains("does not point at an object of the expected kind")),
);
}
#[test]
fn a_nested_reference_is_judged_by_its_kind_too() {
let mut value = wired();
value["info"]["tags"] = json!([ { "$ref": "#/components/schemas/notATag" } ]);
value["components"] = json!({ "schemas": { "notATag": { "type": "object" } } });
assert!(
errors_for(value).iter().any(|e| e
== "#.info.tags[0].$ref: `#/components/schemas/notATag` does not point at an object of the expected kind"),
);
let mut value = wired();
value["channels"]["userSignedUp"]["messages"]["signup"]["payload"] = json!({
"properties": { "p": { "$ref": "#/components/messages/notASchema" } }
});
value["components"] = json!({ "messages": { "notASchema": { "name": "M" } } });
assert!(
errors_for(value).iter().any(|e| e
.starts_with("#.channels.userSignedUp.messages.signup.payload.properties.p.$ref:")
&& e.contains("does not point at an object of the expected kind")),
);
let mut value = wired();
value["info"]["tags"] = json!([ { "$ref": "#/bad~2escape" } ]);
assert_eq!(
errors_for(value),
vec!["#.info.tags[0].$ref: `#/bad~2escape` is not a usable JSON Pointer"]
);
let mut value = wired();
value["info"]["tags"] = json!([ { "$ref": "#/components/tags/real" } ]);
value["components"] = json!({ "tags": { "real": { "name": "real" } } });
assert_eq!(errors_for(value), Vec::<String>::new());
}
#[test]
fn a_pointer_below_a_reference_object_names_nothing() {
let mut value = wired();
value["info"]["tags"] = json!([ { "$ref": "#/components/tags/alias/name" } ]);
value["components"] = json!({ "tags": { "alias": { "$ref": "other.yaml#/tag" } } });
assert!(
errors_for(value).iter().any(|e| e
== "#.info.tags[0].$ref: `#/components/tags/alias/name` names nothing in this document"),
);
}
#[test]
fn a_component_key_may_look_like_an_extension() {
let value = json!({
"asyncapi": "3.1.0",
"info": { "title": "T", "version": "1" },
"components": {
"channels": { "x-thing": { "messages": { "m": { "name": "M" } } } },
"operations": {
"o": {
"action": "send",
"channel": { "$ref": "#/components/channels/x-thing/messages/m" }
}
}
}
});
assert!(
errors_for(value)
.iter()
.any(|e| e.contains("does not point at an object of the expected kind")),
);
let value = json!({
"asyncapi": "3.1.0",
"info": { "title": "T", "version": "1" },
"components": {
"channels": { "x-thing": { "messages": { "m": { "name": "M" } } } },
"operations": {
"o": {
"action": "send",
"channel": { "$ref": "#/components/channels/x-thing" },
"messages": [ { "$ref": "#/components/channels/x-thing/messages/m" } ]
}
}
}
});
assert_eq!(errors_for(value), Vec::<String>::new());
}
#[test]
fn an_empty_path_segment_is_a_segment() {
let mut value = wired_from_components();
value["components"]["operations"]["receiveSignups"]["channel"] =
json!({ "$ref": "a//b.yaml#/c" });
value["components"]["operations"]["receiveSignups"]["messages"] =
json!([ { "$ref": "a/b.yaml#/c/messages/m" } ]);
assert!(
errors_for(value)
.iter()
.any(|e| e.contains("must point at a message of `a//b.yaml#/c`")),
);
}
#[test]
fn structure_continues_below_a_singleton() {
let mut value = wired_from_components();
value["info"]["tags"] = json!([ { "name": "t" } ]);
value["components"]["operations"]["receiveSignups"]["channel"] =
json!({ "$ref": "#/info/tags/0" });
value["components"]["operations"]["receiveSignups"]["messages"] = json!([]);
assert!(
errors_for(value).iter().any(|e| e
== "#.components.operations.receiveSignups.channel.$ref: channel `#/info/tags/0` does not point at an object of the expected kind"),
);
let mut value = wired();
value["info"]["tags"] = json!([ { "name": "t" }, { "$ref": "#/info/tags/0" } ]);
assert_eq!(errors_for(value), Vec::<String>::new());
}
#[test]
fn a_traits_bindings_are_the_traits_kind_of_bindings() {
let mut value = wired();
value["components"] = json!({
"messageBindings": { "mb": { "kafka": {} } },
"operationTraits": { "t": { "bindings": { "$ref": "#/components/messageBindings/mb" } } }
});
assert!(
errors_for(value).iter().any(|e| e
== "#.components.operationTraits.t.bindings.$ref: `#/components/messageBindings/mb` does not point at an object of the expected kind"),
);
let mut value = wired();
value["operations"]["receiveSignups"]["traits"] =
json!([ { "bindings": { "$ref": "#/components/messageBindings/mb" } } ]);
value["components"] = json!({ "messageBindings": { "mb": { "kafka": {} } } });
assert!(
errors_for(value).iter().any(|e| e
== "#.operations.receiveSignups.traits[0].bindings.$ref: `#/components/messageBindings/mb` does not point at an object of the expected kind"),
);
let mut value = wired();
value["channels"]["userSignedUp"]["messages"]["signup"]["traits"] =
json!([ { "bindings": { "$ref": "#/components/operationBindings/ob" } } ]);
value["components"] = json!({ "operationBindings": { "ob": { "kafka": {} } } });
assert!(
errors_for(value).iter().any(|e| e
== "#.channels.userSignedUp.messages.signup.traits[0].bindings.$ref: `#/components/operationBindings/ob` does not point at an object of the expected kind"),
);
let mut value = wired();
value["components"] = json!({
"messageBindings": { "mb": { "kafka": {} } },
"messageTraits": { "t": { "bindings": { "$ref": "#/components/messageBindings/mb" } } }
});
assert_eq!(errors_for(value), Vec::<String>::new());
}
#[test]
fn a_key_may_look_like_an_extension_at_any_depth() {
let value = json!({
"asyncapi": "3.1.0",
"info": { "title": "T", "version": "1" },
"components": {
"channels": { "c": { "messages": { "x-message": { "name": "M" } } } },
"operations": {
"o": {
"action": "send",
"channel": { "$ref": "#/components/channels/c/messages/x-message" }
}
}
}
});
assert!(
errors_for(value)
.iter()
.any(|e| e.contains("does not point at an object of the expected kind")),
);
let value = json!({
"asyncapi": "3.1.0",
"info": { "title": "T", "version": "1" },
"components": {
"channels": { "c": { "messages": { "x-message": { "name": "M" } } } },
"operations": {
"o": {
"action": "send",
"channel": { "$ref": "#/components/channels/c" },
"messages": [ { "$ref": "#/components/channels/c/messages/x-message" } ]
}
}
}
});
assert_eq!(errors_for(value), Vec::<String>::new());
}
#[test]
fn a_resource_is_compared_by_its_path_alone() {
let mut value = wired_from_components();
value["components"]["operations"]["receiveSignups"]["channel"] =
json!({ "$ref": "a//../b.yaml#/c" });
value["components"]["operations"]["receiveSignups"]["messages"] =
json!([ { "$ref": "a/b.yaml#/c/messages/m" } ]);
assert_eq!(errors_for(value), Vec::<String>::new());
let mut value = wired_from_components();
value["components"]["operations"]["receiveSignups"]["channel"] =
json!({ "$ref": "http://host?x=/a/../b#/c" });
value["components"]["operations"]["receiveSignups"]["messages"] =
json!([ { "$ref": "http://host?x=/b#/c/messages/m" } ]);
assert!(
errors_for(value)
.iter()
.any(|e| e.contains("must point at a message of `http://host?x=/a/../b#/c`")),
);
}
#[test]
fn a_boolean_is_a_schema() {
let mut value = wired();
value["channels"]["userSignedUp"]["messages"]["signup"]["payload"] =
json!({ "properties": { "p": { "$ref": "#/components/schemas/always" } } });
value["components"] = json!({ "schemas": { "always": true } });
assert_eq!(errors_for(value), Vec::<String>::new());
}
#[test]
fn a_nested_map_says_what_it_holds() {
let mut value = wired_from_components();
value["components"]["schemas"] =
json!({ "s": { "properties": { "p": { "type": "string" } } } });
value["components"]["operations"]["receiveSignups"]["channel"] =
json!({ "$ref": "#/components/schemas/s/properties/p" });
value["components"]["operations"]["receiveSignups"]["messages"] = json!([]);
assert!(
errors_for(value).iter().any(|e| e
== "#.components.operations.receiveSignups.channel.$ref: channel `#/components/schemas/s/properties/p` does not point at an object of the expected kind"),
);
let mut value = wired_from_components();
value["servers"]["production"]["variables"] = json!({ "v": { "default": "d" } });
value["components"]["operations"]["receiveSignups"]["channel"] =
json!({ "$ref": "#/servers/production/variables" });
value["components"]["operations"]["receiveSignups"]["messages"] = json!([]);
assert!(
errors_for(value)
.iter()
.any(|e| e.contains("does not point at an object of the expected kind")),
);
let mut value = wired();
value["channels"]["userSignedUp"]["messages"]["signup"]["payload"] =
json!({ "properties": { "p": { "$ref": "#/components/schemas/s/properties/inner" } } });
value["components"] = json!({
"schemas": { "s": { "properties": { "inner": { "type": "string" } } } }
});
assert_eq!(errors_for(value), Vec::<String>::new());
}
#[test]
fn a_chain_is_judged_where_it_ends() {
let mut value = wired_from_components();
value["x-alias"] = json!({ "$ref": "#/components/messages/m" });
value["components"]["messages"] = json!({ "m": { "name": "M" } });
value["components"]["operations"]["receiveSignups"]["channel"] =
json!({ "$ref": "#/x-alias" });
value["components"]["operations"]["receiveSignups"]["messages"] = json!([]);
assert!(
errors_for(value).iter().any(|e| e
== "#.components.operations.receiveSignups.channel.$ref: channel `#/x-alias` does not point at an object of the expected kind"),
);
let mut value = wired_from_components();
value["x-alias"] = json!({ "$ref": "#/components/channels/c" });
value["components"]["channels"] = json!({ "c": { "address": "a" } });
value["components"]["operations"]["receiveSignups"]["channel"] =
json!({ "$ref": "#/x-alias" });
value["components"]["operations"]["receiveSignups"]["messages"] = json!([]);
assert_eq!(errors_for(value), Vec::<String>::new());
}
#[test]
fn a_single_object_is_not_a_map() {
let mut value = wired_from_components();
value["info"]["externalDocs"] = json!({
"url": "https://example.com",
"x-store": { "messages": { "c": { "address": "stored" } } }
});
value["components"]["operations"]["receiveSignups"]["channel"] =
json!({ "$ref": "#/info/externalDocs/x-store/messages/c" });
value["components"]["operations"]["receiveSignups"]["messages"] = json!([]);
assert_eq!(errors_for(value), Vec::<String>::new());
}
#[test]
fn a_resource_is_compared_with_its_escapes_decoded() {
let mut value = wired_from_components();
value["components"]["operations"]["receiveSignups"]["channel"] =
json!({ "$ref": "a/%62.yaml#/c" });
value["components"]["operations"]["receiveSignups"]["messages"] =
json!([ { "$ref": "a/b.yaml#/c/messages/m" } ]);
assert_eq!(errors_for(value), Vec::<String>::new());
let mut value = wired_from_components();
value["components"]["operations"]["receiveSignups"]["channel"] =
json!({ "$ref": "a%2Fb.yaml#/c" });
value["components"]["operations"]["receiveSignups"]["messages"] =
json!([ { "$ref": "a/b.yaml#/c/messages/m" } ]);
assert!(
errors_for(value)
.iter()
.any(|e| e.contains("must point at a message of `a%2Fb.yaml#/c`")),
);
}
#[test]
fn a_trait_or_binding_is_whatever_holds_it() {
let value = json!({
"asyncapi": "3.1.0",
"info": { "title": "T", "version": "1" },
"components": {
"channels": { "c": { "address": "a" } },
"messages": { "m": { "name": "M", "traits": [ { "title": "mt" } ] } },
"operations": {
"o": {
"action": "send",
"channel": { "$ref": "#/components/channels/c" },
"traits": [ { "$ref": "#/components/messages/m/traits/0" } ]
}
}
}
});
assert!(
errors_for(value).iter().any(|e| e
== "#.components.operations.o.traits[0].$ref: `#/components/messages/m/traits/0` does not point at an object of the expected kind"),
);
let value = json!({
"asyncapi": "3.1.0",
"info": { "title": "T", "version": "1" },
"components": {
"messages": { "m": { "name": "M", "bindings": { "kafka": {} } } },
"channels": {
"c": {
"address": "a",
"bindings": { "$ref": "#/components/messages/m/bindings" }
}
}
}
});
assert!(
errors_for(value).iter().any(|e| e
== "#.components.channels.c.bindings.$ref: `#/components/messages/m/bindings` does not point at an object of the expected kind"),
);
let value = json!({
"asyncapi": "3.1.0",
"info": { "title": "T", "version": "1" },
"components": {
"channels": { "c": { "address": "a" } },
"operations": {
"o": {
"action": "send",
"channel": { "$ref": "#/components/channels/c" },
"traits": [ { "title": "ot" } ]
}
},
"messages": {
"m": { "name": "M", "traits": [ { "$ref": "#/components/operations/o/traits/0" } ] }
}
}
});
assert!(
errors_for(value).iter().any(|e| e
== "#.components.messages.m.traits[0].$ref: `#/components/operations/o/traits/0` does not point at an object of the expected kind"),
);
let value = json!({
"asyncapi": "3.1.0",
"info": { "title": "T", "version": "1" },
"components": {
"messages": {
"m": { "name": "M", "bindings": { "kafka": {} } },
"other": { "name": "O", "bindings": { "$ref": "#/components/messages/m/bindings" } }
}
}
});
assert_eq!(errors_for(value), Vec::<String>::new());
}
#[test]
fn items_is_one_schema_or_a_list_of_them() {
let mut value = wired();
value["channels"]["userSignedUp"]["messages"]["signup"]["payload"] =
json!({ "$ref": "#/components/schemas/list/items" });
value["components"] = json!({
"schemas": { "list": { "type": "array", "items": { "type": "string" } } }
});
assert_eq!(errors_for(value), Vec::<String>::new());
let mut value = wired();
value["channels"]["userSignedUp"]["messages"]["signup"]["payload"] =
json!({ "$ref": "#/components/schemas/tuple/items/1" });
value["components"] = json!({
"schemas": {
"tuple": { "items": [ { "type": "string" }, { "type": "number" } ] }
}
});
assert_eq!(errors_for(value), Vec::<String>::new());
let mut value = wired();
value["channels"]["userSignedUp"]["messages"]["signup"]["payload"] =
json!({ "$ref": "#/components/schemas/list/items/properties/p" });
value["components"] = json!({
"schemas": {
"list": { "items": { "properties": { "p": { "type": "string" } } } }
}
});
assert_eq!(errors_for(value), Vec::<String>::new());
let mut value = wired_from_components();
value["components"]["schemas"] = json!({
"list": { "type": "array", "items": { "type": "string" } }
});
value["components"]["operations"]["receiveSignups"]["channel"] =
json!({ "$ref": "#/components/schemas/list/items" });
value["components"]["operations"]["receiveSignups"]["messages"] = json!([]);
assert!(
errors_for(value)
.iter()
.any(|e| e.contains("does not point at an object of the expected kind")),
);
}
#[test]
fn every_schema_bearing_keyword_holds_schemas() {
for keyword in ["additionalItems", "propertyNames", "contains", "not", "if"] {
let mut value = wired_from_components();
value["components"]["schemas"] = json!({ "s": { keyword: { "type": "string" } } });
value["components"]["operations"]["receiveSignups"]["channel"] =
json!({ "$ref": format!("#/components/schemas/s/{keyword}") });
value["components"]["operations"]["receiveSignups"]["messages"] = json!([]);
let errors = errors_for(value);
assert!(
errors
.iter()
.any(|e| e.contains("does not point at an object of the expected kind")),
"{keyword} got: {errors:?}"
);
}
let mut value = wired_from_components();
value["components"]["schemas"] =
json!({ "s": { "dependencies": { "d": { "type": "object" } } } });
value["components"]["operations"]["receiveSignups"]["channel"] =
json!({ "$ref": "#/components/schemas/s/dependencies/d" });
value["components"]["operations"]["receiveSignups"]["messages"] = json!([]);
assert!(
errors_for(value)
.iter()
.any(|e| e.contains("does not point at an object of the expected kind")),
);
}
#[test]
fn a_scheme_and_host_are_compared_without_regard_to_case() {
let mut value = wired_from_components();
value["components"]["operations"]["receiveSignups"]["channel"] =
json!({ "$ref": "HTTP://EXAMPLE.COM/a.yaml#/c" });
value["components"]["operations"]["receiveSignups"]["messages"] =
json!([ { "$ref": "http://example.com/a.yaml#/c/messages/m" } ]);
assert_eq!(errors_for(value), Vec::<String>::new());
let mut value = wired_from_components();
value["components"]["operations"]["receiveSignups"]["channel"] =
json!({ "$ref": "http://example.com/A.yaml#/c" });
value["components"]["operations"]["receiveSignups"]["messages"] =
json!([ { "$ref": "http://example.com/a.yaml#/c/messages/m" } ]);
assert!(
errors_for(value)
.iter()
.any(|e| e.contains("must point at a message of `http://example.com/A.yaml#/c`")),
);
}
#[test]
fn bindings_are_judged_by_the_object_that_declares_them() {
let mut value = wired();
value["channels"]["userSignedUp"]["bindings"] =
json!({ "$ref": "#/components/messageBindings/mb" });
value["components"] = json!({ "messageBindings": { "mb": { "kafka": {} } } });
assert!(
errors_for(value).iter().any(|e| e
== "#.channels.userSignedUp.bindings.$ref: `#/components/messageBindings/mb` does not point at an object of the expected kind"),
);
let mut value = wired();
value["channels"]["userSignedUp"]["bindings"] =
json!({ "$ref": "#/components/channelBindings/cb" });
value["components"] = json!({ "channelBindings": { "cb": { "kafka": {} } } });
assert_eq!(errors_for(value), Vec::<String>::new());
}
#[test]
fn external_references_are_skipped_unless_strictness_is_requested() {
let value = json!({
"asyncapi": "3.1.0",
"info": { "title": "T", "version": "1" },
"channels": { "user": { "$ref": "./other.yaml#/channels/user" } },
"operations": {
"send": { "action": "send", "channel": { "$ref": "#/channels/user" } }
}
});
assert_eq!(errors_for(value.clone()), Vec::<String>::new());
let doc: Document = serde_json::from_value(value).unwrap();
let err = doc
.validate(EnumSet::only(ValidationOptions::ErrorOnExternalReference))
.unwrap_err();
assert!(
err.errors.iter().any(|e| e.contains("external reference")),
"got: {err}"
);
}
#[test]
fn a_channel_that_is_itself_a_ref_stops_deeper_checks() {
let value = json!({
"asyncapi": "3.1.0",
"info": { "title": "T", "version": "1" },
"channels": { "user": { "$ref": "./channels.yaml#/user" } },
"operations": {
"send": {
"action": "send",
"channel": { "$ref": "#/channels/user" },
"messages": [ { "$ref": "./channels.yaml#/user/messages/anything" } ]
}
}
});
assert_eq!(errors_for(value), Vec::<String>::new());
let value = json!({
"asyncapi": "3.1.0",
"info": { "title": "T", "version": "1" },
"channels": { "user": { "$ref": "./channels.yaml#/user" } },
"operations": {
"send": {
"action": "send",
"channel": { "$ref": "#/channels/user" },
"messages": [ { "$ref": "#/channels/user/messages/anything" } ]
}
}
});
assert!(
errors_for(value).iter().any(|e| e
== "#.operations.send.messages[0].$ref: message `#/channels/user/messages/anything` must point at a message of `channels.yaml#/user`"),
);
}
#[test]
fn reply_wiring_is_checked_like_the_operation_itself() {
let mut value = wired();
value["operations"]["receiveSignups"]["reply"] = json!({
"channel": { "$ref": "#/channels/missing" },
"messages": [ { "$ref": "#/channels/userSignedUp/messages/signup" } ]
});
let errors = errors_for(value);
assert!(
errors.iter().any(|e| e
== "#.operations.receiveSignups.reply.channel.$ref: channel `#/channels/missing` names nothing in this document"),
"got: {errors:?}"
);
}
#[test]
fn invalid_top_level_map_keys_are_reported() {
let mut value = minimal();
value["channels"] = json!({ "bad key": { "address": "a" } });
value["servers"] = json!({ "also bad": { "host": "h", "protocol": "p" } });
value["operations"] = json!({ "worse key": { "action": "send", "channel": { "$ref": "#/channels/bad key" } } });
let errors = errors_for(value);
assert!(errors.iter().any(|e| e.contains("#.channels.bad key")));
assert!(errors.iter().any(|e| e.contains("#.servers.also bad")));
assert!(errors.iter().any(|e| e.contains("#.operations.worse key")));
}
#[test]
fn nested_object_errors_still_surface_from_the_root() {
let value = json!({
"asyncapi": "3.1.0",
"info": { "title": "", "version": "" },
"components": { "servers": { "s": { "host": "", "protocol": "" } } }
});
let errors = errors_for(value);
assert!(
errors
.iter()
.any(|e| e == "#.info.title: must not be empty")
);
assert!(
errors
.iter()
.any(|e| e == "#.components.servers.s.host: must not be empty")
);
}
#[test]
fn full_document_round_trips_through_json() {
let doc: Document = serde_json::from_value(wired()).unwrap();
let json = serde_json::to_string(&doc).unwrap();
let reparsed: Document = serde_json::from_str(&json).unwrap();
assert_eq!(reparsed, doc);
}
}