Skip to content

Commit 4c3f158

Browse files
committed
feat(validate): String/integer/number validation with full 3.1 numeric keywords
1 parent 2ff1fba commit 4c3f158

2 files changed

Lines changed: 214 additions & 4 deletions

File tree

src/main/java/com/retailsvc/http/validate/DefaultValidator.java

Lines changed: 95 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,12 @@
1717
import com.retailsvc.http.spec.schema.Schema;
1818
import com.retailsvc.http.spec.schema.StringSchema;
1919
import com.retailsvc.http.spec.schema.TypeName;
20+
import java.time.LocalDate;
21+
import java.time.OffsetDateTime;
2022
import java.util.Objects;
23+
import java.util.UUID;
2124
import java.util.function.Function;
25+
import java.util.regex.Pattern;
2226

2327
public final class DefaultValidator implements Validator {
2428
private final Function<String, Schema> refResolver;
@@ -56,23 +60,110 @@ private void validateBoolean(Object value, String pointer) {
5660
}
5761

5862
private void validateString(Object value, StringSchema s, String pointer) {
59-
throw new UnsupportedOperationException("E3 implements string");
63+
require(value instanceof String, pointer, "type", "expected string");
64+
String str = (String) value;
65+
if (s.minLength() != null && str.length() < s.minLength())
66+
fail(pointer, "minLength", "string shorter than " + s.minLength(), str);
67+
if (s.maxLength() != null && str.length() > s.maxLength())
68+
fail(pointer, "maxLength", "string longer than " + s.maxLength(), str);
69+
if (s.pattern() != null && !Pattern.compile(s.pattern()).matcher(str).matches())
70+
fail(pointer, "pattern", "does not match pattern " + s.pattern(), str);
71+
if (s.enumValues() != null && !s.enumValues().contains(str))
72+
fail(pointer, "enum", "value not in enum", str);
73+
if (s.format() != null) validateStringFormat(str, s.format(), pointer);
74+
}
75+
76+
private void validateStringFormat(String str, String format, String pointer) {
77+
switch (format) {
78+
case "uuid" -> {
79+
try {
80+
UUID.fromString(str);
81+
} catch (IllegalArgumentException e) {
82+
fail(pointer, "format", "not a valid uuid", str);
83+
}
84+
}
85+
case "date" -> {
86+
try {
87+
LocalDate.parse(str);
88+
} catch (Exception e) {
89+
fail(pointer, "format", "not a valid date", str);
90+
}
91+
}
92+
case "date-time" -> {
93+
try {
94+
OffsetDateTime.parse(str);
95+
} catch (Exception e) {
96+
fail(pointer, "format", "not a valid date-time", str);
97+
}
98+
}
99+
default -> {}
100+
}
60101
}
61102

62103
private void validateInteger(Object value, IntegerSchema s, String pointer) {
63-
throw new UnsupportedOperationException("E3 implements integer");
104+
long n;
105+
if (value instanceof Number num) n = num.longValue();
106+
else if (value instanceof String str) {
107+
try {
108+
n = Long.parseLong(str);
109+
} catch (NumberFormatException e) {
110+
fail(pointer, "type", "expected integer", value);
111+
return;
112+
}
113+
} else {
114+
fail(pointer, "type", "expected integer", value);
115+
return;
116+
}
117+
118+
if (s.minimum() != null && n < s.minimum())
119+
fail(pointer, "minimum", "integer below minimum " + s.minimum(), n);
120+
if (s.maximum() != null && n > s.maximum())
121+
fail(pointer, "maximum", "integer above maximum " + s.maximum(), n);
122+
if (s.exclusiveMinimum() != null && n <= s.exclusiveMinimum())
123+
fail(pointer, "exclusiveMinimum", "integer not greater than " + s.exclusiveMinimum(), n);
124+
if (s.exclusiveMaximum() != null && n >= s.exclusiveMaximum())
125+
fail(pointer, "exclusiveMaximum", "integer not less than " + s.exclusiveMaximum(), n);
126+
if (s.multipleOf() != null && n % s.multipleOf() != 0)
127+
fail(pointer, "multipleOf", "not a multiple of " + s.multipleOf(), n);
64128
}
65129

66130
private void validateNumber(Object value, NumberSchema s, String pointer) {
67-
throw new UnsupportedOperationException("E3 implements number");
131+
double n;
132+
if (value instanceof Number num) n = num.doubleValue();
133+
else if (value instanceof String str) {
134+
try {
135+
n = Double.parseDouble(str);
136+
} catch (NumberFormatException e) {
137+
fail(pointer, "type", "expected number", value);
138+
return;
139+
}
140+
} else {
141+
fail(pointer, "type", "expected number", value);
142+
return;
143+
}
144+
145+
if (s.minimum() != null && n < s.minimum().doubleValue())
146+
fail(pointer, "minimum", "number below minimum " + s.minimum(), n);
147+
if (s.maximum() != null && n > s.maximum().doubleValue())
148+
fail(pointer, "maximum", "number above maximum " + s.maximum(), n);
149+
if (s.exclusiveMinimum() != null && n <= s.exclusiveMinimum().doubleValue())
150+
fail(pointer, "exclusiveMinimum", "number not greater than " + s.exclusiveMinimum(), n);
151+
if (s.exclusiveMaximum() != null && n >= s.exclusiveMaximum().doubleValue())
152+
fail(pointer, "exclusiveMaximum", "number not less than " + s.exclusiveMaximum(), n);
153+
if (s.multipleOf() != null && (n / s.multipleOf().doubleValue()) % 1 != 0)
154+
fail(pointer, "multipleOf", "not a multiple of " + s.multipleOf(), n);
68155
}
69156

70157
private void validateObject(Object value, ObjectSchema s, String pointer) {
71158
throw new UnsupportedOperationException("E4 implements object");
72159
}
73160

74161
private void validateArray(Object value, ArraySchema s, String pointer) {
75-
throw new UnsupportedOperationException("E4 implements array");
162+
throw new UnsupportedOperationException("E5 implements array");
163+
}
164+
165+
private static void fail(String pointer, String keyword, String message, Object rejectedValue) {
166+
throw new ValidationException(new ValidationError(pointer, keyword, message, rejectedValue));
76167
}
77168

78169
static void require(boolean condition, String pointer, String keyword, String message) {
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
package com.retailsvc.http.validate;
2+
3+
import static org.assertj.core.api.Assertions.assertThatCode;
4+
import static org.assertj.core.api.Assertions.assertThatThrownBy;
5+
6+
import com.retailsvc.http.ValidationException;
7+
import com.retailsvc.http.spec.schema.IntegerSchema;
8+
import com.retailsvc.http.spec.schema.NumberSchema;
9+
import com.retailsvc.http.spec.schema.StringSchema;
10+
import com.retailsvc.http.spec.schema.TypeName;
11+
import java.util.List;
12+
import java.util.Set;
13+
import java.util.UUID;
14+
import org.junit.jupiter.api.Test;
15+
16+
class StringIntegerNumberTest {
17+
private final Validator v =
18+
new DefaultValidator(
19+
name -> {
20+
throw new AssertionError();
21+
});
22+
23+
@Test
24+
void stringMinLength() {
25+
StringSchema s = new StringSchema(Set.of(TypeName.STRING), null, 3, null, null, null);
26+
assertThatCode(() -> v.validate("abc", s, "/v")).doesNotThrowAnyException();
27+
assertThatThrownBy(() -> v.validate("ab", s, "/v"))
28+
.isInstanceOf(ValidationException.class)
29+
.extracting(t -> ((ValidationException) t).error().keyword())
30+
.isEqualTo("minLength");
31+
}
32+
33+
@Test
34+
void stringMaxLength() {
35+
StringSchema s = new StringSchema(Set.of(TypeName.STRING), null, null, 5, null, null);
36+
assertThatThrownBy(() -> v.validate("abcdef", s, "/v"))
37+
.extracting(t -> ((ValidationException) t).error().keyword())
38+
.isEqualTo("maxLength");
39+
}
40+
41+
@Test
42+
void stringPattern() {
43+
StringSchema s = new StringSchema(Set.of(TypeName.STRING), "^[a-z]+$", null, null, null, null);
44+
assertThatCode(() -> v.validate("abc", s, "/v")).doesNotThrowAnyException();
45+
assertThatThrownBy(() -> v.validate("ABC", s, "/v"))
46+
.extracting(t -> ((ValidationException) t).error().keyword())
47+
.isEqualTo("pattern");
48+
}
49+
50+
@Test
51+
void stringEnum() {
52+
StringSchema s =
53+
new StringSchema(Set.of(TypeName.STRING), null, null, null, null, List.of("a", "b"));
54+
assertThatCode(() -> v.validate("a", s, "/v")).doesNotThrowAnyException();
55+
assertThatThrownBy(() -> v.validate("c", s, "/v"))
56+
.extracting(t -> ((ValidationException) t).error().keyword())
57+
.isEqualTo("enum");
58+
}
59+
60+
@Test
61+
void stringFormatUuid() {
62+
StringSchema s = new StringSchema(Set.of(TypeName.STRING), null, null, null, "uuid", null);
63+
assertThatCode(() -> v.validate(UUID.randomUUID().toString(), s, "/v"))
64+
.doesNotThrowAnyException();
65+
assertThatThrownBy(() -> v.validate("not-a-uuid", s, "/v"))
66+
.extracting(t -> ((ValidationException) t).error().keyword())
67+
.isEqualTo("format");
68+
}
69+
70+
@Test
71+
void integerWithMinMax() {
72+
IntegerSchema s =
73+
new IntegerSchema(Set.of(TypeName.INTEGER), 0L, 10L, null, null, null, "int32");
74+
assertThatCode(() -> v.validate(5, s, "/v")).doesNotThrowAnyException();
75+
assertThatThrownBy(() -> v.validate(-1, s, "/v"))
76+
.extracting(t -> ((ValidationException) t).error().keyword())
77+
.isEqualTo("minimum");
78+
assertThatThrownBy(() -> v.validate(11, s, "/v"))
79+
.extracting(t -> ((ValidationException) t).error().keyword())
80+
.isEqualTo("maximum");
81+
}
82+
83+
@Test
84+
void integerExclusiveBoundsBugFixedFromMaster() {
85+
// Master's Schema defaulted minimum to Double.MIN_VALUE (~4.9e-324) and silently rejected
86+
// negative numbers. New model uses null = no constraint.
87+
IntegerSchema s =
88+
new IntegerSchema(Set.of(TypeName.INTEGER), null, null, null, null, null, "int32");
89+
assertThatCode(() -> v.validate(-1_000_000, s, "/v")).doesNotThrowAnyException();
90+
}
91+
92+
@Test
93+
void integerMultipleOf() {
94+
IntegerSchema s =
95+
new IntegerSchema(Set.of(TypeName.INTEGER), null, null, null, null, 5L, "int32");
96+
assertThatCode(() -> v.validate(15, s, "/v")).doesNotThrowAnyException();
97+
assertThatThrownBy(() -> v.validate(7, s, "/v"))
98+
.extracting(t -> ((ValidationException) t).error().keyword())
99+
.isEqualTo("multipleOf");
100+
}
101+
102+
@Test
103+
void numberAcceptsDoublesAndIntegers() {
104+
NumberSchema s = new NumberSchema(Set.of(TypeName.NUMBER), 0, 1, null, null, null, "double");
105+
assertThatCode(() -> v.validate(0.5, s, "/v")).doesNotThrowAnyException();
106+
assertThatCode(() -> v.validate(1, s, "/v")).doesNotThrowAnyException();
107+
assertThatThrownBy(() -> v.validate(2.0, s, "/v"))
108+
.extracting(t -> ((ValidationException) t).error().keyword())
109+
.isEqualTo("maximum");
110+
}
111+
112+
@Test
113+
void stringRejectsNonString() {
114+
StringSchema s = new StringSchema(Set.of(TypeName.STRING), null, null, null, null, null);
115+
assertThatThrownBy(() -> v.validate(42, s, "/v"))
116+
.extracting(t -> ((ValidationException) t).error().keyword())
117+
.isEqualTo("type");
118+
}
119+
}

0 commit comments

Comments
 (0)