use crate::common::bindings::{
ChannelBindings, MessageBindings, OperationBindings, ServerBindings,
};
use crate::common::reference::RefOr;
use crate::v2_6::channel_item::ChannelItem;
use crate::v2_6::correlation_id::CorrelationId;
use crate::v2_6::message::{Message, MessageTrait};
use crate::v2_6::operation::OperationTrait;
use crate::v2_6::parameter::Parameter;
use crate::v2_6::schema::SubSchema;
use crate::v2_6::security_scheme::SecurityScheme;
use crate::v2_6::server::{Server, ServerVariable};
use crate::validation::{Context, ValidateWithContext};
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
macro_rules! component_maps {
($( $field:ident : $ty:ty => $name:literal ),+ $(,)?) => {
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Default)]
pub struct Components {
$(
#[serde(rename = $name, default, skip_serializing_if = "BTreeMap::is_empty")]
pub $field: BTreeMap<String, $ty>,
)+
#[serde(flatten)]
#[serde(with = "crate::common::extensions")]
#[serde(skip_serializing_if = "Option::is_none")]
pub extensions: Option<BTreeMap<String, serde_json::Value>>,
}
impl ValidateWithContext for Components {
fn validate_with_context(&self, ctx: &mut Context) {
$(
ctx.validate_map_keys($name, &self.$field);
for (key, value) in &self.$field {
ctx.in_key($name, key, |ctx| value.validate_with_context(ctx));
}
)+
}
}
};
}
component_maps! {
schemas: SubSchema => "schemas",
servers: RefOr<Server> => "servers",
channels: ChannelItem => "channels",
server_variables: RefOr<ServerVariable> => "serverVariables",
messages: RefOr<Message> => "messages",
security_schemes: RefOr<SecurityScheme> => "securitySchemes",
parameters: RefOr<Parameter> => "parameters",
correlation_ids: RefOr<CorrelationId> => "correlationIds",
operation_traits: RefOr<OperationTrait> => "operationTraits",
message_traits: RefOr<MessageTrait> => "messageTraits",
server_bindings: RefOr<ServerBindings> => "serverBindings",
channel_bindings: RefOr<ChannelBindings> => "channelBindings",
operation_bindings: RefOr<OperationBindings> => "operationBindings",
message_bindings: RefOr<MessageBindings> => "messageBindings",
}
#[cfg(test)]
mod tests {
use super::*;
use enumset::EnumSet;
use serde_json::json;
#[test]
fn round_trips_every_component_map() {
let value = json!({
"schemas": { "user": { "type": "object" } },
"servers": { "prod": { "url": "amqp://e", "protocol": "amqp" } },
"channels": { "user": { "description": "d" } },
"serverVariables": { "port": { "default": "5672" } },
"messages": { "signup": { "name": "Signup" } },
"securitySchemes": { "sasl": { "type": "scramSha256" } },
"parameters": { "userId": { "description": "id" } },
"correlationIds": { "trace": { "location": "$message.header#/id" } },
"operationTraits": { "common": { "summary": "s" } },
"messageTraits": { "common": { "contentType": "application/json" } },
"serverBindings": { "kafka": { "kafka": {} } },
"channelBindings": { "kafka": { "kafka": {} } },
"operationBindings": { "kafka": { "kafka": {} } },
"messageBindings": { "kafka": { "kafka": {} } },
"x-owner": "team"
});
let components: Components = serde_json::from_value(value.clone()).unwrap();
assert_eq!(components.schemas.len(), 1);
assert_eq!(components.message_bindings.len(), 1);
assert_eq!(serde_json::to_value(&components).unwrap(), value);
let mut ctx = Context::with_path(EnumSet::empty(), "#.components");
components.validate_with_context(&mut ctx);
assert!(ctx.errors.is_empty(), "got: {:?}", ctx.errors);
}
#[test]
fn v3_only_maps_are_not_component_fields_here() {
let components: Components = serde_json::from_value(json!({
"operations": { "o": {} },
"replies": { "r": {} },
"tags": { "t": { "name": "x" } }
}))
.unwrap();
assert_eq!(serde_json::to_value(&components).unwrap(), json!({}));
}
#[test]
fn a_component_channel_keeps_its_ref_siblings() {
let value = json!({
"channels": {
"user": { "$ref": "#/channels/other", "description": "d", "deprecated": true }
}
});
let components: Components = serde_json::from_value(value.clone()).unwrap();
let channel = &components.channels["user"];
assert_eq!(channel.reference.as_deref(), Some("#/channels/other"));
assert_eq!(channel.description.as_deref(), Some("d"));
assert!(channel.is_reference());
assert_eq!(serde_json::to_value(&components).unwrap(), value);
}
#[test]
fn a_schema_keeps_its_ref_siblings() {
let value = json!({
"schemas": {
"user": { "$ref": "#/components/schemas/base", "description": "d", "x-note": 1 }
}
});
let components: Components = serde_json::from_value(value.clone()).unwrap();
assert_eq!(serde_json::to_value(&components).unwrap(), value);
}
#[test]
fn schemas_accept_the_boolean_form() {
let value = json!({ "schemas": { "Never": false, "Any": true } });
let components: Components = serde_json::from_value(value.clone()).unwrap();
assert_eq!(components.schemas.len(), 2);
assert_eq!(serde_json::to_value(&components).unwrap(), value);
}
#[test]
fn only_the_maps_the_schema_allows_take_a_reference() {
let referenced = json!({
"messages": { "a": { "$ref": "#/components/messages/b" } },
"securitySchemes": { "s": { "$ref": "#/x" } },
"correlationIds": { "c": { "$ref": "#/x" } }
});
let components: Components = serde_json::from_value(referenced.clone()).unwrap();
assert_eq!(serde_json::to_value(&components).unwrap(), referenced);
let delegated = json!({
"servers": { "s": { "$ref": "#/x" } },
"serverVariables": { "v": { "$ref": "#/x" } },
"parameters": { "p": { "$ref": "#/x" } }
});
let components: Components = serde_json::from_value(delegated.clone()).unwrap();
assert_eq!(serde_json::to_value(&components).unwrap(), delegated);
let traits = json!({
"operationTraits": { "t": { "$ref": "#/x" } },
"messageBindings": { "b": { "$ref": "#/x" } }
});
let components: Components = serde_json::from_value(traits.clone()).unwrap();
assert_eq!(serde_json::to_value(&components).unwrap(), traits);
assert!(components.operation_traits["t"].reference().is_some());
}
#[test]
fn empty_components_serialize_to_an_empty_object() {
let components = Components::default();
assert_eq!(serde_json::to_value(&components).unwrap(), json!({}));
let mut ctx = Context::with_path(EnumSet::empty(), "#.components");
components.validate_with_context(&mut ctx);
assert!(ctx.errors.is_empty());
}
#[test]
fn invalid_keys_and_nested_errors_are_reported() {
let components: Components = serde_json::from_value(json!({
"messages": { "bad key": {} },
"servers": { "prod": { "url": "", "protocol": "" } },
"correlationIds": { "trace": { "location": "nope" } }
}))
.unwrap();
let mut ctx = Context::with_path(EnumSet::empty(), "#.components");
components.validate_with_context(&mut ctx);
let msgs: Vec<_> = ctx.errors.iter().map(ToString::to_string).collect();
assert!(
msgs.iter()
.any(|e| e.contains("#.components.messages.bad key"))
);
assert!(
msgs.iter()
.any(|e| e == "#.components.servers.prod.url: must not be empty")
);
assert!(
msgs.iter()
.any(|e| e.starts_with("#.components.correlationIds.trace.location"))
);
}
}