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

Basic Container Item Annotation Support #61

Merged
merged 4 commits into from
Mar 31, 2020
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
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,18 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]
### `jsonschema-generator`
#### Added
- Convenience methods on `FieldScope`/`MethodScope` for accessing (first level) container item annotations: `getContainerItemAnnotation()` and `getContainerItemAnnotationConsideringFieldAndGetter()`

#### Changed
- `MethodScope.getAnnotation()` now also considers annotations directly on return type (when no matching annotation was found on the method itself)

### `jsonschema-module-javax-validation`
#### Changed
- Consider (first level) container item annotations (e.g. List<@Size(min = 3) String>`)

## [4.8.1] - 2020-03-31
### All
#### Fixed
Expand Down Expand Up @@ -297,6 +309,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Indicate a number's "exclusiveMaximum" according to `@DecimalMax` or `@Negative`


[Unreleased]: https://github.com/victools/jsonschema-generator/compare/v4.8.1...HEAD
[4.8.1]: https://github.com/victools/jsonschema-generator/compare/v4.8.0...v4.8.2
[4.8.0]: https://github.com/victools/jsonschema-generator/compare/v4.7.0...v4.8.0
[4.7.0]: https://github.com/victools/jsonschema-generator/compare/v4.6.0...v4.7.0
[4.6.0]: https://github.com/victools/jsonschema-generator/compare/v4.5.0...v4.6.0
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@
import com.fasterxml.classmate.members.ResolvedField;
import com.fasterxml.classmate.members.ResolvedMethod;
import java.lang.annotation.Annotation;
import java.lang.reflect.AnnotatedParameterizedType;
import java.lang.reflect.AnnotatedType;
import java.lang.reflect.Field;
import java.util.stream.Stream;

Expand Down Expand Up @@ -67,6 +69,11 @@ public FieldScope withOverriddenName(String overriddenName) {
this.isFakeContainerItemScope(), this.getContext());
}

@Override
public FieldScope asFakeContainerItemScope() {
return (FieldScope) super.asFakeContainerItemScope();
}

/**
* Returns the name to be used to reference this field in its parent's "properties".
*
Expand Down Expand Up @@ -117,6 +124,18 @@ public boolean hasGetter() {
return this.findGetter() != null;
}

@Override
public <A extends Annotation> A getContainerItemAnnotation(Class<A> annotationClass) {
AnnotatedType annotatedType = this.getRawMember().getAnnotatedType();
if (annotatedType instanceof AnnotatedParameterizedType) {
AnnotatedType[] typeArguments = ((AnnotatedParameterizedType) annotatedType).getAnnotatedActualTypeArguments();
if (typeArguments.length > 0) {
return typeArguments[0].getAnnotation(annotationClass);
}
}
return null;
}

@Override
public <A extends Annotation> A getAnnotationConsideringFieldAndGetter(Class<A> annotationClass) {
A annotation = this.getAnnotation(annotationClass);
Expand All @@ -126,4 +145,14 @@ public <A extends Annotation> A getAnnotationConsideringFieldAndGetter(Class<A>
}
return annotation;
}

@Override
public <A extends Annotation> A getContainerItemAnnotationConsideringFieldAndGetter(Class<A> annotationClass) {
A annotation = this.getContainerItemAnnotation(annotationClass);
if (annotation == null) {
MemberScope<?, ?> associatedGetter = this.findGetter();
annotation = associatedGetter == null ? null : associatedGetter.getContainerItemAnnotation(annotationClass);
}
return annotation;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,16 @@ public <A extends Annotation> A getAnnotation(Class<A> annotationClass) {
return this.member.get(annotationClass);
}

/**
* Return the annotation of the given type on the member's container item (i.e. first type parameter if there is one), if such an annotation is
* present on either the field or its getter.
*
* @param <A> type of annotation
* @param annotationClass type of annotation
* @return annotation instance (or {@code null} if no annotation of the given type is present)
*/
public abstract <A extends Annotation> A getContainerItemAnnotation(Class<A> annotationClass);

/**
* Return the annotation of the given type on the member, if such an annotation is present on either the field or its getter.
*
Expand All @@ -272,6 +282,16 @@ public <A extends Annotation> A getAnnotation(Class<A> annotationClass) {
*/
public abstract <A extends Annotation> A getAnnotationConsideringFieldAndGetter(Class<A> annotationClass);

/**
* Return the annotation of the given type on the member's container item (i.e. first type parameter if there is one), if such an annotation is
* present on either the field or its getter.
*
* @param <A> type of annotation
* @param annotationClass type of annotation
* @return annotation instance (or {@code null} if no annotation of the given type is present)
*/
public abstract <A extends Annotation> A getContainerItemAnnotationConsideringFieldAndGetter(Class<A> annotationClass);

/**
* Returns the name to be used to reference this member in its parent's "properties".
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@
import com.fasterxml.classmate.ResolvedTypeWithMembers;
import com.fasterxml.classmate.members.ResolvedMethod;
import java.lang.annotation.Annotation;
import java.lang.reflect.AnnotatedParameterizedType;
import java.lang.reflect.AnnotatedType;
import java.lang.reflect.Method;
import java.util.List;
import java.util.stream.Collectors;
Expand Down Expand Up @@ -69,6 +71,11 @@ public MethodScope withOverriddenName(String overriddenName) {
this.isFakeContainerItemScope(), this.getContext());
}

@Override
public MethodScope asFakeContainerItemScope() {
return (MethodScope) super.asFakeContainerItemScope();
}

/**
* Indicating whether the method is declared as {@code void}, i.e. has no return value.
*
Expand Down Expand Up @@ -142,6 +149,34 @@ public boolean isGetter() {
return this.findGetterField() != null;
}

/**
* Return the annotation of the given type on the method or its return type, if such an annotation is present.
*
* @param <A> type of annotation to look-up
* @param annotationClass annotation class to look up instance on member for
* @return annotation instance (or {@code null} if no annotation of the given type is present
*/
@Override
public <A extends Annotation> A getAnnotation(Class<A> annotationClass) {
A annotation = super.getAnnotation(annotationClass);
if (annotation == null) {
annotation = this.getRawMember().getAnnotatedReturnType().getAnnotation(annotationClass);
}
return annotation;
}

@Override
public <A extends Annotation> A getContainerItemAnnotation(Class<A> annotationClass) {
AnnotatedType annotatedReturnType = this.getRawMember().getAnnotatedReturnType();
if (annotatedReturnType instanceof AnnotatedParameterizedType) {
AnnotatedType[] typeArguments = ((AnnotatedParameterizedType) annotatedReturnType).getAnnotatedActualTypeArguments();
if (typeArguments.length > 0) {
return typeArguments[0].getAnnotation(annotationClass);
}
}
return null;
}

@Override
public <A extends Annotation> A getAnnotationConsideringFieldAndGetter(Class<A> annotationClass) {
A annotation = this.getAnnotation(annotationClass);
Expand All @@ -152,6 +187,16 @@ public <A extends Annotation> A getAnnotationConsideringFieldAndGetter(Class<A>
return annotation;
}

@Override
public <A extends Annotation> A getContainerItemAnnotationConsideringFieldAndGetter(Class<A> annotationClass) {
A annotation = this.getContainerItemAnnotation(annotationClass);
if (annotation == null) {
MemberScope<?, ?> associatedField = this.findGetterField();
annotation = associatedField == null ? null : associatedField.getContainerItemAnnotation(annotationClass);
}
return annotation;
}

/**
* Returns the name to be used to reference this method in its parent's "properties".
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -130,32 +130,30 @@ private void applyToConfigPart(SchemaGeneratorConfigPart<?> configPart) {
}

/**
* Retrieves the annotation instance of the given type, either from the field it self or (if not present) from its getter.
* Retrieves the annotation instance of the given type, either from the field itself or (if not present) from its getter.
* <br>
* If the given field/method represents only a container item of the actual declared type, that container item's annotations are being checked.
*
* @param <A> type of annotation
* @param member field or method to retrieve annotation instance from (or from a field's getter or getter method's field)
* @param annotationClass type of annotation
* @param validationGroupsLookup how to look-up the associated validation groups of an annotation instance
* @return annotation instance (or {@code null})
* @see MemberScope#getAnnotation(Class)
* @see FieldScope#findGetter()
* @see MethodScope#findGetterField()
* @see MemberScope#getAnnotationConsideringFieldAndGetter(Class)
* @see MemberScope#getContainerItemAnnotationConsideringFieldAndGetter(Class)
*/
protected <A extends Annotation> A getAnnotationFromFieldOrGetter(MemberScope<?, ?> member, Class<A> annotationClass,
Function<A, Class<?>[]> validationGroupsLookup) {
A annotation = member.getAnnotationConsideringFieldAndGetter(annotationClass);
if (annotation != null) {
A annotation;
if (member.isFakeContainerItemScope()) {
annotation = member.getContainerItemAnnotationConsideringFieldAndGetter(annotationClass);
} else {
annotation = member.getAnnotationConsideringFieldAndGetter(annotationClass);
}
if (annotation != null && this.validationGroups != null) {
Class<?>[] associatedGroups = validationGroupsLookup.apply(annotation);
/*
* the annotation is deemed applicable in one of the following three cases:
* 1. Validation groups are specifically ignored (i.e. forValidationGroups() was never called or with null as only parameter)
* 2. No validation groups are specified on the annotation.
* 3. Some validation group(s) are specified on the annotation and at least one of them was provided via forValidationGroups().
*/
if (this.validationGroups != null && associatedGroups.length > 0
&& Collections.disjoint(this.validationGroups, Arrays.asList(associatedGroups))) {
// ignore the looked-up annotation as it is not associated with one of the desired validation groups
annotation = null;
if (associatedGroups.length > 0 && Collections.disjoint(this.validationGroups, Arrays.asList(associatedGroups))) {
return null;
}
}
return annotation;
Expand Down Expand Up @@ -202,7 +200,7 @@ protected boolean isRequired(MemberScope<?, ?> member) {
* @see Size
*/
protected Integer resolveArrayMinItems(MemberScope<?, ?> member) {
if (member.isContainerType() && !member.isFakeContainerItemScope()) {
if (member.isContainerType()) {
Size sizeAnnotation = this.getAnnotationFromFieldOrGetter(member, Size.class, Size::groups);
if (sizeAnnotation != null && sizeAnnotation.min() > 0) {
// minimum length greater than the default 0 was specified
Expand All @@ -223,7 +221,7 @@ protected Integer resolveArrayMinItems(MemberScope<?, ?> member) {
* @see Size
*/
protected Integer resolveArrayMaxItems(MemberScope<?, ?> member) {
if (member.isContainerType() && !member.isFakeContainerItemScope()) {
if (member.isContainerType()) {
Size sizeAnnotation = this.getAnnotationFromFieldOrGetter(member, Size.class, Size::groups);
if (sizeAnnotation != null && sizeAnnotation.max() < 2147483647) {
// maximum length below the default 2147483647 was specified
Expand All @@ -243,7 +241,7 @@ protected Integer resolveArrayMaxItems(MemberScope<?, ?> member) {
* @see NotBlank
*/
protected Integer resolveStringMinLength(MemberScope<?, ?> member) {
if (member.getType().isInstanceOf(CharSequence.class) && !member.isFakeContainerItemScope()) {
if (member.getType().isInstanceOf(CharSequence.class)) {
Size sizeAnnotation = this.getAnnotationFromFieldOrGetter(member, Size.class, Size::groups);
if (sizeAnnotation != null && sizeAnnotation.min() > 0) {
// minimum length greater than the default 0 was specified
Expand All @@ -265,7 +263,7 @@ protected Integer resolveStringMinLength(MemberScope<?, ?> member) {
* @see Size
*/
protected Integer resolveStringMaxLength(MemberScope<?, ?> member) {
if (member.getType().isInstanceOf(CharSequence.class) && !member.isFakeContainerItemScope()) {
if (member.getType().isInstanceOf(CharSequence.class)) {
Size sizeAnnotation = this.getAnnotationFromFieldOrGetter(member, Size.class, Size::groups);
if (sizeAnnotation != null && sizeAnnotation.max() < 2147483647) {
// maximum length below the default 2147483647 was specified
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,11 +90,11 @@ static class TestClass {
public Object nullObject;

@NotNull
public List<String> notNullList;
public List<@Min(2) @Max(2048) Integer> notNullList;
@NotEmpty
public List<String> notEmptyList;
public List<@DecimalMin(value = "0", inclusive = false) @DecimalMax(value = "1", inclusive = false) Double> notEmptyList;
@Size(min = 3, max = 25)
public List<String> sizeRangeList;
public List<@NotEmpty @Size(max = 100) String> sizeRangeList;

@NotNull
@Email(regexp = ".+@.+\\..+")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@
"minItems": 1,
"type": "array",
"items": {
"type": "string"
"type": "number",
"exclusiveMinimum": 0,
"exclusiveMaximum": 1
}
},
"notEmptyPatternText": {
Expand All @@ -35,7 +37,9 @@
"notNullList": {
"type": "array",
"items": {
"type": "string"
"type": "integer",
"minimum": 2,
"maximum": 2048
}
},
"nullObject": {},
Expand All @@ -44,7 +48,9 @@
"maxItems": 25,
"type": ["array", "null"],
"items": {
"type": "string"
"type": "string",
"minLength": 1,
"maxLength": 100
}
},
"sizeRangeText": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.Scanner;
import org.junit.Test;
import org.skyscreamer.jsonassert.JSONAssert;
Expand Down Expand Up @@ -80,7 +81,7 @@ static class TestClass {
public Object hiddenField;

@ApiModelProperty(name = "fieldWithOverriddenName")
public boolean originalFieldName;
public List<Boolean> originalFieldName;

@ApiModelProperty(value = "field description", allowableValues = "A, B, C, D")
public String fieldWithDescriptionAndAllowableValues;
Expand Down
Loading