diff --git a/amf-api-contract/shared/src/main/scala/amf/apicontract/internal/validation/model/AMFRawValidations.scala b/amf-api-contract/shared/src/main/scala/amf/apicontract/internal/validation/model/AMFRawValidations.scala index 09578d3792..5066f59a02 100644 --- a/amf-api-contract/shared/src/main/scala/amf/apicontract/internal/validation/model/AMFRawValidations.scala +++ b/amf-api-contract/shared/src/main/scala/amf/apicontract/internal/validation/model/AMFRawValidations.scala @@ -1589,6 +1589,27 @@ object AMFRawValidations { owlClass = shape("ScalarShape"), owlProperty = shape("values"), constraint = shape("duplicatedEnumValues") + ), + AMFValidation( + uri = amfParser("empty-enum"), + owlClass = shape("ScalarShape"), + owlProperty = shape("values"), + constraint = shape("emptyEnum"), + message = "Enum definitions must have at least one value" + ), + AMFValidation( + uri = amfParser("empty-union"), + owlClass = shape("UnionShape"), + owlProperty = shape("AnyOf"), + constraint = shape("emptyUnion"), + message = "Union definitions must have at least one value" + ), + AMFValidation( + uri = amfParser("empty-definition"), + owlClass = sh("NodeShape"), + owlProperty = sh("PropertyShape"), + constraint = shape("emptyDefinition"), + message = "Types definition must have at least one field" ) ) override def validations(): Seq[AMFValidation] = result diff --git a/amf-api-contract/shared/src/main/scala/amf/apicontract/internal/validation/shacl/CustomShaclFunctions.scala b/amf-api-contract/shared/src/main/scala/amf/apicontract/internal/validation/shacl/CustomShaclFunctions.scala index af882d000e..2f12d5d453 100644 --- a/amf-api-contract/shared/src/main/scala/amf/apicontract/internal/validation/shacl/CustomShaclFunctions.scala +++ b/amf-api-contract/shared/src/main/scala/amf/apicontract/internal/validation/shacl/CustomShaclFunctions.scala @@ -14,6 +14,7 @@ import amf.core.client.scala.model.domain._ import amf.core.client.scala.model.domain.extensions.{CustomDomainProperty, DomainExtension, PropertyShape} import amf.core.internal.annotations.SynthesizedField import amf.core.internal.metamodel.Field +import amf.core.internal.metamodel.domain.ShapeModel import amf.core.internal.metamodel.domain.common.NameFieldSchema import amf.core.internal.metamodel.domain.extensions.{CustomDomainPropertyModel, PropertyShapeModel} import amf.core.internal.utils.RegexConverter @@ -24,16 +25,60 @@ import amf.validation.internal.shacl.custom.CustomShaclValidator.{ CustomShaclFunctions, ValidationInfo } + import java.util.regex.Pattern object CustomShaclFunctions { val listOfFunctions: List[CustomShaclFunction] = List( + new CustomShaclFunction { + override val name: String = "emptyDefinition" + override def run(element: AmfObject, validate: Option[ValidationInfo] => Unit): Unit = { + val node = element.asInstanceOf[NodeShape] + val isInterface = node.isAbstract.value() + val isInput = node.isInputOnly.value() + val isExtensionWrapper = node.and.nonEmpty + val isSchema = node.name.isNullOrEmpty + if (node.properties.isEmpty && node.operations.isEmpty) { + if (!isExtensionWrapper && !isSchema) { + val nodeType = + if (isInterface) "Interface" + else if (isInput) "Input Type" + else "Type" + validate( + Some( + ValidationInfo( + NodeShapeModel.Properties, + Some(s"$nodeType definitions must have at least one field"), + Some(element.annotations) + ) + ) + ) + } + } + } + }, + new CustomShaclFunction { + override val name: String = "emptyUnion" + override def run(element: AmfObject, validate: Option[ValidationInfo] => Unit): Unit = { + val union = element.asInstanceOf[UnionShape] + val isWrapper = union.name.isNullOrEmpty || union.or.nonEmpty + if (!isWrapper && union.anyOf.isEmpty) validate(None) + } + }, + new CustomShaclFunction { + override val name: String = "emptyEnum" + override def run(element: AmfObject, validate: Option[ValidationInfo] => Unit): Unit = { + val enum = element.asInstanceOf[ScalarShape] + if (enum.fields.exists(ShapeModel.Values) && enum.values.isEmpty) validate(None) + } + }, new CustomShaclFunction { override val name: String = "unionInvalidMembers" override def run(element: AmfObject, validate: Option[ValidationInfo] => Unit): Unit = { - val union = element.asInstanceOf[UnionShape] - if (union.name.nonNull) { + val union = element.asInstanceOf[UnionShape] + val isWrapper = union.name.isNullOrEmpty || union.or.nonEmpty + if (!isWrapper) { val members = union.anyOf val invalidMembers = members.filter { case n: NodeShape if n.isAbstract.value() => true // interfaces diff --git a/amf-cli/shared/src/test/resources/graphql/tck/apis/invalid/empty-enum.api.graphql b/amf-cli/shared/src/test/resources/graphql/tck/apis/invalid/empty-enum.api.graphql index 186abf3e4e..4ca0d12d23 100644 --- a/amf-cli/shared/src/test/resources/graphql/tck/apis/invalid/empty-enum.api.graphql +++ b/amf-cli/shared/src/test/resources/graphql/tck/apis/invalid/empty-enum.api.graphql @@ -7,4 +7,4 @@ type Query { } enum Season { -} \ No newline at end of file +} diff --git a/amf-cli/shared/src/test/resources/graphql/tck/apis/invalid/empty-enum.api.report b/amf-cli/shared/src/test/resources/graphql/tck/apis/invalid/empty-enum.api.report new file mode 100644 index 0000000000..90c409ab22 --- /dev/null +++ b/amf-cli/shared/src/test/resources/graphql/tck/apis/invalid/empty-enum.api.report @@ -0,0 +1,14 @@ +ModelId: file://amf-cli/shared/src/test/resources/graphql/tck/apis/invalid/empty-enum.api.graphql +Profile: GraphQL +Conforms: false +Number of results: 1 + +Level: Violation + +- Constraint: http://a.ml/vocabularies/amf/parser#empty-enum + Message: Enum definitions must have at least one value + Severity: Violation + Target: file://amf-cli/shared/src/test/resources/graphql/tck/apis/invalid/empty-enum.api.graphql#/declares/scalar/Season + Property: + Range: [(9,0)-(9,11)] + Location: diff --git a/amf-cli/shared/src/test/resources/graphql/tck/apis/invalid/empty-input-type.api.graphql b/amf-cli/shared/src/test/resources/graphql/tck/apis/invalid/empty-input-type.api.graphql index 63b4373924..fc3f5ca350 100644 --- a/amf-cli/shared/src/test/resources/graphql/tck/apis/invalid/empty-input-type.api.graphql +++ b/amf-cli/shared/src/test/resources/graphql/tck/apis/invalid/empty-input-type.api.graphql @@ -8,13 +8,9 @@ type Query { } type Mutation { - changeUserStatus(input_: InputType!): OutputType + changeUserStatus(input_: InputType!): String } input InputType { # this is empty } - -type OutputType { - success: Boolean -} \ No newline at end of file diff --git a/amf-cli/shared/src/test/resources/graphql/tck/apis/invalid/empty-input-type.api.report b/amf-cli/shared/src/test/resources/graphql/tck/apis/invalid/empty-input-type.api.report new file mode 100644 index 0000000000..fe917105f3 --- /dev/null +++ b/amf-cli/shared/src/test/resources/graphql/tck/apis/invalid/empty-input-type.api.report @@ -0,0 +1,14 @@ +ModelId: file://amf-cli/shared/src/test/resources/graphql/tck/apis/invalid/empty-input-type.api.graphql +Profile: GraphQL +Conforms: false +Number of results: 1 + +Level: Violation + +- Constraint: http://a.ml/vocabularies/amf/parser#empty-definition + Message: Input Type definitions must have at least one field + Severity: Violation + Target: file://amf-cli/shared/src/test/resources/graphql/tck/apis/invalid/empty-input-type.api.graphql#/declares/shape/InputType + Property: http://www.w3.org/ns/shacl#property + Range: [(14,0)-(14,15)] + Location: diff --git a/amf-cli/shared/src/test/resources/graphql/tck/apis/invalid/empty-interface.api.report b/amf-cli/shared/src/test/resources/graphql/tck/apis/invalid/empty-interface.api.report new file mode 100644 index 0000000000..8d11ce0507 --- /dev/null +++ b/amf-cli/shared/src/test/resources/graphql/tck/apis/invalid/empty-interface.api.report @@ -0,0 +1,14 @@ +ModelId: file://amf-cli/shared/src/test/resources/graphql/tck/apis/invalid/empty-interface.api.graphql +Profile: GraphQL +Conforms: false +Number of results: 1 + +Level: Violation + +- Constraint: http://a.ml/vocabularies/amf/parser#empty-definition + Message: Interface definitions must have at least one field + Severity: Violation + Target: file://amf-cli/shared/src/test/resources/graphql/tck/apis/invalid/empty-interface.api.graphql#/declares/shape/HasId + Property: http://www.w3.org/ns/shacl#property + Range: [(13,0)-(13,15)] + Location: diff --git a/amf-cli/shared/src/test/resources/graphql/tck/apis/invalid/empty-object.api.report b/amf-cli/shared/src/test/resources/graphql/tck/apis/invalid/empty-object.api.report new file mode 100644 index 0000000000..fa314d8f76 --- /dev/null +++ b/amf-cli/shared/src/test/resources/graphql/tck/apis/invalid/empty-object.api.report @@ -0,0 +1,14 @@ +ModelId: file://amf-cli/shared/src/test/resources/graphql/tck/apis/invalid/empty-object.api.graphql +Profile: GraphQL +Conforms: false +Number of results: 1 + +Level: Violation + +- Constraint: http://a.ml/vocabularies/amf/parser#empty-definition + Message: Type definitions must have at least one field + Severity: Violation + Target: file://amf-cli/shared/src/test/resources/graphql/tck/apis/invalid/empty-object.api.graphql#/declares/shape/Person + Property: http://www.w3.org/ns/shacl#property + Range: [(9,0)-(9,11)] + Location: diff --git a/amf-cli/shared/src/test/resources/graphql/tck/apis/invalid/empty-union.api.report b/amf-cli/shared/src/test/resources/graphql/tck/apis/invalid/empty-union.api.report new file mode 100644 index 0000000000..7b4c11aad9 --- /dev/null +++ b/amf-cli/shared/src/test/resources/graphql/tck/apis/invalid/empty-union.api.report @@ -0,0 +1,14 @@ +ModelId: file://amf-cli/shared/src/test/resources/graphql/tck/apis/invalid/empty-union.api.graphql +Profile: GraphQL +Conforms: false +Number of results: 1 + +Level: Violation + +- Constraint: http://a.ml/vocabularies/amf/parser#empty-union + Message: Union definitions must have at least one value + Severity: Violation + Target: file://amf-cli/shared/src/test/resources/graphql/tck/apis/invalid/empty-union.api.graphql#/declares/union/SearchResult + Property: + Range: [(19,0)-(19,20)] + Location: diff --git a/amf-graphql/shared/src/main/scala/amf/graphql/internal/spec/domain/GraphQLNestedEnumParser.scala b/amf-graphql/shared/src/main/scala/amf/graphql/internal/spec/domain/GraphQLNestedEnumParser.scala index 25b2439f6c..05c0554f3b 100644 --- a/amf-graphql/shared/src/main/scala/amf/graphql/internal/spec/domain/GraphQLNestedEnumParser.scala +++ b/amf-graphql/shared/src/main/scala/amf/graphql/internal/spec/domain/GraphQLNestedEnumParser.scala @@ -26,16 +26,18 @@ class GraphQLNestedEnumParser(enumTypeDef: Node)(implicit val ctx: GraphQLWebApi } private def parseValues(): Unit = { - path(enumTypeDef, Seq(ENUM_VALUES_DEFINITION)) map { case valuesNode: Node => - val values = collect(valuesNode, Seq(ENUM_VALUE_DEFINITION)) map { case valueDefNode: Node => - getEnumValue(valueDefNode) match { - case Some(value: ScalarNode) => - parseDirectives(valueDefNode, value) - value - case None => ScalarNode() + path(enumTypeDef, Seq(ENUM_VALUES_DEFINITION)) match { + case Some(valuesNode: Node) => + val values = collect(valuesNode, Seq(ENUM_VALUE_DEFINITION)) map { case valueDefNode: Node => + getEnumValue(valueDefNode) match { + case Some(value: ScalarNode) => + parseDirectives(valueDefNode, value) + value + case None => ScalarNode() + } } - } - enum.withValues(values) + enum.withValues(values) + case _ => enum.withValues(Seq()) } }