Skip to content

Commit

Permalink
Merge pull request #1333 from jmartisk/main-issue-1329
Browse files Browse the repository at this point in the history
Transform Bean Validation annotations into directives
  • Loading branch information
phillip-kruger authored Apr 1, 2022
2 parents effdfb5 + f58eda6 commit 8cd464a
Show file tree
Hide file tree
Showing 16 changed files with 422 additions and 21 deletions.
8 changes: 8 additions & 0 deletions common/schema-builder/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,14 @@
<groupId>${project.groupId}</groupId>
<artifactId>smallrye-graphql-schema-model</artifactId>
</dependency>

<!-- Bean validation annotations are scanned via jandex so we don't need the validation API to run,
but there is a test that actually declares those annotations -->
<dependency>
<groupId>jakarta.validation</groupId>
<artifactId>jakarta.validation-api</artifactId>
<scope>test</scope>
</dependency>

<!-- Jandex indexer -->
<dependency>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
import io.smallrye.graphql.schema.creator.type.InputTypeCreator;
import io.smallrye.graphql.schema.creator.type.InterfaceCreator;
import io.smallrye.graphql.schema.creator.type.TypeCreator;
import io.smallrye.graphql.schema.helper.BeanValidationDirectivesHelper;
import io.smallrye.graphql.schema.helper.Directives;
import io.smallrye.graphql.schema.helper.GroupHelper;
import io.smallrye.graphql.schema.helper.TypeAutoNameStrategy;
Expand Down Expand Up @@ -136,10 +137,13 @@ private Schema generateSchema() {
}

private void addDirectiveTypes(Schema schema) {
// custom directives from annotations
for (AnnotationInstance annotationInstance : ScanningContext.getIndex().getAnnotations(DIRECTIVE)) {
ClassInfo classInfo = annotationInstance.target().asClass();
schema.addDirectiveType(directiveTypeCreator.create(classInfo));
}
// bean validation directives
schema.addDirectiveType(BeanValidationDirectivesHelper.CONSTRAINT_DIRECTIVE_TYPE);
}

private void setupDirectives(Directives directives) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,21 @@
package io.smallrye.graphql.schema.creator;

import java.util.List;
import java.util.Optional;

import org.jboss.jandex.DotName;
import org.jboss.jandex.MethodInfo;
import org.jboss.jandex.Type;
import org.jboss.logging.Logger;

import io.smallrye.graphql.schema.Annotations;
import io.smallrye.graphql.schema.SchemaBuilderException;
import io.smallrye.graphql.schema.helper.BeanValidationDirectivesHelper;
import io.smallrye.graphql.schema.helper.Direction;
import io.smallrye.graphql.schema.helper.IgnoreHelper;
import io.smallrye.graphql.schema.helper.MethodHelper;
import io.smallrye.graphql.schema.model.Argument;
import io.smallrye.graphql.schema.model.DirectiveInstance;
import io.smallrye.graphql.schema.model.Operation;
import io.smallrye.graphql.schema.model.Reference;
import io.smallrye.graphql.schema.model.ReferenceType;
Expand All @@ -23,8 +27,13 @@
*/
public class ArgumentCreator extends ModelCreator {

private BeanValidationDirectivesHelper validationHelper;

private Logger logger = Logger.getLogger(ArgumentCreator.class.getName());

public ArgumentCreator(ReferenceCreator referenceCreator) {
super(referenceCreator);
validationHelper = new BeanValidationDirectivesHelper();
}

/**
Expand Down Expand Up @@ -76,6 +85,16 @@ public Optional<Argument> createArgument(Operation operation, MethodInfo methodI
argument.setSourceArgument(true);
}

if (validationHelper != null) {
List<DirectiveInstance> constraintDirectives = validationHelper
.transformBeanValidationConstraintsToDirectives(annotationsForThisArgument);
if (!constraintDirectives.isEmpty()) {
logger.debug("Adding constraint directives " + constraintDirectives + " to argument '" + argument.getName()
+ "' of method '" + argument.getMethodName() + "'");
argument.addDirectiveInstances(constraintDirectives);
}
}

populateField(Direction.IN, argument, argumentType, annotationsForThisArgument);

return Optional.of(argument);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,18 +1,22 @@
package io.smallrye.graphql.schema.creator;

import java.lang.reflect.Modifier;
import java.util.List;
import java.util.Optional;

import org.jboss.jandex.FieldInfo;
import org.jboss.jandex.MethodInfo;
import org.jboss.jandex.Type;
import org.jboss.logging.Logger;

import io.smallrye.graphql.schema.Annotations;
import io.smallrye.graphql.schema.Classes;
import io.smallrye.graphql.schema.SchemaBuilderException;
import io.smallrye.graphql.schema.helper.BeanValidationDirectivesHelper;
import io.smallrye.graphql.schema.helper.Direction;
import io.smallrye.graphql.schema.helper.IgnoreHelper;
import io.smallrye.graphql.schema.helper.MethodHelper;
import io.smallrye.graphql.schema.model.DirectiveInstance;
import io.smallrye.graphql.schema.model.Field;
import io.smallrye.graphql.schema.model.Reference;

Expand All @@ -23,8 +27,13 @@
*/
public class FieldCreator extends ModelCreator {

private Logger logger = Logger.getLogger(FieldCreator.class.getName());

private BeanValidationDirectivesHelper validationHelper;

public FieldCreator(ReferenceCreator referenceCreator) {
super(referenceCreator);
validationHelper = new BeanValidationDirectivesHelper();
}

/**
Expand Down Expand Up @@ -86,6 +95,9 @@ public Optional<Field> createFieldForPojo(Direction direction, FieldInfo fieldIn

Field field = new Field(methodInfo.name(), MethodHelper.getPropertyName(direction, methodInfo.name()), name,
reference);
if (direction == Direction.IN) {
addDirectivesForBeanValidationConstraints(annotationsForPojo, field, parentObjectReference);
}

populateField(direction, field, fieldType, methodType, annotationsForPojo);

Expand All @@ -109,7 +121,7 @@ public Optional<Field> createFieldForParameter(MethodInfo method, short position

String fieldName = fieldInfo != null ? fieldInfo.name() : null;
Field field = new Field(null, fieldName, name, reference);

addDirectivesForBeanValidationConstraints(annotationsForPojo, field, parentObjectReference);
populateField(Direction.IN, field, fieldType, method.parameters().get(position), annotationsForPojo);

return Optional.of(field);
Expand Down Expand Up @@ -141,14 +153,29 @@ public Optional<Field> createFieldForPojo(Direction direction, FieldInfo fieldIn
fieldInfo.name(),
name,
reference);

if (direction == Direction.IN) {
addDirectivesForBeanValidationConstraints(annotationsForPojo, field, parentObjectReference);
}
populateField(direction, field, fieldType, annotationsForPojo);

return Optional.of(field);
}
return Optional.empty();
}

private void addDirectivesForBeanValidationConstraints(Annotations annotationsForPojo, Field field,
Reference parentObjectReference) {
if (validationHelper != null) {
List<DirectiveInstance> constraintDirectives = validationHelper
.transformBeanValidationConstraintsToDirectives(annotationsForPojo);
if (!constraintDirectives.isEmpty()) {
logger.debug("Adding constraint directives " + constraintDirectives + " to field '" + field.getName()
+ "' of parent type '" + parentObjectReference.getName() + "'");
field.addDirectiveInstances(constraintDirectives);
}
}
}

/**
* Checks if method and/or field are use-able as a GraphQL-Field.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ private void doPopulateField(Direction direction, Field field, Type type, Annota

// Directives
if (directives != null) { // this happens while scanning for the directive types
field.setDirectiveInstances(
field.addDirectiveInstances(
directives.buildDirectiveInstances(name -> annotations.getOneOfTheseAnnotations(name).orElse(null)));
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
package io.smallrye.graphql.schema.helper;

import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;

import org.jboss.jandex.AnnotationValue;
import org.jboss.jandex.DotName;

import io.smallrye.graphql.schema.Annotations;
import io.smallrye.graphql.schema.model.DirectiveArgument;
import io.smallrye.graphql.schema.model.DirectiveInstance;
import io.smallrye.graphql.schema.model.DirectiveType;
import io.smallrye.graphql.schema.model.Reference;
import io.smallrye.graphql.schema.model.ReferenceType;

public class BeanValidationDirectivesHelper {

private static final DotName VALIDATION_ANNOTATION_EMAIL = DotName.createSimple("javax.validation.constraints.Email");
private static final DotName VALIDATION_ANNOTATION_MAX = DotName.createSimple("javax.validation.constraints.Max");
private static final DotName VALIDATION_ANNOTATION_MIN = DotName.createSimple("javax.validation.constraints.Min");
private static final DotName VALIDATION_ANNOTATION_PATTERN = DotName.createSimple("javax.validation.constraints.Pattern");
private static final DotName VALIDATION_ANNOTATION_SIZE = DotName.createSimple("javax.validation.constraints.Size");

public final static DirectiveType CONSTRAINT_DIRECTIVE_TYPE;

static {
CONSTRAINT_DIRECTIVE_TYPE = new DirectiveType();
CONSTRAINT_DIRECTIVE_TYPE.setName("constraint");
Set<String> locations = new LinkedHashSet<>();
locations.add("INPUT_FIELD_DEFINITION");
locations.add("ARGUMENT_DEFINITION");
CONSTRAINT_DIRECTIVE_TYPE.setLocations(locations);
CONSTRAINT_DIRECTIVE_TYPE.setDescription("Indicates a Bean Validation constraint");
CONSTRAINT_DIRECTIVE_TYPE.setRepeatable(true);

Reference INTEGER = new Reference();
INTEGER.setType(ReferenceType.SCALAR);
INTEGER.setClassName("java.lang.Integer");
INTEGER.setGraphQlClassName("Int");
INTEGER.setName("Int");

Reference LONG = new Reference();
LONG.setType(ReferenceType.SCALAR);
LONG.setClassName("java.lang.Long");
LONG.setGraphQlClassName("BigInteger");
LONG.setName("BigInteger");

Reference STRING = new Reference();
STRING.setType(ReferenceType.SCALAR);
STRING.setClassName("java.lang.String");
STRING.setGraphQlClassName("String");
STRING.setName("String");

addArgument("minLength", INTEGER);
addArgument("maxLength", INTEGER);
addArgument("format", STRING);
addArgument("min", LONG);
addArgument("max", LONG);
addArgument("pattern", STRING);
}

private static void addArgument(String name, Reference reference) {
DirectiveArgument arg = new DirectiveArgument();
arg.setName(name);
arg.setReference(reference);
CONSTRAINT_DIRECTIVE_TYPE.addArgumentType(arg);
}

/**
* Finds supported bean validation annotations within the `annotations` list and for each of them, generates
* a `DirectiveInstance` containing a corresponding `@constraint` GraphQL directive.
*/
public List<DirectiveInstance> transformBeanValidationConstraintsToDirectives(Annotations annotations) {
List<DirectiveInstance> result = new ArrayList<>();
Set<DotName> annotationNames = annotations.getAnnotationNames();
for (DotName annotationName : annotationNames) {
if (annotationName.equals(VALIDATION_ANNOTATION_SIZE)) {
DirectiveInstance directive = new DirectiveInstance();
directive.setType(CONSTRAINT_DIRECTIVE_TYPE);

Integer min = getIntValue(annotations, annotationName, "min");
if (min != null) {
directive.setValue("minLength", min);
}

Integer max = getIntValue(annotations, annotationName, "max");
if (max != null) {
directive.setValue("maxLength", max);
}
result.add(directive);
}
if (annotationName.equals(VALIDATION_ANNOTATION_EMAIL)) {
DirectiveInstance directive = new DirectiveInstance();
directive.setType(CONSTRAINT_DIRECTIVE_TYPE);
directive.setValue("format", "email");
result.add(directive);
}
if (annotationName.equals(VALIDATION_ANNOTATION_MAX)) {
DirectiveInstance directive = new DirectiveInstance();
directive.setType(CONSTRAINT_DIRECTIVE_TYPE);
directive.setValue("max", getLongValue(annotations, annotationName, "value"));
result.add(directive);
}
if (annotationName.equals(VALIDATION_ANNOTATION_MIN)) {
DirectiveInstance directive = new DirectiveInstance();
directive.setType(CONSTRAINT_DIRECTIVE_TYPE);
directive.setValue("min", getLongValue(annotations, annotationName, "value"));
result.add(directive);
}
if (annotationName.equals(VALIDATION_ANNOTATION_PATTERN)) {
DirectiveInstance directive = new DirectiveInstance();
directive.setType(CONSTRAINT_DIRECTIVE_TYPE);
directive.setValue("pattern", getStringValue(annotations, annotationName, "regexp"));
result.add(directive);
}
}
return result;
}

private String getStringValue(Annotations annotations, DotName annotationName, String parameterName) {
AnnotationValue aValue = annotations.getAnnotationValue(annotationName, parameterName);
return aValue != null ? aValue.asString() : null;
}

private Integer getIntValue(Annotations annotations, DotName annotationName, String parameterName) {
AnnotationValue aValue = annotations.getAnnotationValue(annotationName, parameterName);
return aValue != null ? aValue.asInt() : null;
}

private Long getLongValue(Annotations annotations, DotName annotationName, String parameterName) {
AnnotationValue aValue = annotations.getAnnotationValue(annotationName, parameterName);
return aValue != null ? aValue.asLong() : null;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -16,18 +16,31 @@
import io.smallrye.graphql.schema.model.DirectiveType;

public class Directives {

// Directives generated from application annotations that have a `@Directive` on them.
// These directive types are expected to have the `className` field defined
private final Map<DotName, DirectiveType> directiveTypes;

// Other directive types - for example, directives from bean validation constraints.
private final List<DirectiveType> directiveTypesOther;

public Directives(List<DirectiveType> directiveTypes) {
// not with streams/collector, so duplicate keys are allowed and overwritten
this.directiveTypes = new HashMap<>();
this.directiveTypesOther = new ArrayList<>();
for (DirectiveType directiveType : directiveTypes) {
this.directiveTypes.put(DotName.createSimple(directiveType.getClassName()), directiveType);
if (directiveType.getClassName() != null) {
this.directiveTypes.put(DotName.createSimple(directiveType.getClassName()), directiveType);
} else {
this.directiveTypesOther.add(directiveType);
}
}
}

public List<DirectiveInstance> buildDirectiveInstances(Function<DotName, AnnotationInstance> getAnnotation) {
List<DirectiveInstance> result = null;
// only build directive instances from `@Directive` annotations here (that means the `directiveTypes` map),
// because `directiveTypesOther` directives get their instances added on-the-go by classes that extend `ModelCreator`
for (DotName directiveTypeName : directiveTypes.keySet()) {
AnnotationInstance annotationInstance = getAnnotation.apply(directiveTypeName);
if (annotationInstance == null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@ public void testSchemaWithDirectives() throws IOException {
assertNotNull(someDirective);
assertEquals("someDirective", someDirective.getName());
assertEquals(SomeDirective.class.getName(), someDirective.getClassName());
assertEquals(singleton("value"), someDirective.getArgumentNames());
assertEquals(singleton("value"), someDirective.argumentNames());
assertEquals(new HashSet<>(asList("INTERFACE", "FIELD", "OBJECT")), someDirective.getLocations());

// check directive instances on type
Expand Down
Loading

0 comments on commit 8cd464a

Please sign in to comment.