Skip to content

Commit 5f08f50

Browse files
thcedclaude
andcommitted
feat: Implement combinator validation in DefaultValidator
Replace the four UnsupportedOperationException branches for allOf / anyOf / oneOf / not with real validation. allOf propagates the first failing branch; anyOf short-circuits on first match; oneOf evaluates all branches and asserts exactly one matches; not inverts the inner result. Adds 9 unit tests covering pass/fail per combinator. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent c4f3a8c commit 5f08f50

2 files changed

Lines changed: 132 additions & 8 deletions

File tree

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

Lines changed: 48 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -64,10 +64,14 @@ case EnumSchema(List<Object> values) ->
6464
require(values.contains(value), pointer, "enum", "value not in enum");
6565
case ConstSchema(Object expected) ->
6666
require(Objects.equals(expected, value), pointer, "const", "value does not equal const");
67-
case OneOfSchema _ -> throw new UnsupportedOperationException("oneOf not yet supported");
68-
case AnyOfSchema _ -> throw new UnsupportedOperationException("anyOf not yet supported");
69-
case AllOfSchema _ -> throw new UnsupportedOperationException("allOf not yet supported");
70-
case NotSchema _ -> throw new UnsupportedOperationException("not not yet supported");
67+
case AllOfSchema(List<Schema> parts) -> {
68+
for (Schema p : parts) {
69+
validate(value, p, pointer);
70+
}
71+
}
72+
case AnyOfSchema(List<Schema> options) -> validateAnyOf(value, options, pointer);
73+
case OneOfSchema(List<Schema> options) -> validateOneOf(value, options, pointer);
74+
case NotSchema(Schema inner) -> validateNot(value, inner, pointer);
7175
}
7276
}
7377

@@ -290,4 +294,44 @@ static void require(boolean condition, String pointer, String keyword, String me
290294
throw new ValidationException(new ValidationError(pointer, keyword, message, null));
291295
}
292296
}
297+
298+
private void validateAnyOf(Object value, List<Schema> options, String pointer) {
299+
for (Schema o : options) {
300+
try {
301+
validate(value, o, pointer);
302+
return;
303+
} catch (ValidationException ignored) {
304+
// try next branch
305+
}
306+
}
307+
fail(pointer, "anyOf", "did not match any anyOf branch", value);
308+
}
309+
310+
private void validateOneOf(Object value, List<Schema> options, String pointer) {
311+
int matched = 0;
312+
for (Schema o : options) {
313+
try {
314+
validate(value, o, pointer);
315+
matched++;
316+
} catch (ValidationException ignored) {
317+
// count misses
318+
}
319+
}
320+
if (matched != 1) {
321+
fail(
322+
pointer,
323+
"oneOf",
324+
"matched " + matched + " of " + options.size() + " oneOf branches",
325+
value);
326+
}
327+
}
328+
329+
private void validateNot(Object value, Schema inner, String pointer) {
330+
try {
331+
validate(value, inner, pointer);
332+
} catch (ValidationException expected) {
333+
return;
334+
}
335+
fail(pointer, "not", "value matched 'not' schema", value);
336+
}
293337
}

src/test/java/com/retailsvc/http/validate/DefaultValidatorDispatchTest.java

Lines changed: 84 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,13 @@
33
import static org.assertj.core.api.Assertions.assertThatThrownBy;
44

55
import com.retailsvc.http.ValidationException;
6+
import com.retailsvc.http.spec.schema.AllOfSchema;
7+
import com.retailsvc.http.spec.schema.AnyOfSchema;
68
import com.retailsvc.http.spec.schema.BooleanSchema;
9+
import com.retailsvc.http.spec.schema.NotSchema;
710
import com.retailsvc.http.spec.schema.NullSchema;
811
import com.retailsvc.http.spec.schema.OneOfSchema;
12+
import com.retailsvc.http.spec.schema.StringSchema;
913
import com.retailsvc.http.spec.schema.TypeName;
1014
import java.util.List;
1115
import java.util.Set;
@@ -43,10 +47,86 @@ void booleanSchemaRejectsString() {
4347
assertThatThrownBy(() -> v.validate("x", schema, "/v")).isInstanceOf(ValidationException.class);
4448
}
4549

50+
private StringSchema stringSchema(Integer min, Integer max) {
51+
return new StringSchema(Set.of(TypeName.STRING), null, min, max, null, null);
52+
}
53+
4654
@Test
47-
void combinatorThrowsUnsupported() {
48-
var schema = new OneOfSchema(List.of());
49-
assertThatThrownBy(() -> v.validate("x", schema, "/v"))
50-
.isInstanceOf(UnsupportedOperationException.class);
55+
void allOfPassesWhenAllBranchesPass() {
56+
var schema = new AllOfSchema(List.of(stringSchema(1, null), stringSchema(null, 10)));
57+
v.validate("hello", schema, "/v");
58+
}
59+
60+
@Test
61+
void allOfPropagatesFirstFailingBranch() {
62+
var schema = new AllOfSchema(List.of(stringSchema(1, null), stringSchema(null, 3)));
63+
assertThatThrownBy(() -> v.validate("hello", schema, "/v"))
64+
.isInstanceOf(ValidationException.class)
65+
.extracting(t -> ((ValidationException) t).error().keyword())
66+
.isEqualTo("maxLength");
67+
}
68+
69+
@Test
70+
void anyOfPassesWhenOneBranchPasses() {
71+
var schema = new AnyOfSchema(List.of(stringSchema(100, null), stringSchema(null, 10)));
72+
v.validate("hello", schema, "/v");
73+
}
74+
75+
@Test
76+
void anyOfFailsWhenNoBranchMatches() {
77+
var schema = new AnyOfSchema(List.of(stringSchema(100, null), stringSchema(null, 2)));
78+
assertThatThrownBy(() -> v.validate("hello", schema, "/v"))
79+
.isInstanceOf(ValidationException.class)
80+
.extracting(t -> ((ValidationException) t).error().keyword())
81+
.isEqualTo("anyOf");
82+
}
83+
84+
@Test
85+
void oneOfPassesWhenExactlyOneBranchMatches() {
86+
// value "hello" — len 5. branch[0] requires min 100 (fails), branch[1] max 10 (passes).
87+
var schema = new OneOfSchema(List.of(stringSchema(100, null), stringSchema(null, 10)));
88+
v.validate("hello", schema, "/v");
89+
}
90+
91+
@Test
92+
void oneOfFailsWhenZeroBranchesMatch() {
93+
var schema = new OneOfSchema(List.of(stringSchema(100, null), stringSchema(null, 2)));
94+
assertThatThrownBy(() -> v.validate("hello", schema, "/v"))
95+
.isInstanceOf(ValidationException.class)
96+
.satisfies(
97+
t -> {
98+
var err = ((ValidationException) t).error();
99+
org.assertj.core.api.Assertions.assertThat(err.keyword()).isEqualTo("oneOf");
100+
org.assertj.core.api.Assertions.assertThat(err.message()).contains("matched 0 of 2");
101+
});
102+
}
103+
104+
@Test
105+
void oneOfFailsWhenTwoBranchesMatch() {
106+
// value "hello" — both branches accept.
107+
var schema = new OneOfSchema(List.of(stringSchema(null, 10), stringSchema(1, null)));
108+
assertThatThrownBy(() -> v.validate("hello", schema, "/v"))
109+
.isInstanceOf(ValidationException.class)
110+
.satisfies(
111+
t -> {
112+
var err = ((ValidationException) t).error();
113+
org.assertj.core.api.Assertions.assertThat(err.keyword()).isEqualTo("oneOf");
114+
org.assertj.core.api.Assertions.assertThat(err.message()).contains("matched 2 of 2");
115+
});
116+
}
117+
118+
@Test
119+
void notPassesWhenInnerFails() {
120+
var schema = new NotSchema(stringSchema(100, null));
121+
v.validate("hello", schema, "/v");
122+
}
123+
124+
@Test
125+
void notFailsWhenInnerPasses() {
126+
var schema = new NotSchema(stringSchema(null, 10));
127+
assertThatThrownBy(() -> v.validate("hello", schema, "/v"))
128+
.isInstanceOf(ValidationException.class)
129+
.extracting(t -> ((ValidationException) t).error().keyword())
130+
.isEqualTo("not");
51131
}
52132
}

0 commit comments

Comments
 (0)