Skip to content

Commit

Permalink
Add Documentation for Validation Constraints
Browse files Browse the repository at this point in the history
  • Loading branch information
raynigon committed Oct 26, 2023
1 parent 43fd48a commit 4327f82
Show file tree
Hide file tree
Showing 5 changed files with 222 additions and 16 deletions.
2 changes: 1 addition & 1 deletion spring-boot-core-starter/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,5 @@ dependencies {
testImplementation('org.springframework.boot:spring-boot-starter-test') {
exclude group: 'org.junit.vintage', module: 'junit-vintage-engine'
}
testImplementation 'org.spockframework:spock-spring'
testImplementation 'org.spockframework:spock-spring:2.4-M1-groovy-4.0'
}
3 changes: 2 additions & 1 deletion spring-boot-springdoc-starter/build.gradle
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
dependencies {
compileOnly 'org.springframework.boot:spring-boot-starter'
compileOnly 'org.springdoc:springdoc-openapi-webmvc-core:1.7.0'
implementation project(":unit-api-jackson")
implementation project(":unit-api-core")
implementation project(":unit-api-jackson")
implementation project(":unit-api-validation")
implementation project(":spring-boot-core-starter")

testImplementation 'org.springdoc:springdoc-openapi-webmvc-core:1.7.0'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,23 @@
import com.raynigon.unit.api.core.service.UnitsApiService;
import com.raynigon.unit.api.jackson.annotation.JsonUnit;
import com.raynigon.unit.api.jackson.annotation.JsonUnitHelper;
import com.raynigon.unit.api.validation.annotation.UnitMax;
import com.raynigon.unit.api.validation.annotation.UnitMin;
import com.raynigon.unit.api.validation.validator.AbstractUnitValidator;
import io.swagger.v3.core.converter.AnnotatedType;
import io.swagger.v3.oas.models.media.NumberSchema;
import io.swagger.v3.oas.models.media.Schema;

import java.lang.annotation.Annotation;
import java.lang.reflect.Type;
import java.util.HashMap;
import java.util.Map;
import java.util.*;

Check warning on line 17 in spring-boot-springdoc-starter/src/main/java/com/raynigon/unit/api/springdoc/UnitApiPropertyCustomizer.java

View workflow job for this annotation

GitHub Actions / Checkstyle

com.puppycrawl.tools.checkstyle.checks.imports.AvoidStarImportCheck

Using the '.*' form of import should be avoided - java.util.*.
import java.util.stream.Collectors;
import java.util.stream.Stream;
import javax.measure.Quantity;
import javax.measure.Unit;

import io.swagger.v3.oas.models.media.StringSchema;
import lombok.val;
import org.springdoc.core.customizers.PropertyCustomizer;

public class UnitApiPropertyCustomizer implements PropertyCustomizer {

Check warning on line 27 in spring-boot-springdoc-starter/src/main/java/com/raynigon/unit/api/springdoc/UnitApiPropertyCustomizer.java

View workflow job for this annotation

GitHub Actions / Checkstyle

com.puppycrawl.tools.checkstyle.checks.javadoc.MissingJavadocTypeCheck

Missing a Javadoc comment.
Expand All @@ -26,6 +31,7 @@ public class UnitApiPropertyCustomizer implements PropertyCustomizer {
public Schema<?> customize(Schema property, AnnotatedType type) {
if (isApplicable(type.getType())) {
Unit<?> unit = resolveUnit(type);
Set<Annotation> constraints = resolveConstraints(type);
QuantityShape shape = resolveShape(type);
switch (shape) {
case OBJECT:
Expand All @@ -42,19 +48,13 @@ public Schema<?> customize(Schema property, AnnotatedType type) {
property.setType("string");
property.setProperties(null);
}
property.setDescription(
buildDescription(type.getPropertyName(), unit, property.getDescription()));
String description = buildDescription(type, property, unit, constraints);
property.setDescription(description);
property.setExample("1" + (unit.getSymbol() != null ? " " + unit.getSymbol() : ""));
}
return property;
}

private QuantityShape resolveShape(AnnotatedType type) {
JsonUnit jsonUnit = resolveJsonUnit(type);
if (jsonUnit == null) return QuantityShape.NUMBER;
return JsonUnitHelper.getShape(jsonUnit);
}


@SuppressWarnings({"rawtypes", "unchecked"})
private Unit<?> resolveUnit(AnnotatedType type) {
Expand All @@ -79,12 +79,26 @@ private JsonUnit resolveJsonUnit(AnnotatedType type) {
return jsonUnit;
}

private QuantityShape resolveShape(AnnotatedType type) {
JsonUnit jsonUnit = resolveJsonUnit(type);
if (jsonUnit == null) return QuantityShape.NUMBER;

Check warning on line 84 in spring-boot-springdoc-starter/src/main/java/com/raynigon/unit/api/springdoc/UnitApiPropertyCustomizer.java

View workflow job for this annotation

GitHub Actions / Checkstyle

com.puppycrawl.tools.checkstyle.checks.blocks.NeedBracesCheck

'if' construct must use '{}'s.
return JsonUnitHelper.getShape(jsonUnit);
}

private Set<Annotation> resolveConstraints(AnnotatedType type) {
return Arrays.stream(type.getCtxAnnotations())
.filter(it -> UnitMin.class.isAssignableFrom(it.annotationType()) || UnitMax.class.isAssignableFrom(it.annotationType()))

Check warning on line 90 in spring-boot-springdoc-starter/src/main/java/com/raynigon/unit/api/springdoc/UnitApiPropertyCustomizer.java

View workflow job for this annotation

GitHub Actions / Checkstyle

com.puppycrawl.tools.checkstyle.checks.sizes.LineLengthCheck

Line is longer than 120 characters (found 137).
.collect(Collectors.toSet());
}

private boolean isApplicable(Type type) {
return (type instanceof SimpleType)
&& Quantity.class.isAssignableFrom(((SimpleType) type).getRawClass());
}

private String buildDescription(String name, Unit<?> unit, String description) {
private String buildDescription(AnnotatedType type, Schema property, Unit<?> unit, Set<Annotation> constraints) {
String name = type.getPropertyName();
String description = property.getDescription();
String result = "";
if (unit != null) {
String unitName = unit.getName();
Expand All @@ -96,15 +110,46 @@ private String buildDescription(String name, Unit<?> unit, String description) {
result += " (" + unitSymbol + ")";
}
}
if (constraints != null) {
if (constraints.size() == 2) {
// Case 1: UnitMin and UnitMax are present in the constraints set
UnitMin min = getAnnotation(constraints, UnitMin.class);
Unit<?> minUnit = AbstractUnitValidator.createUnit(min.unit());
UnitMax max = getAnnotation(constraints, UnitMax.class);
Unit<?> maxUnit = AbstractUnitValidator.createUnit(max.unit());

result += " and must be between " + min.value() + " " + minUnit.getSymbol() + " and " + max.value() + " " + maxUnit.getSymbol();

Check warning on line 121 in spring-boot-springdoc-starter/src/main/java/com/raynigon/unit/api/springdoc/UnitApiPropertyCustomizer.java

View workflow job for this annotation

GitHub Actions / Checkstyle

com.puppycrawl.tools.checkstyle.checks.sizes.LineLengthCheck

Line is longer than 120 characters (found 144).
} else if (constraints.stream().anyMatch(it -> UnitMin.class.isAssignableFrom(it.annotationType()))) {
// Case 2: UnitMin is present in the constraints set
UnitMin min = getAnnotation(constraints, UnitMin.class);
Unit<?> minUnit = AbstractUnitValidator.createUnit(min.unit());

result += " and must be greater than " + min.value() + " " + minUnit.getSymbol();
} else if (constraints.stream().anyMatch(it -> UnitMax.class.isAssignableFrom(it.annotationType()))) {
// Case 2: UnitMax is present in the constraints set
UnitMax max = getAnnotation(constraints, UnitMax.class);
Unit<?> maxUnit = AbstractUnitValidator.createUnit(max.unit());

result += " and must be less than " + max.value() + " " + maxUnit.getSymbol();
}
}
if (description != null) {
if (!result.equals("")) {
if (!result.isEmpty()) {
result += "\n";
}
result += description;
}
return result;
}

@SuppressWarnings("unchecked")
private <T> T getAnnotation(Collection<Annotation> events, Class<T> clazz) {
return (T) events.stream()
.filter(it -> clazz.isAssignableFrom(it.annotationType()))
.findFirst()
.orElse(null);
}

private Map<String, Schema<?>> buildQuantityObjectProperties() {
Map<String, Schema<?>> properties = new HashMap<>();
NumberSchema value = new NumberSchema();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,11 @@ import com.fasterxml.jackson.databind.JavaType
import com.fasterxml.jackson.databind.type.SimpleType
import com.fasterxml.jackson.databind.type.TypeBindings
import com.raynigon.unit.api.core.annotation.QuantityShape
import com.raynigon.unit.api.core.units.si.power.Watt
import com.raynigon.unit.api.core.units.si.speed.KilometrePerHour
import com.raynigon.unit.api.jackson.annotation.JsonUnit
import com.raynigon.unit.api.validation.annotation.UnitMax
import com.raynigon.unit.api.validation.annotation.UnitMin
import io.swagger.v3.core.converter.AnnotatedType
import io.swagger.v3.oas.models.media.NumberSchema
import io.swagger.v3.oas.models.media.Schema
Expand Down Expand Up @@ -67,6 +70,7 @@ class UnitApiPropertyCustomizerSpec extends Specification {
annotatedType.ctxAnnotations([
jsonUnit
] as Annotation[])
jsonUnit.annotationType() >> JsonUnit.class
jsonUnit.value() >> JsonUnit.NoneUnit.class
jsonUnit.shape() >> QuantityShape.STRING

Expand Down Expand Up @@ -102,6 +106,7 @@ class UnitApiPropertyCustomizerSpec extends Specification {
annotatedType.ctxAnnotations([
jsonUnit
] as Annotation[])
jsonUnit.annotationType() >> JsonUnit.class
jsonUnit.value() >> JsonUnit.NoneUnit.class
jsonUnit.shape() >> QuantityShape.OBJECT

Expand Down Expand Up @@ -140,6 +145,7 @@ class UnitApiPropertyCustomizerSpec extends Specification {
annotatedType.ctxAnnotations([
jsonUnit
] as Annotation[])
jsonUnit.annotationType() >> JsonUnit.class
jsonUnit.value() >> JsonUnit.NoneUnit.class
jsonUnit.shape() >> QuantityShape.STRING

Expand All @@ -159,4 +165,158 @@ class UnitApiPropertyCustomizerSpec extends Specification {
"" | "speed is given in Kilometre per Hour (km/h)\n"
"test" | "speed is given in Kilometre per Hour (km/h)\ntest"
}

def 'convert quantity with annotation and min'() {

given:
def customizer = new UnitApiPropertyCustomizer()
def property = new Schema()
property.name = "speed"

and:
def annotatedType = new AnnotatedType()
annotatedType.propertyName = "speed"
annotatedType.type = new SimpleType(
Quantity.class,
TypeBindings.create(
Quantity.class,
[new SimpleType(Speed.class)] as JavaType[],
),
null,
null
)
and:
JsonUnit jsonUnit = Mock(JsonUnit)
jsonUnit.annotationType() >> JsonUnit.class
jsonUnit.value() >> JsonUnit.NoneUnit.class
jsonUnit.shape() >> QuantityShape.STRING

and:
UnitMin unitMin = Mock(UnitMin)
unitMin.annotationType() >> UnitMin.class
unitMin.value() >> 0.0
unitMin.unit() >> Watt.class

and:
annotatedType.ctxAnnotations([
jsonUnit,
unitMin
] as Annotation[])


when:
def result = customizer.customize(property, annotatedType)

then:
2 * jsonUnit.unit() >> KilometrePerHour.class

and:
result.type == "string"
result.description == "speed is given in Kilometre per Hour (km/h) and must be greater than 0.0 W"
}

def 'convert quantity with annotation and max'() {

given:
def customizer = new UnitApiPropertyCustomizer()
def property = new Schema()
property.name = "speed"

and:
def annotatedType = new AnnotatedType()
annotatedType.propertyName = "speed"
annotatedType.type = new SimpleType(
Quantity.class,
TypeBindings.create(
Quantity.class,
[new SimpleType(Speed.class)] as JavaType[],
),
null,
null
)
and:
JsonUnit jsonUnit = Mock(JsonUnit)
jsonUnit.annotationType() >> JsonUnit.class
jsonUnit.value() >> JsonUnit.NoneUnit.class
jsonUnit.shape() >> QuantityShape.STRING

and:
UnitMax unitMax = Mock(UnitMax)
unitMax.annotationType() >> unitMax.class
unitMax.value() >> 10.0
unitMax.unit() >> Watt.class

and:
annotatedType.ctxAnnotations([
jsonUnit,
unitMax
] as Annotation[])


when:
def result = customizer.customize(property, annotatedType)

then:
2 * jsonUnit.unit() >> KilometrePerHour.class

and:
result.type == "string"
result.description == "speed is given in Kilometre per Hour (km/h) and must be less than 10.0 W"
}

def 'convert quantity with annotation and min + max'() {

given:
def customizer = new UnitApiPropertyCustomizer()
def property = new Schema()
property.name = "speed"

and:
def annotatedType = new AnnotatedType()
annotatedType.propertyName = "speed"
annotatedType.type = new SimpleType(
Quantity.class,
TypeBindings.create(
Quantity.class,
[new SimpleType(Speed.class)] as JavaType[],
),
null,
null
)
and:
JsonUnit jsonUnit = Mock(JsonUnit)
jsonUnit.annotationType() >> JsonUnit.class
jsonUnit.value() >> JsonUnit.NoneUnit.class
jsonUnit.shape() >> QuantityShape.STRING

and:
UnitMin unitMin = Mock(UnitMin)
unitMin.annotationType() >> UnitMin.class
unitMin.value() >> 0.0
unitMin.unit() >> Watt.class

and:
UnitMax unitMax = Mock(UnitMax)
unitMax.annotationType() >> UnitMax.class
unitMax.value() >> 10.0
unitMax.unit() >> Watt.class

and:
annotatedType.ctxAnnotations([
jsonUnit,
unitMin,
unitMax
] as Annotation[])


when:
def result = customizer.customize(property, annotatedType)

then:
2 * jsonUnit.unit() >> KilometrePerHour.class

and:
result.type == "string"
result.description == "speed is given in Kilometre per Hour (km/h) and must be between 0.0 W and 10.0 W"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
import java.lang.reflect.InvocationTargetException;
import java.util.function.BiPredicate;

abstract class AbstractUnitValidator<A extends Annotation>
abstract public class AbstractUnitValidator<A extends Annotation>
implements ConstraintValidator<A, Quantity<?>> {

private Unit<?> unit = null;
Expand All @@ -35,7 +35,7 @@ protected boolean check(ConstraintValidatorContext context, Quantity<?> quantity

protected abstract double getValue(A constraintAnnotation);

private static Unit<?> createUnit(Class<? extends Unit<?>> unitType) {
public static Unit<?> createUnit(Class<? extends Unit<?>> unitType) {
try {
Constructor<? extends Unit<?>> ctor = unitType.getConstructor();
return ctor.newInstance();
Expand Down

0 comments on commit 4327f82

Please sign in to comment.