Skip to content

Commit 5357ab0

Browse files
authored
Specify and add TCK test cases for @Digits constraint processing (microprofile#733)
* Specify and add TCK test cases for `@Digits` constraint processing Signed-off-by: Michael Edgar <michael@xlate.io> * Exclude `integer` types from `multipleOf = 1` specification and test Signed-off-by: Michael Edgar <michael@xlate.io> --------- Signed-off-by: Michael Edgar <michael@xlate.io>
1 parent 37f9846 commit 5357ab0

5 files changed

Lines changed: 233 additions & 11 deletions

File tree

spec/src/main/asciidoc/microprofile-openapi-spec.asciidoc

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -599,7 +599,7 @@ In some cases, additional schema restrictions can be inferred from Jakarta Bean
599599

600600
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:
601601

602-
* the annotation is applied to to an element for which a schema is generated and
602+
* the annotation is applied to an element for which a schema is generated and
603603
* the annotation and generated schema type are listed together in the table below and
604604
* the annotation has a `group` attribute which is empty or includes `jakarta.validation.groups.Default` and
605605
* 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
611611
| `@NotEmpty` | `array` | `minItems = 1`
612612
| `@NotEmpty` | `object` | `minProperties = 1`
613613
| `@NotBlank` | `string` | `pattern = \S`
614-
| `@Size(min = a, max = b)` | `string`
615-
| `minLength = a +
614+
| `@Size(min = a, max = b)` | `string` | `minLength = a +
616615
maxLenth = b`
617-
| `@Size(min = a, max = b)` | `array`
618-
| `minItems = a +
616+
| `@Size(min = a, max = b)` | `array` | `minItems = a +
619617
maxItems = b`
620-
| `@Size(min = a, max = b)` | `object`
621-
| `minProperties = a +
618+
| `@Size(min = a, max = b)` | `object` | `minProperties = a +
622619
maxProperties = b`
623620
| `@DecimalMax(value = a)` | `number` or `integer` | `maximum = a`
624621
| `@DecimalMax(value = a, inclusive = false)` | `number` or `integer` | `exclusiveMaximum = a`
625622
| `@DecimalMin(value = a)` | `number` or `integer` | `minimum = a`
626623
| `@DecimalMin(value = a, inclusive = false)` | `number` or `integer` | `exclusiveMinimum = a`
624+
| `@Digits(integer = <any>, fraction = f)` | `number` | `multipleOf = 1` when `fraction` is less than 1, else `multipleOf` equal to 10^-f
625+
| `@Digits(integer = i, fraction = f)` | `string` | `pattern` matching any string value that satisfies the constraint
627626
| `@Max(a)` | `number` or `integer` | `maximum = a`
628627
| `@Min(a)` | `number` or `integer` | `minimum = a`
629628
| `@Negative` | `number` or `integer` | `exclusiveMaximum = 0`

spec/src/main/asciidoc/release_notes.asciidoc

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,12 @@ A full list of changes delivered in the 4.2 release can be found at link:https:/
2929

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

32+
[[other_changes_42]]
33+
==== Other Changes
34+
35+
* Add processing of Jakarta Bean Validation `@Digits` annotation (https://github.com/eclipse/microprofile-open-api/issues/717[717])
36+
37+
3238
[[release_notes_41]]
3339
=== Release Notes for MicroProfile OpenAPI 4.1
3440

tck/src/main/java/org/eclipse/microprofile/openapi/apps/beanvalidation/BeanValidationData.java

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,15 @@
1616
package org.eclipse.microprofile.openapi.apps.beanvalidation;
1717

1818
import java.math.BigDecimal;
19+
import java.math.BigInteger;
1920
import java.util.List;
2021
import java.util.Map;
2122

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

2425
import jakarta.validation.constraints.DecimalMax;
2526
import jakarta.validation.constraints.DecimalMin;
27+
import jakarta.validation.constraints.Digits;
2628
import jakarta.validation.constraints.Max;
2729
import jakarta.validation.constraints.Min;
2830
import jakarta.validation.constraints.Negative;
@@ -69,6 +71,40 @@ public class BeanValidationData {
6971
@DecimalMin(value = "3.25", inclusive = false)
7072
private BigDecimal minDecimalExclusive;
7173

74+
@Digits(integer = 9, fraction = 0)
75+
private int digitsInt32;
76+
77+
@Digits(integer = 18, fraction = 0)
78+
private int digitsInt64;
79+
80+
@Digits(integer = 5, fraction = 3)
81+
private float digitsFloat32;
82+
83+
@Digits(integer = 10, fraction = 6)
84+
private double digitsFloat64;
85+
86+
@Digits(integer = 20, fraction = 10)
87+
private BigDecimal digitsDecimal;
88+
89+
@Digits(integer = 5, fraction = 0)
90+
private float digitsFloat32AsInteger;
91+
92+
@Digits(integer = 10, fraction = 0)
93+
private double digitsFloat64AsInteger;
94+
95+
@Digits(integer = 20, fraction = 0)
96+
private BigDecimal digitsDecimalAsInteger;
97+
98+
@Digits(integer = 20, fraction = 0)
99+
private BigInteger digitsInteger;
100+
101+
@Digits(integer = 20, fraction = 0)
102+
@Schema(multipleOf = 1000)
103+
private BigInteger digitsCustomInteger;
104+
105+
@Digits(integer = 10, fraction = 5)
106+
private String digitsString;
107+
72108
@Max(5)
73109
private int maxInt;
74110

@@ -185,6 +221,94 @@ public void setMinDecimalExclusive(BigDecimal minDecimalExclusive) {
185221
this.minDecimalExclusive = minDecimalExclusive;
186222
}
187223

224+
public int getDigitsInt32() {
225+
return digitsInt32;
226+
}
227+
228+
public void setDigitsInt32(int digitsInt32) {
229+
this.digitsInt32 = digitsInt32;
230+
}
231+
232+
public int getDigitsInt64() {
233+
return digitsInt64;
234+
}
235+
236+
public void setDigitsInt64(int digitsInt64) {
237+
this.digitsInt64 = digitsInt64;
238+
}
239+
240+
public float getDigitsFloat32() {
241+
return digitsFloat32;
242+
}
243+
244+
public void setDigitsFloat32(float digitsFloat32) {
245+
this.digitsFloat32 = digitsFloat32;
246+
}
247+
248+
public double getDigitsFloat64() {
249+
return digitsFloat64;
250+
}
251+
252+
public void setDigitsFloat64(double digitsFloat64) {
253+
this.digitsFloat64 = digitsFloat64;
254+
}
255+
256+
public BigDecimal getDigitsDecimal() {
257+
return digitsDecimal;
258+
}
259+
260+
public void setDigitsDecimal(BigDecimal digitsDecimal) {
261+
this.digitsDecimal = digitsDecimal;
262+
}
263+
264+
public float getDigitsFloat32AsInteger() {
265+
return digitsFloat32AsInteger;
266+
}
267+
268+
public void setDigitsFloat32AsInteger(float digitsFloat32AsInteger) {
269+
this.digitsFloat32AsInteger = digitsFloat32AsInteger;
270+
}
271+
272+
public double getDigitsFloat64AsInteger() {
273+
return digitsFloat64AsInteger;
274+
}
275+
276+
public void setDigitsFloat64AsInteger(double digitsFloat64AsInteger) {
277+
this.digitsFloat64AsInteger = digitsFloat64AsInteger;
278+
}
279+
280+
public BigDecimal getDigitsDecimalAsInteger() {
281+
return digitsDecimalAsInteger;
282+
}
283+
284+
public void setDigitsDecimalAsInteger(BigDecimal digitsDecimalAsInteger) {
285+
this.digitsDecimalAsInteger = digitsDecimalAsInteger;
286+
}
287+
288+
public BigInteger getDigitsInteger() {
289+
return digitsInteger;
290+
}
291+
292+
public void setDigitsInteger(BigInteger digitsInteger) {
293+
this.digitsInteger = digitsInteger;
294+
}
295+
296+
public BigInteger getDigitsCustomInteger() {
297+
return digitsCustomInteger;
298+
}
299+
300+
public void setDigitsCustomInteger(BigInteger digitsCustomInteger) {
301+
this.digitsCustomInteger = digitsCustomInteger;
302+
}
303+
304+
public String getDigitsString() {
305+
return digitsString;
306+
}
307+
308+
public void setDigitsString(String digitsString) {
309+
this.digitsString = digitsString;
310+
}
311+
188312
public int getMaxInt() {
189313
return maxInt;
190314
}

tck/src/main/java/org/eclipse/microprofile/openapi/tck/beanvalidation/BeanValidationTest.java

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,17 @@
1616
package org.eclipse.microprofile.openapi.tck.beanvalidation;
1717

1818
import static org.eclipse.microprofile.openapi.tck.Groups.BEAN_VALIDATION;
19+
import static org.eclipse.microprofile.openapi.tck.utils.TCKMatchers.comparesEqualToNumber;
1920
import static org.eclipse.microprofile.openapi.tck.utils.TCKMatchers.itemOrSingleton;
21+
import static org.eclipse.microprofile.openapi.tck.utils.TCKMatchers.patternMatchesValue;
22+
import static org.hamcrest.Matchers.either;
2023
import static org.hamcrest.Matchers.hasEntry;
2124
import static org.hamcrest.Matchers.hasKey;
2225
import static org.hamcrest.Matchers.is;
2326
import static org.hamcrest.Matchers.not;
2427

28+
import java.util.Arrays;
29+
2530
import org.eclipse.microprofile.openapi.apps.beanvalidation.BeanValidationApp;
2631
import org.eclipse.microprofile.openapi.tck.AppTestBase;
2732
import org.hamcrest.Matcher;
@@ -34,6 +39,8 @@
3439

3540
public class BeanValidationTest extends AppTestBase {
3641

42+
private static final String MULTIPLE_OF = "multipleOf";
43+
3744
@Deployment(testable = false)
3845
public static WebArchive buildApp() {
3946
return ShrinkWrap.create(WebArchive.class, "beanValidation.war")
@@ -181,6 +188,52 @@ public void defaultAndOtherGroupsTest(String format) {
181188
assertProperty(vr, "defaultAndOtherGroups", hasEntry("minLength", 1));
182189
}
183190

191+
@SuppressWarnings("unchecked")
192+
@Test(dataProvider = "formatProvider", groups = BEAN_VALIDATION)
193+
public <T extends Object> void integerDigitsTest(String format) {
194+
Matcher<T> isInteger = (Matcher<T>) hasEntry(is("type"), itemOrSingleton("integer"));
195+
Matcher<T> isMultipleOfOne = (Matcher<T>) hasEntry(is(MULTIPLE_OF), comparesEqualToNumber(1));
196+
197+
ValidatableResponse vr = callEndpoint(format);
198+
199+
for (String property : Arrays.asList(
200+
"digitsInt32",
201+
"digitsInt64",
202+
"digitsInteger",
203+
"digitsFloat32AsInteger",
204+
"digitsFloat64AsInteger",
205+
"digitsDecimalAsInteger")) {
206+
assertProperty(vr, property, either(isInteger).or(isMultipleOfOne));
207+
}
208+
}
209+
210+
@Test(dataProvider = "formatProvider", groups = BEAN_VALIDATION)
211+
public void decimalDigitsTest(String format) {
212+
ValidatableResponse vr = callEndpoint(format);
213+
assertProperty(vr, "digitsFloat32", hasEntry(is(MULTIPLE_OF), comparesEqualToNumber(0.001)));
214+
assertProperty(vr, "digitsFloat64", hasEntry(is(MULTIPLE_OF), comparesEqualToNumber(0.000001)));
215+
assertProperty(vr, "digitsDecimal", hasEntry(is(MULTIPLE_OF), comparesEqualToNumber(0.0000000001)));
216+
}
217+
218+
@Test(dataProvider = "formatProvider", groups = BEAN_VALIDATION)
219+
public void customIntegerDigitsTest(String format) {
220+
ValidatableResponse vr = callEndpoint(format);
221+
assertProperty(vr, "digitsCustomInteger", hasEntry(is(MULTIPLE_OF), comparesEqualToNumber(1000)));
222+
}
223+
224+
@Test(dataProvider = "formatProvider", groups = BEAN_VALIDATION)
225+
public void stringDigitsTest(String format) {
226+
final String patternAttribute = "digitsString.pattern";
227+
ValidatableResponse vr = callEndpoint(format);
228+
// Constraint allows up to 10 integer digits and up to 5 fractional digits
229+
assertProperty(vr, patternAttribute, patternMatchesValue("1"));
230+
assertProperty(vr, patternAttribute, patternMatchesValue("1.5"));
231+
assertProperty(vr, patternAttribute, patternMatchesValue("123456789.1234"));
232+
assertProperty(vr, patternAttribute, patternMatchesValue("1234567890.12345"));
233+
assertProperty(vr, patternAttribute, not(patternMatchesValue("12345678901.12345")));
234+
assertProperty(vr, patternAttribute, not(patternMatchesValue("1234567890.123456")));
235+
}
236+
184237
@Test(dataProvider = "formatProvider", groups = BEAN_VALIDATION)
185238
public void parameterTest(String format) {
186239
ValidatableResponse vr = callEndpoint(format);

tck/src/main/java/org/eclipse/microprofile/openapi/tck/utils/TCKMatchers.java

Lines changed: 44 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
import java.math.BigInteger;
2929
import java.util.Collection;
3030
import java.util.Comparator;
31+
import java.util.regex.Pattern;
3132

3233
import org.hamcrest.Description;
3334
import org.hamcrest.Matcher;
@@ -41,10 +42,18 @@ private TCKMatchers() {
4142
/**
4243
* Compares two numbers as BigDecimals
4344
*/
44-
private static final Comparator<Number> NUMERIC_COMPARATOR = (value1, value2) -> {
45-
final BigDecimal decimal1 = BigDecimal.valueOf(value1.doubleValue());
46-
final BigDecimal decimal2 = BigDecimal.valueOf(value2.doubleValue());
47-
return decimal1.compareTo(decimal2);
45+
private static final Comparator<Number> NUMERIC_COMPARATOR = new Comparator<>() {
46+
@Override
47+
public String toString() {
48+
return getClass().getName();
49+
}
50+
51+
@Override
52+
public int compare(Number value1, Number value2) {
53+
final BigDecimal decimal1 = new BigDecimal(value1.toString());
54+
final BigDecimal decimal2 = new BigDecimal(value2.toString());
55+
return decimal1.compareTo(decimal2);
56+
}
4857
};
4958

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

193202
return allOf(isA(java.util.Map.class), either(hasEntry).or(entryMissing));
194203
}
204+
205+
/**
206+
* Creates a matcher which matches when the given value matches a string compiled to a regular expression pattern.
207+
*
208+
* @param value
209+
* a value to match against the regular expression
210+
* @return the matcher
211+
*/
212+
public static Matcher<String> patternMatchesValue(String value) {
213+
return new PatternMatchable(value);
214+
}
215+
216+
private static class PatternMatchable extends TypeSafeDiagnosingMatcher<String> {
217+
String value;
218+
219+
PatternMatchable(String value) {
220+
this.value = value;
221+
}
222+
223+
@Override
224+
public void describeTo(Description desc) {
225+
desc.appendText("A pattern matching string ").appendValue(value);
226+
}
227+
228+
@Override
229+
protected boolean matchesSafely(String item, Description mismatchDescription) {
230+
Pattern pattern = Pattern.compile(item);
231+
mismatchDescription.appendText("pattern was: ").appendValue(pattern);
232+
return pattern.matcher(this.value).matches();
233+
}
234+
}
195235
}

0 commit comments

Comments
 (0)