Skip to content
Draft
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: 6 additions & 7 deletions spec/src/main/asciidoc/microprofile-openapi-spec.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -599,7 +599,7 @@ In some cases, additional schema restrictions can be inferred from Jakarta Bean

If an implementation includes support for the Jakarta Bean Validation specification, then it must also process Jakarta Bean Validation annotations when creating OpenAPI schemas. Such implementations must add the properties listed in the table below to the schema model when:

* the annotation is applied to to an element for which a schema is generated and
* the annotation is applied to an element for which a schema is generated and
* the annotation and generated schema type are listed together in the table below and
* the annotation has a `group` attribute which is empty or includes `jakarta.validation.groups.Default` and
* the user has not set any of the relevant property values using other annotations and
Expand All @@ -611,19 +611,18 @@ If an implementation includes support for the Jakarta Bean Validation specificat
| `@NotEmpty` | `array` | `minItems = 1`
| `@NotEmpty` | `object` | `minProperties = 1`
| `@NotBlank` | `string` | `pattern = \S`
| `@Size(min = a, max = b)` | `string`
| `minLength = a +
| `@Size(min = a, max = b)` | `string` | `minLength = a +
maxLenth = b`
| `@Size(min = a, max = b)` | `array`
| `minItems = a +
| `@Size(min = a, max = b)` | `array` | `minItems = a +
maxItems = b`
| `@Size(min = a, max = b)` | `object`
| `minProperties = a +
| `@Size(min = a, max = b)` | `object` | `minProperties = a +
maxProperties = b`
| `@DecimalMax(value = a)` | `number` or `integer` | `maximum = a`
| `@DecimalMax(value = a, inclusive = false)` | `number` or `integer` | `exclusiveMaximum = a`
| `@DecimalMin(value = a)` | `number` or `integer` | `minimum = a`
| `@DecimalMin(value = a, inclusive = false)` | `number` or `integer` | `exclusiveMinimum = a`
| `@Digits(integer = <any>, fraction = f)` | `number` or `integer` | `multipleOf` equal to `1` for integer types or 10^-f for non-integer types
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we need to generate multipleOf: 1 if the schema type is integer, since that's already implied.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe a better way to specify this would be in terms of fraction rather than in terms of the schema type. When fraction < 1, we expect multipleOf = 1, otherwise it will be 10^-f.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well possibly, but consider this field:

@Digits(integer = 4, fraction = 0)
int myNumber;

We'd expect this schema:

{
    "type": "integer",
    "multipleOf": 1
}

Here the multipleOf: 1 is redundant since the schema already requires an integer. I don't think we should require this.

This might mean that in the TCK we have to check for both cases: either the schema type is integer, or multipleOf: 1 is present.

| `@Digits(integer = i, fraction = f)` | `string` | `pattern` matching any string value that satisfies the constraint
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Azquelt did we discuss having the string case in the specification or only the multipleOf for numbers?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm happy to have both.

The only reason we don't have @Digits for strings already was that it was odd to have it for strings but not numbers.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, I need to review the BV spec to make sure it's aligned there. I can imagine various gaps in how BV implementations validate cases like integer = 0 or fraction = 0. For example, do we expect @Digits(integer = 0, fraction = 2) to allow both ".25" and "0.25" or maybe the TCK just stays away from those edge cases.

| `@Max(a)` | `number` or `integer` | `maximum = a`
| `@Min(a)` | `number` or `integer` | `minimum = a`
| `@Negative` | `number` or `integer` | `exclusiveMaximum = 0`
Expand Down
6 changes: 6 additions & 0 deletions spec/src/main/asciidoc/release_notes.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,12 @@ A full list of changes delivered in the 4.2 release can be found at link:https:/

* Add `example` and `examples` to `@Header` and verify implementation support in TCK (https://github.com/microprofile/microprofile-open-api/issues/697)[697])

[[other_changes_42]]
==== Other Changes

* Add processing of Jakarta Bean Validation `@Digits` annotation (https://github.com/eclipse/microprofile-open-api/issues/717[717])


[[release_notes_41]]
=== Release Notes for MicroProfile OpenAPI 4.1

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,15 @@
package org.eclipse.microprofile.openapi.apps.beanvalidation;

import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.List;
import java.util.Map;

import org.eclipse.microprofile.openapi.annotations.media.Schema;

import jakarta.validation.constraints.DecimalMax;
import jakarta.validation.constraints.DecimalMin;
import jakarta.validation.constraints.Digits;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.Negative;
Expand Down Expand Up @@ -69,6 +71,31 @@ public class BeanValidationData {
@DecimalMin(value = "3.25", inclusive = false)
private BigDecimal minDecimalExclusive;

@Digits(integer = 9, fraction = 0)
private int digitsInt32;

@Digits(integer = 18, fraction = 0)
private int digitsInt64;

@Digits(integer = 5, fraction = 3)
private float digitsFloat32;

@Digits(integer = 10, fraction = 6)
private double digitsFloat64;

@Digits(integer = 20, fraction = 10)
private BigDecimal digitsDecimal;

@Digits(integer = 20, fraction = 0)
private BigInteger digitsInteger;

@Digits(integer = 20, fraction = 0)
@Schema(multipleOf = 1000)
private BigInteger digitsCustomInteger;

@Digits(integer = 10, fraction = 5)
private BigInteger digitsString;

@Max(5)
private int maxInt;

Expand Down Expand Up @@ -185,6 +212,70 @@ public void setMinDecimalExclusive(BigDecimal minDecimalExclusive) {
this.minDecimalExclusive = minDecimalExclusive;
}

public int getDigitsInt32() {
return digitsInt32;
}

public void setDigitsInt32(int digitsInt32) {
this.digitsInt32 = digitsInt32;
}

public int getDigitsInt64() {
return digitsInt64;
}

public void setDigitsInt64(int digitsInt64) {
this.digitsInt64 = digitsInt64;
}

public float getDigitsFloat32() {
return digitsFloat32;
}

public void setDigitsFloat32(float digitsFloat32) {
this.digitsFloat32 = digitsFloat32;
}

public double getDigitsFloat64() {
return digitsFloat64;
}

public void setDigitsFloat64(double digitsFloat64) {
this.digitsFloat64 = digitsFloat64;
}

public BigDecimal getDigitsDecimal() {
return digitsDecimal;
}

public void setDigitsDecimal(BigDecimal digitsDecimal) {
this.digitsDecimal = digitsDecimal;
}

public BigInteger getDigitsInteger() {
return digitsInteger;
}

public void setDigitsInteger(BigInteger digitsInteger) {
this.digitsInteger = digitsInteger;
}

public BigInteger getDigitsCustomInteger() {
return digitsCustomInteger;
}

public void setDigitsCustomInteger(BigInteger digitsCustomInteger) {
this.digitsCustomInteger = digitsCustomInteger;
}

public BigInteger getDigitsString() {
return digitsString;
}

public void setDigitsString(BigInteger digitsString) {
this.digitsString = digitsString;
}

public int getMaxInt() {
return maxInt;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@
package org.eclipse.microprofile.openapi.tck.beanvalidation;

import static org.eclipse.microprofile.openapi.tck.Groups.BEAN_VALIDATION;
import static org.eclipse.microprofile.openapi.tck.utils.TCKMatchers.comparesEqualToNumber;
import static org.eclipse.microprofile.openapi.tck.utils.TCKMatchers.itemOrSingleton;
import static org.eclipse.microprofile.openapi.tck.utils.TCKMatchers.patternMatchesValue;
import static org.hamcrest.Matchers.hasEntry;
import static org.hamcrest.Matchers.hasKey;
import static org.hamcrest.Matchers.is;
Expand All @@ -34,6 +36,8 @@

public class BeanValidationTest extends AppTestBase {

private static final String MULTIPLE_OF = "multipleOf";

@Deployment(testable = false)
public static WebArchive buildApp() {
return ShrinkWrap.create(WebArchive.class, "beanValidation.war")
Expand Down Expand Up @@ -181,6 +185,41 @@ public void defaultAndOtherGroupsTest(String format) {
assertProperty(vr, "defaultAndOtherGroups", hasEntry("minLength", 1));
}

@Test(dataProvider = "formatProvider", groups = BEAN_VALIDATION)
public void integerDigitsTest(String format) {
ValidatableResponse vr = callEndpoint(format);
assertProperty(vr, "digitsInt32", hasEntry(is(MULTIPLE_OF), comparesEqualToNumber(1)));
assertProperty(vr, "digitsInt64", hasEntry(is(MULTIPLE_OF), comparesEqualToNumber(1)));
assertProperty(vr, "digitsInteger", hasEntry(is(MULTIPLE_OF), comparesEqualToNumber(1)));
}

@Test(dataProvider = "formatProvider", groups = BEAN_VALIDATION)
public void decimalDigitsTest(String format) {
ValidatableResponse vr = callEndpoint(format);
assertProperty(vr, "digitsFloat32", hasEntry(is(MULTIPLE_OF), comparesEqualToNumber(0.001)));
assertProperty(vr, "digitsFloat64", hasEntry(is(MULTIPLE_OF), comparesEqualToNumber(0.000001)));
assertProperty(vr, "digitsDecimal", hasEntry(is(MULTIPLE_OF), comparesEqualToNumber(0.0000000001)));
}

@Test(dataProvider = "formatProvider", groups = BEAN_VALIDATION)
public void customIntegerDigitsTest(String format) {
ValidatableResponse vr = callEndpoint(format);
assertProperty(vr, "digitsCustomInteger", hasEntry(is(MULTIPLE_OF), comparesEqualToNumber(1000)));
}

@Test(dataProvider = "formatProvider", groups = BEAN_VALIDATION)
public void stringDigitsTest(String format) {
final String patternAttribute = "digitsString.pattern";
ValidatableResponse vr = callEndpoint(format);
// Constraint allows up to 10 integer digits and up to 5 fractional digits
assertProperty(vr, patternAttribute, patternMatchesValue("1"));
assertProperty(vr, patternAttribute, patternMatchesValue("1.5"));
assertProperty(vr, patternAttribute, patternMatchesValue("123456789.1234"));
assertProperty(vr, patternAttribute, patternMatchesValue("1234567890.12345"));
assertProperty(vr, patternAttribute, not(patternMatchesValue("12345678901.12345")));
assertProperty(vr, patternAttribute, not(patternMatchesValue("1234567890.123456")));
}

@Test(dataProvider = "formatProvider", groups = BEAN_VALIDATION)
public void parameterTest(String format) {
ValidatableResponse vr = callEndpoint(format);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
import java.math.BigInteger;
import java.util.Collection;
import java.util.Comparator;
import java.util.regex.Pattern;

import org.hamcrest.Description;
import org.hamcrest.Matcher;
Expand All @@ -41,10 +42,18 @@ private TCKMatchers() {
/**
* Compares two numbers as BigDecimals
*/
private static final Comparator<Number> NUMERIC_COMPARATOR = (value1, value2) -> {
final BigDecimal decimal1 = BigDecimal.valueOf(value1.doubleValue());
final BigDecimal decimal2 = BigDecimal.valueOf(value2.doubleValue());
return decimal1.compareTo(decimal2);
private static final Comparator<Number> NUMERIC_COMPARATOR = new Comparator<>() {
@Override
public String toString() {
return getClass().getName();
}

@Override
public int compare(Number value1, Number value2) {
final BigDecimal decimal1 = new BigDecimal(value1.toString());
final BigDecimal decimal2 = new BigDecimal(value2.toString());
return decimal1.compareTo(decimal2);
}
};

/**
Expand Down Expand Up @@ -192,4 +201,35 @@ public static <T> Matcher<T> hasOptionalEntry(String entryName, Object value) {

return allOf(isA(java.util.Map.class), either(hasEntry).or(entryMissing));
}

/**
* Creates a matcher which matches when the given value matches a string compiled to a regular expression pattern.
*
* @param value
* a value to match against the regular expression
* @return the matcher
*/
public static Matcher<String> patternMatchesValue(String value) {
return new PatternMatchable(value);
}

private static class PatternMatchable extends TypeSafeDiagnosingMatcher<String> {
String value;

PatternMatchable(String value) {
this.value = value;
}

@Override
public void describeTo(Description desc) {
desc.appendText("A pattern matching string ").appendValue(value);
}

@Override
protected boolean matchesSafely(String item, Description mismatchDescription) {
Pattern pattern = Pattern.compile(item);
mismatchDescription.appendText("pattern was: ").appendValue(pattern);
return pattern.matcher(this.value).matches();
}
}
}