Skip to content

Commit 084a07a

Browse files
author
Bence Eros
authored
ArraySchema#definesProperty() (#443)
1 parent 7fa625e commit 084a07a

File tree

4 files changed

+258
-18
lines changed

4 files changed

+258
-18
lines changed

core/src/main/java/org/everit/json/schema/ArraySchema.java

Lines changed: 81 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,18 +5,21 @@
55
import java.util.ArrayList;
66
import java.util.List;
77
import java.util.Objects;
8+
import java.util.stream.Collectors;
89

910
import org.everit.json.schema.internal.JSONPrinter;
1011

1112
/**
1213
* Array schema validator.
1314
*/
14-
public class ArraySchema extends Schema {
15+
public class ArraySchema
16+
extends Schema {
1517

1618
/**
1719
* Builder class for {@link ArraySchema}.
1820
*/
19-
public static class Builder extends Schema.Builder<ArraySchema> {
21+
public static class Builder
22+
extends Schema.Builder<ArraySchema> {
2023

2124
private boolean requiresArray = true;
2225

@@ -42,8 +45,7 @@ public static class Builder extends Schema.Builder<ArraySchema> {
4245
* {@code addItemSchema()} invocation defines the expected schema of the {n}th item of the array
4346
* being validated.
4447
*
45-
* @param itemSchema
46-
* the schema of the next item.
48+
* @param itemSchema the schema of the next item.
4749
* @return this
4850
*/
4951
public Builder addItemSchema(final Schema itemSchema) {
@@ -125,8 +127,7 @@ public static Builder builder() {
125127
/**
126128
* Constructor.
127129
*
128-
* @param builder
129-
* contains validation criteria.
130+
* @param builder contains validation criteria.
130131
*/
131132
public ArraySchema(final Builder builder) {
132133
super(builder);
@@ -207,10 +208,83 @@ public boolean equals(final Object o) {
207208
}
208209
}
209210

210-
@Override void accept(Visitor visitor) {
211+
@Override
212+
void accept(Visitor visitor) {
211213
visitor.visitArraySchema(this);
212214
}
213215

216+
@Override
217+
public boolean definesProperty(String field) {
218+
String[] headAndTail = headAndTailOfJsonPointerFragment(field);
219+
String nextToken = headAndTail[0];
220+
String remaining = headAndTail[1];
221+
boolean hasRemaining = remaining != null;
222+
try {
223+
return tryPropertyDefinitionByNumericIndex(nextToken, remaining, hasRemaining);
224+
} catch (NumberFormatException e) {
225+
return tryPropertyDefinitionByMetaIndex(nextToken, remaining, hasRemaining);
226+
}
227+
}
228+
229+
private boolean tryPropertyDefinitionByMetaIndex(String nextToken, String remaining, boolean hasRemaining) {
230+
boolean isAll = "all".equals(nextToken);
231+
boolean isAny = "any".equals(nextToken);
232+
if (!hasRemaining && (isAll || isAny)) {
233+
return true;
234+
}
235+
if (isAll) {
236+
if (allItemSchema != null) {
237+
return allItemSchema.definesProperty(remaining);
238+
} else {
239+
boolean allItemSchemasDefine = itemSchemas.stream()
240+
.map(schema -> schema.definesProperty(remaining))
241+
.reduce(true, Boolean::logicalAnd);
242+
if (allItemSchemasDefine) {
243+
if (schemaOfAdditionalItems != null) {
244+
return schemaOfAdditionalItems.definesProperty(remaining);
245+
} else {
246+
return true;
247+
}
248+
}
249+
return false;
250+
}
251+
} else if (isAny) {
252+
if (allItemSchema != null) {
253+
return allItemSchema.definesProperty(remaining);
254+
} else {
255+
boolean anyItemSchemasDefine = itemSchemas.stream()
256+
.map(schema -> schema.definesProperty(remaining))
257+
.reduce(false, Boolean::logicalOr);
258+
return anyItemSchemasDefine
259+
|| (schemaOfAdditionalItems == null || schemaOfAdditionalItems.definesProperty(remaining));
260+
}
261+
}
262+
return false;
263+
}
264+
265+
private boolean tryPropertyDefinitionByNumericIndex(String nextToken, String remaining, boolean hasRemaining) {
266+
int index = Integer.parseInt(nextToken);
267+
if (index < 0) {
268+
return false;
269+
}
270+
if (maxItems != null && maxItems <= index) {
271+
return false;
272+
}
273+
if (allItemSchema != null && hasRemaining) {
274+
return allItemSchema.definesProperty(remaining);
275+
} else {
276+
if (hasRemaining) {
277+
if (index < itemSchemas.size()) {
278+
return itemSchemas.get(index).definesProperty(remaining);
279+
}
280+
if (schemaOfAdditionalItems != null) {
281+
return schemaOfAdditionalItems.definesProperty(remaining);
282+
}
283+
}
284+
return additionalItems;
285+
}
286+
}
287+
214288
@Override
215289
protected boolean canEqual(final Object other) {
216290
return other instanceof ArraySchema;

core/src/main/java/org/everit/json/schema/ObjectSchema.java

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -284,18 +284,14 @@ boolean hasDefaultProperty() {
284284
return oneOrMoreDefaultProperty;
285285
}
286286

287+
288+
287289
@Override
288290
public boolean definesProperty(String field) {
289-
field = field.replaceFirst("^#", "").replaceFirst("^/", "");
290-
int firstSlashIdx = field.indexOf('/');
291-
String nextToken, remaining;
292-
if (firstSlashIdx == -1) {
293-
nextToken = field;
294-
remaining = null;
295-
} else {
296-
nextToken = field.substring(0, firstSlashIdx);
297-
remaining = field.substring(firstSlashIdx + 1);
298-
}
291+
String[] headAndTail = headAndTailOfJsonPointerFragment(field);
292+
String nextToken = headAndTail[0];
293+
String remaining = headAndTail[1];
294+
field = headAndTail[2];
299295
return !field.isEmpty() && (definesSchemaProperty(nextToken, remaining)
300296
|| definesPatternProperty(nextToken, remaining)
301297
|| definesSchemaDependencyProperty(field));

core/src/main/java/org/everit/json/schema/Schema.java

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -167,7 +167,19 @@ public void validate(Object subject) {
167167
* "d" : {
168168
* "type" : "object",
169169
* "properties" : {
170-
* "rectangle" : {"$ref" : "#/definitions/Rectangle" }
170+
* "rectangle" : {
171+
* "$ref" : "#/definitions/Rectangle"
172+
* },
173+
* "list": {
174+
* "type": "array",
175+
* "items": {
176+
* "properties": {
177+
* "prop": {}
178+
* }
179+
* },
180+
* "minItems": 2,
181+
* "maxItems: 3
182+
* }
171183
* }
172184
* }
173185
* },
@@ -187,6 +199,18 @@ public void validate(Object subject) {
187199
* }
188200
* </code>
189201
* </pre>
202+
*
203+
* You can also check if a subschema of an array defines a property. In that case, to traverse the array, you can either use
204+
* an integer array index, or the {@code "all"} or {@code "any"} meta-indexes. For example, in the above schema
205+
* <ul>
206+
* <li>{@code definesProperty("#/list/any/prop")} returns {@code true}</li>
207+
* <li>{@code definesProperty("#/list/all/prop")} returns {@code true}</li>
208+
* <li>{@code definesProperty("#/list/1/prop")} returns {@code true}</li>
209+
* <li>{@code definesProperty("#/list/1/nonexistent")} returns {@code false} (the property is not present in the
210+
* subschema)</li>
211+
* <li>{@code definesProperty("#/list/8/prop")} returns {@code false} (the {@code "list"} does not define
212+
* property {@code 8}, since {@code "maxItems"} is {@code 3})</li>
213+
* </ul>
190214
* The default implementation of this method always returns false.
191215
*
192216
* @param field
@@ -198,6 +222,26 @@ public boolean definesProperty(String field) {
198222
return false;
199223
}
200224

225+
/**
226+
* Shared method for {@link #definesProperty(String)} implementations.
227+
*
228+
* @param pointer
229+
* @return
230+
*/
231+
String[] headAndTailOfJsonPointerFragment(String pointer) {
232+
String field = pointer.replaceFirst("^#", "").replaceFirst("^/", "");
233+
int firstSlashIdx = field.indexOf('/');
234+
String nextToken, remaining;
235+
if (firstSlashIdx == -1) {
236+
nextToken = field;
237+
remaining = null;
238+
} else {
239+
nextToken = field.substring(0, firstSlashIdx);
240+
remaining = field.substring(firstSlashIdx + 1);
241+
}
242+
return new String[]{nextToken, remaining, field};
243+
}
244+
201245
@Override
202246
public boolean equals(Object o) {
203247
if (this == o) {

core/src/test/java/org/everit/json/schema/loader/DefinesPropertyTest.java

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,10 @@
33
import static org.junit.jupiter.api.Assertions.assertFalse;
44
import static org.junit.jupiter.api.Assertions.assertTrue;
55

6+
import org.everit.json.schema.ArraySchema;
67
import org.everit.json.schema.BooleanSchema;
78
import org.everit.json.schema.CombinedSchema;
9+
import org.everit.json.schema.NullSchema;
810
import org.everit.json.schema.ObjectSchema;
911
import org.everit.json.schema.ResourceLoader;
1012
import org.everit.json.schema.Schema;
@@ -20,6 +22,36 @@ private JSONObject get(final String schemaName) {
2022
return ALL_SCHEMAS.getJSONObject(schemaName);
2123
}
2224

25+
private static final ArraySchema TUPLE_SCHEMA = ArraySchema.builder()
26+
.minItems(2).maxItems(6)
27+
.addItemSchema(ObjectSchema.builder()
28+
.addPropertySchema("sub1prop", BooleanSchema.INSTANCE)
29+
.addPropertySchema("commonprop", BooleanSchema.INSTANCE)
30+
.build())
31+
.addItemSchema(ObjectSchema.builder()
32+
.addPropertySchema("sub2prop", BooleanSchema.INSTANCE)
33+
.addPropertySchema("commonprop", BooleanSchema.INSTANCE)
34+
.build())
35+
.schemaOfAdditionalItems(ObjectSchema.builder()
36+
.addPropertySchema("sub3prop", BooleanSchema.INSTANCE)
37+
.addPropertySchema("commonprop", BooleanSchema.INSTANCE)
38+
.build())
39+
.build();
40+
41+
private static final ArraySchema TUPLE_WITHOUT_ADDITIONAL = ArraySchema.builder()
42+
.addItemSchema(BooleanSchema.INSTANCE)
43+
.addItemSchema(NullSchema.INSTANCE)
44+
.additionalItems(false)
45+
.maxItems(10)
46+
.build();
47+
48+
private static final ArraySchema ARRAY_SCHEMA = ArraySchema.builder()
49+
.minItems(2).maxItems(6)
50+
.allItemSchema(ObjectSchema.builder()
51+
.addPropertySchema("prop", BooleanSchema.INSTANCE)
52+
.build())
53+
.build();
54+
2355
@Test
2456
public void objectSchemaHasField() {
2557
ObjectSchema actual = (ObjectSchema) SchemaLoader.load(get("pointerResolution"));
@@ -120,4 +152,98 @@ public void patternPropsAndSchemaDefs() {
120152
// Assert.assertTrue(actual.definesProperty("#/1stLevel/2ndLevel/3rdLevel/4thLevel"));
121153
}
122154

155+
@Test
156+
void tupleSchema_definesIndex() {
157+
assertTrue(TUPLE_SCHEMA.definesProperty("#/0"));
158+
}
159+
160+
@Test
161+
void tupleSchema_definesAdditionalIndex() {
162+
assertTrue(TUPLE_SCHEMA.definesProperty("#/2"));
163+
}
164+
165+
@Test
166+
void tupleSchema_doesNotDefine_IndexGreaterThan_maxItems() {
167+
assertFalse(TUPLE_SCHEMA.definesProperty("#/6"));
168+
}
169+
170+
@Test
171+
void tupleSchema_defines_index_tupleSubschema() {
172+
assertTrue(TUPLE_SCHEMA.definesProperty("#/0/sub1prop"));
173+
assertTrue(TUPLE_SCHEMA.definesProperty("#/1/sub2prop"));
174+
}
175+
176+
@Test
177+
void tupleSchema_defines_index_doesNotDefine_subschemaProp() {
178+
assertFalse(TUPLE_SCHEMA.definesProperty("#/0/nonexistent"));
179+
assertFalse(TUPLE_SCHEMA.definesProperty("#/5/nonexistent"));
180+
}
181+
182+
@Test
183+
void tupleSchema_doesNotDefine_negativeIndex() {
184+
assertFalse(TUPLE_SCHEMA.definesProperty("#/-1"));
185+
}
186+
187+
@Test
188+
void tupleSchema_additionalPropsFalse_doesNotDefine() {
189+
assertFalse(TUPLE_WITHOUT_ADDITIONAL.definesProperty("#/8"));
190+
}
191+
192+
@Test
193+
void tupleSchema_additionalPropsFalse_definesIndex_inBound() {
194+
assertFalse(TUPLE_WITHOUT_ADDITIONAL.definesProperty("#/0"));
195+
}
196+
197+
@Test
198+
void arraySchema_definesIndex_inBound() {
199+
assertTrue(ARRAY_SCHEMA.definesProperty("#/5/prop"));
200+
}
201+
202+
@Test
203+
void arraySchema_doesNotDefineIndex_greaterThan_maxLength() {
204+
assertFalse(ARRAY_SCHEMA.definesProperty("#/10"));
205+
}
206+
207+
@Test
208+
void arraySchema_definesIndex_noRemaining(){
209+
assertTrue(ARRAY_SCHEMA.definesProperty("#/5"));
210+
}
211+
212+
@Test
213+
void arraySchema_nonNumericIndex(){
214+
assertFalse(ARRAY_SCHEMA.definesProperty("#/prop"));
215+
}
216+
217+
@Test
218+
void arraySchema_floatIndex() {
219+
assertFalse(ARRAY_SCHEMA.definesProperty("#/12.34"));
220+
}
221+
222+
@Test
223+
void arraySchema_all_definesProperty() {
224+
assertTrue(ARRAY_SCHEMA.definesProperty("#/all"));
225+
assertTrue(ARRAY_SCHEMA.definesProperty("#/all/prop"));
226+
assertFalse(ARRAY_SCHEMA.definesProperty("#/all/nonexistent"));
227+
}
228+
229+
@Test
230+
void arraySchema_any_definesProperty() {
231+
assertTrue(ARRAY_SCHEMA.definesProperty("#/any"));
232+
assertTrue(ARRAY_SCHEMA.definesProperty("#/any/prop"));
233+
}
234+
235+
@Test
236+
void tupleSchema_all() {
237+
assertTrue(TUPLE_SCHEMA.definesProperty("#/all"));
238+
assertFalse(TUPLE_SCHEMA.definesProperty("#/all/sub1prop"));
239+
assertTrue(TUPLE_SCHEMA.definesProperty("#/all/commonprop"));
240+
}
241+
242+
@Test
243+
void tupleSchema_any() {
244+
assertTrue(TUPLE_SCHEMA.definesProperty("#/any/sub1prop"));
245+
assertTrue(TUPLE_SCHEMA.definesProperty("#/any/sub2prop"));
246+
assertTrue(TUPLE_SCHEMA.definesProperty("#/any/sub3prop"));
247+
assertFalse(TUPLE_SCHEMA.definesProperty("#/any/nonexistent"));
248+
}
123249
}

0 commit comments

Comments
 (0)