diff --git a/spec/src/main/asciidoc/microprofile-openapi-spec.asciidoc b/spec/src/main/asciidoc/microprofile-openapi-spec.asciidoc index fa9594b4..2a647e96 100644 --- a/spec/src/main/asciidoc/microprofile-openapi-spec.asciidoc +++ b/spec/src/main/asciidoc/microprofile-openapi-spec.asciidoc @@ -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 @@ -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 = , fraction = f)` | `number` or `integer` | `multipleOf` equal to `1` for integer types or 10^-f for non-integer types +| `@Digits(integer = i, fraction = f)` | `string` | `pattern` matching any string value that satisfies the constraint | `@Max(a)` | `number` or `integer` | `maximum = a` | `@Min(a)` | `number` or `integer` | `minimum = a` | `@Negative` | `number` or `integer` | `exclusiveMaximum = 0` diff --git a/spec/src/main/asciidoc/release_notes.asciidoc b/spec/src/main/asciidoc/release_notes.asciidoc index 3e500df6..11bafb6e 100644 --- a/spec/src/main/asciidoc/release_notes.asciidoc +++ b/spec/src/main/asciidoc/release_notes.asciidoc @@ -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 diff --git a/tck/src/main/java/org/eclipse/microprofile/openapi/apps/beanvalidation/BeanValidationData.java b/tck/src/main/java/org/eclipse/microprofile/openapi/apps/beanvalidation/BeanValidationData.java index e5fa143e..fcf290d2 100644 --- a/tck/src/main/java/org/eclipse/microprofile/openapi/apps/beanvalidation/BeanValidationData.java +++ b/tck/src/main/java/org/eclipse/microprofile/openapi/apps/beanvalidation/BeanValidationData.java @@ -16,6 +16,7 @@ package org.eclipse.microprofile.openapi.apps.beanvalidation; import java.math.BigDecimal; +import java.math.BigInteger; import java.util.List; import java.util.Map; @@ -23,6 +24,7 @@ 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; @@ -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; @@ -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; } diff --git a/tck/src/main/java/org/eclipse/microprofile/openapi/tck/beanvalidation/BeanValidationTest.java b/tck/src/main/java/org/eclipse/microprofile/openapi/tck/beanvalidation/BeanValidationTest.java index 396f3943..6e121d5f 100644 --- a/tck/src/main/java/org/eclipse/microprofile/openapi/tck/beanvalidation/BeanValidationTest.java +++ b/tck/src/main/java/org/eclipse/microprofile/openapi/tck/beanvalidation/BeanValidationTest.java @@ -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; @@ -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") @@ -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); diff --git a/tck/src/main/java/org/eclipse/microprofile/openapi/tck/utils/TCKMatchers.java b/tck/src/main/java/org/eclipse/microprofile/openapi/tck/utils/TCKMatchers.java index 977aee30..8e877742 100644 --- a/tck/src/main/java/org/eclipse/microprofile/openapi/tck/utils/TCKMatchers.java +++ b/tck/src/main/java/org/eclipse/microprofile/openapi/tck/utils/TCKMatchers.java @@ -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; @@ -41,10 +42,18 @@ private TCKMatchers() { /** * Compares two numbers as BigDecimals */ - private static final Comparator 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 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); + } }; /** @@ -192,4 +201,35 @@ public static Matcher 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 patternMatchesValue(String value) { + return new PatternMatchable(value); + } + + private static class PatternMatchable extends TypeSafeDiagnosingMatcher { + 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(); + } + } }