Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

W-14990427: add custom binding validations #1955

Merged
merged 4 commits into from
Apr 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ import amf.core.client.scala.model.{BoolField, IntField, StrField}
import amf.core.internal.metamodel.Field
import amf.core.internal.parser.domain.{Annotations, Fields}
import amf.shapes.client.scala.model.domain.Key
import amf.core.client.scala.model.domain._

class IBMMQChannelBinding(override val fields: Fields, override val annotations: Annotations)
extends ChannelBinding
Expand Down Expand Up @@ -64,7 +63,7 @@ class IBMMQChannelQueue(override val fields: Fields, override val annotations: A
def isPartitioned: BoolField = fields.field(IBMMQChannelQueueModel.IsPartitioned)
def exclusive: BoolField = fields.field(IBMMQChannelQueueModel.Exclusive)

def withObjectName(objectName: String): this.type = set(IBMMQChannelQueueModel.ObjectName, objectName)
def withObjectName(objectName: String): this.type = set(IBMMQChannelQueueModel.ObjectName, objectName)
def withIsPartitioned(isPartitioned: Boolean): this.type = set(IBMMQChannelQueueModel.IsPartitioned, isPartitioned)
def withExclusive(exclusive: Boolean): this.type = set(IBMMQChannelQueueModel.Exclusive, exclusive)

Expand All @@ -87,7 +86,7 @@ class IBMMQChannelTopic(override val fields: Fields, override val annotations: A

override def nameField: Field = IBMMQChannelTopicModel.Name

def string: StrField = fields.field(IBMMQChannelTopicModel.String)
def string: StrField = fields.field(IBMMQChannelTopicModel.String)
def objectName: StrField = fields.field(IBMMQChannelTopicModel.ObjectName)
def durablePermitted: BoolField = fields.field(IBMMQChannelTopicModel.DurablePermitted)
def lastMsgRetained: BoolField = fields.field(IBMMQChannelTopicModel.LastMsgRetained)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,10 @@ object IBMMQChannelBindingParser extends BindingParser[IBMMQChannelBinding] {
val queue = IBMMQChannelQueue(Annotations(entry.value))
val queueMap = entry.value.as[YMap]

queueMap.key("objectName", IBMMQChannelQueueModel.ObjectName in queue)
queueMap.key("objectName") match {
case Some(value) => Some(value).foreach(IBMMQChannelQueueModel.ObjectName in queue)
case None => missingRequiredFieldViolation(ctx, binding, "objectName", "IBMMQ Channel Queue")
}

queueMap.key("isPartitioned") match {
case Some(value) => Some(value).foreach(IBMMQChannelQueueModel.IsPartitioned in queue)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,13 @@ object IBMMQMessageBindingParser extends BindingParser[IBMMQMessageBinding] {

map.key("headers").foreach { entry =>
val values = entry.value.toString.split(",").map(AmfScalar(_)).toSeq
binding.setWithoutId(
IBMMQMessageBindingModel.Headers,
AmfArray(values, Annotations.virtual()),
Annotations(entry)
)
if (values.nonEmpty) {
binding.setWithoutId(
IBMMQMessageBindingModel.Headers,
AmfArray(values, Annotations.virtual()),
Annotations(entry)
)
}
}

map.key("description", IBMMQMessageBindingModel.Description in binding)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ object SolaceOperationBindingParser extends BindingParser[SolaceOperationBinding
val map = entry.value.as[YMap]

map.key("destinations").foreach { entry =>
val destinations = entry.value.as[Seq[YMap]].map(destinationMap => parseDestination(binding, destinationMap))
val destinations = entry.value.as[Seq[YMap]].map(parseDestination)
binding.setArrayWithoutId(SolaceOperationBindingModel.Destinations, destinations)
}

Expand All @@ -36,13 +36,14 @@ object SolaceOperationBindingParser extends BindingParser[SolaceOperationBinding
binding
}

private def parseDestination(binding: SolaceOperationBinding, map: YMap)(implicit
private def parseDestination(map: YMap)(implicit
ctx: AsyncWebApiContext
): SolaceOperationDestination = {
val destination = SolaceOperationDestination(Annotations())

map.key("destinationType", SolaceOperationDestinationModel.DestinationType in destination)

map.key("deliveryMode") match { // todo validate that it can only be 'direct' or 'persistent'
map.key("deliveryMode") match {
case Some(value) => Some(value).foreach(SolaceOperationDestinationModel.DeliveryMode in destination)
case None => setDefaultValue(destination, SolaceOperationDestinationModel.DeliveryMode, AmfScalar("persistent"))
}
Expand All @@ -65,7 +66,9 @@ object SolaceOperationBindingParser extends BindingParser[SolaceOperationBinding
queueMap.key("topicSubscriptions", SolaceOperationQueueModel.TopicSubscriptions in queue)

queueMap.key("accessType", SolaceOperationQueueModel.AccessType in queue)

queueMap.key("maxMsgSpoolSize", SolaceOperationQueueModel.MaxMsgSpoolSize in queue)

queueMap.key("maxTtl", SolaceOperationQueueModel.MaxTtl in queue)

ctx.closedShape(queue, queueMap, "SolaceOperationQueue")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1088,6 +1088,106 @@ object APIRawValidations extends CommonValidationDefinitions {
constraint = sh("pattern"),
value =
"^(Api\\sKey|OAuth\\s2.0|http|httpApiKey|openIdConnect|userPassword|X509|symmetricEncryption|asymmetricEncryption|plain|scramSha256|scramSha512|gssapi|x-.+)$"
),
AMFValidation(
message = "Invalid 'destinationType' value. The options are: 'queue' or 'topic'.",
owlClass = apiBinding("SolaceOperationDestination"),
owlProperty = apiBinding("destinationType"),
constraint = sh("in"),
value = "queue,topic"
),
AMFValidation(
message = "Invalid 'deliveryMode' value. The options are: 'direct' or 'persistent'.",
owlClass = apiBinding("SolaceOperationDestination"),
owlProperty = apiBinding("deliveryMode"),
constraint = sh("in"),
value = "direct,persistent"
),
AMFValidation(
message = "Invalid 'accessType' value. The options are: 'exclusive' or 'nonexclusive'.",
owlClass = apiBinding("SolaceOperationQueue"),
owlProperty = apiBinding("accessType"),
constraint = sh("in"),
value = "exclusive,nonexclusive"
),
AMFValidation(
message = "Invalid 'destinationType' value. The options are: 'exchange', 'queue' or 'fifo-queue'.",
owlClass = apiBinding("AnypointMQChannelBinding"),
owlProperty = apiBinding("destinationType"),
constraint = sh("in"),
value = "exchange,queue,fifo-queue"
),
AMFValidation(
owlClass = apiBinding("AnypointMQMessageBinding"),
owlProperty = apiBinding("headers"),
constraint = shape("anypointMQHeadersValidation")
),
AMFValidation(
message = "IBMMQ Server Binding 'heartBeatInterval' field must be a number between 0-999999",
owlClass = apiBinding("IBMMQServerBinding"),
owlProperty = apiBinding("heartBeatInterval"),
constraint = sh("pattern"),
value = "^[0-999999]$"
),
AMFValidation(
message = "IBMMQ Channel Binding 'destinationType' field must be either 'topic' or 'queue'",
owlClass = apiBinding("IBMMQChannelBinding"),
owlProperty = apiBinding("destinationType"),
constraint = sh("in"),
value = "topic,queue"
),
AMFValidation(
message = "IBMMQ queue 'objectName' field MUST NOT exceed 48 characters in length",
owlClass = apiBinding("IBMMQChannelQueue"),
owlProperty = apiBinding("objectName"),
constraint = sh("maxLength"),
value = "48"
),
AMFValidation(
message = "'queue' and 'topic' fields MUST NOT coexist within an IBMMQ channel binding",
owlClass = apiBinding("IBMMQChannelBinding"),
owlProperty = apiBinding("queue"),
constraint = shape("IBMMQDestinationValidation")
),
AMFValidation(
message = "IBMMQ topic 'string' field MUST NOT exceed 10240 characters in length",
owlClass = apiBinding("IBMMQChannelTopic"),
owlProperty = apiBinding("string"),
constraint = sh("maxLength"),
value = "10240"
),
AMFValidation(
message = "IBMMQ topic 'objectName' field MUST NOT exceed 48 characters in length",
owlClass = apiBinding("IBMMQChannelTopic"),
owlProperty = apiBinding("objectName"),
constraint = sh("maxLength"),
value = "48"
),
AMFValidation(
message = "IBMMQ channel Binding 'maxMsgLength' field must be a number between 0-104857600 (100MB)",
owlClass = apiBinding("IBMMQChannelBinding"),
owlProperty = apiBinding("maxMsgLength"),
constraint = sh("pattern"),
value = "^[0-104857600]$"
),
AMFValidation(
message = "IBMMQ message Binding 'type' field must be either 'string', 'jms' or 'binary'",
owlClass = apiBinding("IBMMQMessageBinding"),
owlProperty = apiBinding("messageType"),
constraint = sh("in"),
value = "string,jms,binary"
),
AMFValidation(
message = "IBMMQ message Binding 'expiry' field must be 0 or greater",
owlClass = apiBinding("IBMMQMessageBinding"),
owlProperty = apiBinding("expiry"),
constraint = sh("minInclusive")
),
AMFValidation(
message = "IBMMQ message Binding 'headers' MUST NOT be specified if 'type' field is 'string' or 'jms'",
owlClass = apiBinding("IBMMQMessageBinding"),
owlProperty = apiBinding("headers"),
constraint = shape("IBMMQHeadersValidation")
)
) ++ baseApiValidations("AsyncAPI")

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,19 @@ package amf.apicontract.internal.validation.shacl

import amf.apicontract.client.scala.model.domain.{EndPoint, Request}
import amf.apicontract.client.scala.model.domain.api.{Api, WebApi}
import amf.apicontract.client.scala.model.domain.bindings.anypointmq.AnypointMQMessageBinding
import amf.apicontract.client.scala.model.domain.bindings.ibmmq.{IBMMQChannelBinding, IBMMQMessageBinding}
import amf.apicontract.client.scala.model.domain.security.{OAuth2Settings, OpenIdConnectSettings, SecurityScheme}
import amf.apicontract.internal.metamodel.domain._
import amf.apicontract.internal.metamodel.domain.api.BaseApiModel
import amf.apicontract.internal.metamodel.domain.bindings.{BindingHeaders, BindingQuery, HttpMessageBindingModel}
import amf.apicontract.internal.metamodel.domain.bindings.{
AnypointMQMessageBindingModel,
BindingHeaders,
BindingQuery,
HttpMessageBindingModel,
IBMMQChannelBindingModel,
IBMMQMessageBindingModel
}
import amf.apicontract.internal.metamodel.domain.security.{
OAuth2SettingsModel,
OpenIdConnectSettingsModel,
Expand Down Expand Up @@ -741,6 +750,68 @@ object APICustomShaclFunctions extends BaseCustomShaclFunctions {
case _ => // ignore
}
}
},
new CustomShaclFunction {
override val name: String = "anypointMQHeadersValidation"

override def run(element: AmfObject, validate: Option[ValidationInfo] => Unit): Unit = {

element.asInstanceOf[AnypointMQMessageBinding].headers match {
case node: NodeShape =>
node.fields.?[AmfArray](NodeShapeModel.Properties) match {
case Some(_) => // ignore
case None =>
validate(
validationInfo(
AnypointMQMessageBindingModel.Headers,
"AnypointMQ Message Binding 'headers' field must have a 'properties' field",
element.annotations
)
)
}

case elem =>
validate(
validationInfo(
AnypointMQMessageBindingModel.Headers,
"AnypointMQ Message Binding 'headers' field must be an object",
elem.annotations
)
)
}
}
},
new CustomShaclFunction {
override val name: String = "IBMMQDestinationValidation"

override def run(element: AmfObject, validate: Option[ValidationInfo] => Unit): Unit = {
val binding = element.asInstanceOf[IBMMQChannelBinding]
if (binding.topic != null && binding.queue != null) {
validate(
validationInfo(
IBMMQChannelBindingModel.Queue,
"'queue' and 'topic' fields MUST NOT coexist within an IBMMQ channel binding",
element.annotations
)
)
}
}
},
new CustomShaclFunction {
override val name: String = "IBMMQHeadersValidation"

override def run(element: AmfObject, validate: Option[ValidationInfo] => Unit): Unit = {
val binding = element.asInstanceOf[IBMMQMessageBinding]
if (Seq("string", "jms").contains(binding.messageType.value()) && binding.headers.nonEmpty) {
validate(
validationInfo(
IBMMQMessageBindingModel.Headers,
"headers MUST NOT be specified if type = string or jms in an IBMMQ Message Binding",
element.annotations
)
)
}
}
}
)

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
asyncapi: 2.2.0
info:
title: test anypoint binding
version: 1.0.0
channels:
some-channel:
bindings:
anypointmq:
destination: user-signup-exchg
destinationType: wrong destinationType # MUST be either exchange or queue or fifo-queue
bindingVersion: '0.1.0'
publish:
message:
bindings:
anypointmq:
headers:
type: string # MUST be an object
bindingVersion: '0.1.0'
payload:
type: string

other-channel:
publish:
message:
bindings:
anypointmq:
headers:
type: object # MUST have a 'properties' field
bindingVersion: '0.1.0'
payload:
type: string
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
asyncapi: 2.1.0
info:
title: test binding
version: 1.0.0
channels:
some-channel:
bindings:
ibmmq:
destinationType: queue
# 10. objectName is REQUIRED inside queue
queue: { }
Loading