Skip to content

Commit

Permalink
Merge pull request #403 from raynigon/feature/validation
Browse files Browse the repository at this point in the history
Add Validation annotations for quantities
  • Loading branch information
raynigon committed Oct 6, 2023
2 parents adfd65c + 07010a3 commit 061d331
Show file tree
Hide file tree
Showing 9 changed files with 314 additions and 0 deletions.
1 change: 1 addition & 0 deletions settings.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ rootProject.name = 'unit-api'
include 'unit-api-core'
include 'unit-api-kotlin'
include 'unit-api-jackson'
include 'unit-api-validation'
include 'spring-boot-core-starter'
include 'spring-boot-jackson-starter'
include 'spring-boot-jpa-starter'
Expand Down
8 changes: 8 additions & 0 deletions unit-api-validation/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
dependencies {
implementation("jakarta.validation:jakarta.validation-api:3.0.2")
implementation project(":unit-api-core")

testImplementation 'com.fasterxml.jackson.core:jackson-databind:2.15.2'
testImplementation 'org.hibernate.validator:hibernate-validator:8.0.1.Final'
testImplementation("org.glassfish:jakarta.el:4.0.2")
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package com.raynigon.unit.api.validation.annotation;

import com.raynigon.unit.api.validation.validator.UnitMaxValidator;
import jakarta.validation.Constraint;
import jakarta.validation.Payload;

import javax.measure.Unit;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;

import static java.lang.annotation.ElementType.*;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

/**
* The annotated element must be a number whose value must be higher or
* equal to the specified minimum.
* <p>
* Supported types are:
* <ul>
* <li>{@code Quantity}</li>
* </ul>
* Note that {@code double} and {@code float} are not supported due to rounding errors
* (some providers might provide some approximative support).
* <p>
* {@code null} elements are considered valid.
*
* @author Simon Schneider
*/
@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE})
@Retention(RUNTIME)
@Documented
@Constraint(validatedBy = {UnitMaxValidator.class})
public @interface UnitMax {

String message() default "{com.raynigon.unit.api.validation.UnitMax.message}";

Class<?>[] groups() default {};

Class<? extends Payload>[] payload() default {};

/**
* The {@code String} representation of the min value according to the
* {@code BigDecimal} string representation.
*
* @return value the element must be higher or equal to
*/
double value();

/**
* The unit which should be used to compare the value to
*/
Class<? extends Unit<?>> unit();
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package com.raynigon.unit.api.validation.annotation;

import com.raynigon.unit.api.validation.validator.UnitMinValidator;
import jakarta.validation.Constraint;
import jakarta.validation.Payload;

import javax.measure.Unit;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;

import static java.lang.annotation.ElementType.*;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

/**
* The annotated element must be a number whose value must be higher or
* equal to the specified minimum.
* <p>
* Supported types are:
* <ul>
* <li>{@code Quantity}</li>
* </ul>
* Note that {@code double} and {@code float} are not supported due to rounding errors
* (some providers might provide some approximative support).
* <p>
* {@code null} elements are considered valid.
*
* @author Simon Schneider
*/
@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE})
@Retention(RUNTIME)
@Documented
@Constraint(validatedBy = {UnitMinValidator.class})
public @interface UnitMin {

String message() default "{com.raynigon.unit.api.validation.UnitMin.message}";

Class<?>[] groups() default {};

Class<? extends Payload>[] payload() default {};

/**
* The {@code String} representation of the min value according to the
* {@code BigDecimal} string representation.
*
* @return value the element must be higher or equal to
*/
double value();

/**
* The unit which should be used to compare the value to
*/
Class<? extends Unit<?>> unit();
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package com.raynigon.unit.api.validation.validator;

import jakarta.validation.ConstraintValidator;

import javax.measure.Quantity;
import javax.measure.Unit;
import java.lang.annotation.Annotation;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.util.function.BiPredicate;

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

private Unit<?> unit = null;
private double value = 0.0;

@Override
public void initialize(A constraintAnnotation) {
unit = createUnit(getUnit(constraintAnnotation));
value = getValue(constraintAnnotation);
}

@SuppressWarnings({"rawtypes", "unchecked"})
protected boolean check(Quantity<?> quantity, BiPredicate<Double, Double> comparator) {
if (quantity == null) return true;
if (unit == null) return false;
double quantityAsNumber = ((Quantity) quantity)
.to(unit).getValue().doubleValue();
return comparator.test(quantityAsNumber, value);
}

protected abstract Class<? extends Unit<?>> getUnit(A constraintAnnotation);

protected abstract double getValue(A constraintAnnotation);

private static Unit<?> createUnit(Class<? extends Unit<?>> unitType) {
try {
Constructor<? extends Unit<?>> ctor = unitType.getConstructor();
return ctor.newInstance();
} catch (NoSuchMethodException | InstantiationException | IllegalAccessException |
InvocationTargetException e) {
throw new IllegalArgumentException("Unable to create Unit " + unitType, e);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package com.raynigon.unit.api.validation.validator;

import com.raynigon.unit.api.validation.annotation.UnitMax;
import jakarta.validation.ConstraintValidatorContext;

import javax.measure.Quantity;
import javax.measure.Unit;

public class UnitMaxValidator extends AbstractUnitValidator<UnitMax> {

@Override
protected Class<? extends Unit<?>> getUnit(UnitMax constraintAnnotation) {
return constraintAnnotation.unit();
}

@Override
protected double getValue(UnitMax constraintAnnotation) {
return constraintAnnotation.value();
}

@Override
public boolean isValid(Quantity<?> value, ConstraintValidatorContext context) {
return check(value, ((q, v) -> q <= v));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package com.raynigon.unit.api.validation.validator;

import com.raynigon.unit.api.validation.annotation.UnitMin;
import jakarta.validation.ConstraintValidatorContext;

import javax.measure.Quantity;
import javax.measure.Unit;

public class UnitMinValidator extends AbstractUnitValidator<UnitMin> {

@Override
protected Class<? extends Unit<?>> getUnit(UnitMin constraintAnnotation) {
return constraintAnnotation.unit();
}

@Override
protected double getValue(UnitMin constraintAnnotation) {
return constraintAnnotation.value();
}

@Override
public boolean isValid(Quantity<?> value, ConstraintValidatorContext context) {
return check(value, ((q, v) -> q >= v));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package com.raynigon.unit.api.validation.validator

import com.raynigon.unit.api.core.units.si.SISystemUnitsConstants
import com.raynigon.unit.api.core.units.si.power.Watt
import com.raynigon.unit.api.validation.annotation.UnitMax
import jakarta.validation.Validation
import jakarta.validation.Validator
import spock.lang.Specification

import javax.measure.Quantity
import javax.measure.quantity.Power

class UnitMaxValidatorSpec extends Specification {

Validator validator

def setup() {
validator = Validation.buildDefaultValidatorFactory().getValidator()
}

def "validate unit max with value=#input"() {
given:
DummyEntity entity = new DummyEntity(SISystemUnitsConstants.Watt(input))

when:
def result = validator.validate(entity)

then:
result.size() == expected

where:
input | expected
1 | 0
9 | 0
10 | 1
11 | 1
}

static class DummyEntity {

@UnitMax(value = 9.0, unit = Watt.class)
Quantity<Power> power

DummyEntity(Quantity<Power> power) {
this.power = power
}
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package com.raynigon.unit.api.validation.validator

import com.raynigon.unit.api.core.units.si.SISystemUnitsConstants
import com.raynigon.unit.api.core.units.si.power.Watt
import com.raynigon.unit.api.validation.annotation.UnitMax
import com.raynigon.unit.api.validation.annotation.UnitMin
import jakarta.validation.Validation
import jakarta.validation.Validator
import spock.lang.Specification

import javax.measure.Quantity
import javax.measure.quantity.Power

class UnitMinValidatorSpec extends Specification {

Validator validator

def setup() {
validator = Validation.buildDefaultValidatorFactory().getValidator()
}

def "validate unit max with value=#input"() {
given:
DummyEntity entity = new DummyEntity(SISystemUnitsConstants.Watt(input))

when:
def result = validator.validate(entity)

then:
result.size() == expected

where:
input | expected
1 | 1
8 | 1
9 | 0
10 | 0
}

static class DummyEntity {

@UnitMin(value = 9.0, unit = Watt.class)
Quantity<Power> power

DummyEntity(Quantity<Power> power) {
this.power = power
}
}
}

0 comments on commit 061d331

Please sign in to comment.