Skip to content
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
13 changes: 12 additions & 1 deletion base/src/main/java/io/spine/validate/ComparableNumber.java
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@
import com.google.common.base.Objects;
import com.google.errorprone.annotations.Immutable;

import static com.google.common.base.Preconditions.checkNotNull;

/**
* A number that can be compared to another number.
*
Expand All @@ -41,7 +43,7 @@ public final class ComparableNumber extends Number implements Comparable<Number>
/** Creates a new instance from the specified number. */
public ComparableNumber(Number value) {
super();
this.value = value;
this.value = checkNotNull(value);
}

/** Converts this number to its textual representation. */
Expand All @@ -56,6 +58,8 @@ public Number value() {

@Override
public int compareTo(Number anotherNumber) {
checkNotNull(anotherNumber);

long thisLong = longValue();
long thatLong = anotherNumber.longValue();
if (thisLong == thatLong) {
Expand Down Expand Up @@ -84,6 +88,13 @@ public double doubleValue() {
return value.doubleValue();
}

/**
* Checks if this number is a whole number, i.e. an {@code int} or a {@code long}.
*/
public boolean isInteger() {
return value instanceof Integer || value instanceof Long;
}

@Override
public boolean equals(Object o) {
if (this == o) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ private IllegalStateException couldNotGetOptionValueFrom(String fieldName,
* @apiNote Use this in favour of {@link
* FieldOption#optionsFrom(com.google.protobuf.Descriptors.FieldDescriptor)
* optionsFrom(FieldDescriptor)} when {@code FieldContext} matters, e.g. when handling
* {@code (validation_for)} options.
* {@code (constraint_for)} options.
*/
public Optional<T> valueFrom(FieldContext context) {
Optional<T> externalConstraint =
Expand Down
31 changes: 31 additions & 0 deletions base/src/main/java/io/spine/validate/option/RangedConstraint.java
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,14 @@
import com.google.common.collect.Range;
import com.google.errorprone.annotations.Immutable;
import com.google.errorprone.annotations.ImmutableTypeParameter;
import com.google.protobuf.Descriptors.FieldDescriptor.JavaType;
import io.spine.code.proto.FieldContext;
import io.spine.code.proto.FieldDeclaration;
import io.spine.validate.ComparableNumber;

import static com.google.common.base.Preconditions.checkState;
import static com.google.common.collect.BoundType.CLOSED;
import static io.spine.util.Exceptions.newIllegalArgumentException;

/**
* A constraint that puts a numeric field value into a range.
Expand All @@ -47,9 +50,37 @@ public abstract class RangedConstraint<@ImmutableTypeParameter T> extends FieldC

RangedConstraint(T optionValue, Range<ComparableNumber> range, FieldDeclaration field) {
super(optionValue, field);
verifyType(field, range);
this.range = range;
}

@SuppressWarnings("EnumSwitchStatementWhichMissesCases")
private static void verifyType(FieldDeclaration field, Range<ComparableNumber> range) {
JavaType fieldType = field.javaType();
switch (fieldType) {
case INT: // Fallthrough intended.
case LONG: {
if (range.hasLowerBound()) {
checkInteger(range.lowerEndpoint(), field);
}
if (range.hasUpperBound()) {
checkInteger(range.upperEndpoint(), field);
}
return;
}
case FLOAT: // Fallthrough intended.
case DOUBLE:
return;
default:
throw newIllegalArgumentException("Field `%s` cannot have a number bound.", field);
}
}

private static void checkInteger(ComparableNumber number, FieldDeclaration field) {
checkState(number.isInteger(),
"An integer bound expected for field `%s`, but got `%s`.", field, number);
}

public final Range<ComparableNumber> range() {
return range;
}
Expand Down
11 changes: 11 additions & 0 deletions base/src/test/java/io/spine/validate/ComparableNumberTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,24 @@
package io.spine.validate;

import com.google.common.testing.EqualsTester;
import com.google.common.testing.NullPointerTester;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;

import static io.spine.testing.DisplayNames.NOT_ACCEPT_NULLS;

@DisplayName("Comparable number should")
class ComparableNumberTest {

@Test
@DisplayName(NOT_ACCEPT_NULLS)
void notAcceptNulls() {
NullPointerTester tester = new NullPointerTester();
tester.testAllPublicConstructors(ComparableNumber.class);
tester.testAllPublicInstanceMethods(new ComparableNumber(42));
}

@Nested
@DisplayName("have a consistent equality relationship")
class EqualsTests {
Expand Down
18 changes: 18 additions & 0 deletions base/src/test/java/io/spine/validate/NumberRangeTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,19 @@

package io.spine.validate;

import com.google.common.truth.StringSubject;
import com.google.protobuf.DoubleValue;
import com.google.protobuf.Message;
import io.spine.test.validate.InvalidBound;
import io.spine.test.validate.MaxExclusiveNumberFieldValue;
import io.spine.test.validate.MaxInclusiveNumberFieldValue;
import io.spine.test.validate.MinExclusiveNumberFieldValue;
import io.spine.test.validate.MinInclusiveNumberFieldValue;
import io.spine.type.TypeName;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

import static com.google.common.truth.Truth.assertThat;
import static io.spine.validate.ValidationOfConstraintTest.VALIDATION_SHOULD;
import static io.spine.validate.given.MessageValidatorTestEnv.EQUAL_MAX;
import static io.spine.validate.given.MessageValidatorTestEnv.EQUAL_MIN;
Expand All @@ -39,6 +43,7 @@
import static io.spine.validate.given.MessageValidatorTestEnv.LESS_THAN_MAX;
import static io.spine.validate.given.MessageValidatorTestEnv.LESS_THAN_MIN;
import static io.spine.validate.given.MessageValidatorTestEnv.VALUE;
import static org.junit.jupiter.api.Assertions.assertThrows;

@DisplayName(VALIDATION_SHOULD + "analyze (min) and (max) options and")
class NumberRangeTest extends ValidationOfConstraintTest {
Expand Down Expand Up @@ -138,6 +143,19 @@ void provideOneValidViolationIfNumberIsGreaterThanDecimalMax() {
assertSingleViolation(GREATER_MAX_MSG, VALUE);
}

@Test
@DisplayName("not allow fraction boundaries for integer fields")
void fractionsBounds() {
IllegalStateException exception =
assertThrows(IllegalStateException.class,
() -> validate(InvalidBound.getDefaultInstance()));
StringSubject assertMessage = assertThat(exception).hasMessageThat();
assertMessage
.contains("2.71");
assertMessage
.contains(TypeName.of(InvalidBound.class).value());
}

private void minNumberTest(double value, boolean inclusive, boolean valid) {
Message msg = inclusive
? MinInclusiveNumberFieldValue
Expand Down
4 changes: 4 additions & 0 deletions base/src/test/proto/spine/test/validate/messages.proto
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,10 @@ message MaxExclusiveNumberFieldValue {
double value = 1 [(max).value = "64.5", (max).exclusive = true];
}

message InvalidBound {
int64 value = 1 [(min).value = "2.71"];
}

// Messages for "valid" option tests.

message EnclosedMessageFieldValue {
Expand Down
Loading