Skip to content

Commit 2ad8d22

Browse files
committed
introducing SchemaExtractors
Re-working `SchemaLoader#loadSchemaObject()` to handle a list of extractors, which are able to extract a Schema.Builder. The conceptual change here is that previously the `#loadSchemaObject()` method was only able to create a single Schema.Builder instance out of a JSON object, which caused problems. Now multiple independent extractors can create Schema.Builder instances at the same nesting level. Also, even a single extactor can extract multiple schema builders. If there are more than one schemas extracted by the extactors, then `#loadSchemaObject()` will wrap these with a "synthetic" `"allOf"` schema as the returned schema builder.
1 parent 4e85cb1 commit 2ad8d22

File tree

7 files changed

+156
-79
lines changed

7 files changed

+156
-79
lines changed
Lines changed: 14 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,22 @@
11
package org.everit.json.schema.loader;
22

3-
import static java.lang.String.format;
4-
import static java.util.Arrays.asList;
53
import static java.util.Objects.requireNonNull;
4+
import static java.util.stream.Collectors.toList;
65

76
import java.util.ArrayList;
87
import java.util.Collection;
98
import java.util.HashMap;
109
import java.util.List;
1110
import java.util.Map;
12-
import java.util.Optional;
1311
import java.util.function.Function;
14-
import java.util.stream.Collectors;
1512

1613
import org.everit.json.schema.CombinedSchema;
1714
import org.everit.json.schema.Schema;
1815

1916
/**
2017
* @author erosb
2118
*/
22-
class CombinedSchemaLoader {
19+
class CombinedSchemaLoader implements SchemaExtractor {
2320

2421
/**
2522
* Alias for {@code Function<Collection<Schema>, CombinedSchema.Builder>}.
@@ -38,45 +35,25 @@ private interface CombinedSchemaProvider
3835
COMB_SCHEMA_PROVIDERS.put("oneOf", CombinedSchema::oneOf);
3936
}
4037

41-
private final LoadingState ls;
42-
4338
private final SchemaLoader defaultLoader;
4439

45-
public CombinedSchemaLoader(LoadingState ls, SchemaLoader defaultLoader) {
46-
this.ls = requireNonNull(ls, "ls cannot be null");
40+
public CombinedSchemaLoader(SchemaLoader defaultLoader) {
4741
this.defaultLoader = requireNonNull(defaultLoader, "defaultLoader cannot be null");
4842
}
4943

50-
public Optional<Schema.Builder<?>> load() {
44+
@Override
45+
public Collection<Schema.Builder<?>> extract(LoadingState ls) {
5146
List<String> presentKeys = COMB_SCHEMA_PROVIDERS.keySet().stream()
5247
.filter(ls.schemaJson()::containsKey)
53-
.collect(Collectors.toList());
54-
if (presentKeys.size() > 1) {
55-
throw ls.createSchemaException(format("expected at most 1 of 'allOf', 'anyOf', 'oneOf', %d found", presentKeys.size()));
56-
} else if (presentKeys.size() == 1) {
57-
String key = presentKeys.get(0);
58-
Collection<Schema> subschemas = new ArrayList<>();
59-
ls.schemaJson().require(key).requireArray()
60-
.forEach((i, subschema) -> {
61-
subschemas.add(defaultLoader.loadChild(subschema).build());
62-
});
63-
CombinedSchema.Builder combinedSchema = COMB_SCHEMA_PROVIDERS.get(key).apply(
64-
subschemas);
65-
Schema.Builder<?> baseSchema;
66-
if (ls.schemaJson().containsKey("type")) {
67-
baseSchema = defaultLoader.loadForType(ls.schemaJson().require("type"));
68-
} else {
69-
baseSchema = defaultLoader.sniffSchemaByProps();
70-
}
71-
if (baseSchema == null) {
72-
return Optional.of(combinedSchema);
73-
} else {
74-
return Optional.of(CombinedSchema.allOf(asList(baseSchema.build(),
75-
combinedSchema.build())));
76-
}
77-
} else {
78-
return Optional.empty();
79-
}
48+
.collect(toList());
49+
return presentKeys.stream().map(key -> loadCombinedSchemaForKeyword(ls, key)).collect(toList());
50+
}
51+
52+
private CombinedSchema.Builder loadCombinedSchemaForKeyword(LoadingState ls, String key) {
53+
Collection<Schema> subschemas = new ArrayList<>();
54+
ls.schemaJson().require(key).requireArray()
55+
.forEach((i, subschema) -> subschemas.add(defaultLoader.loadChild(subschema).build()));
56+
return COMB_SCHEMA_PROVIDERS.get(key).apply(subschemas);
8057
}
8158

8259
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package org.everit.json.schema.loader;
2+
3+
import static java.util.Arrays.asList;
4+
import static java.util.Collections.emptyList;
5+
6+
import java.util.Collection;
7+
import java.util.HashSet;
8+
import java.util.Set;
9+
10+
import org.everit.json.schema.EnumSchema;
11+
import org.everit.json.schema.Schema;
12+
13+
interface SchemaExtractor {
14+
15+
Collection<Schema.Builder<?>> extract(LoadingState ls);
16+
17+
}
18+
19+
class EnumSchemaExtractor implements SchemaExtractor {
20+
21+
@Override public Collection<Schema.Builder<?>> extract(LoadingState ls) {
22+
if (!ls.schemaJson().containsKey("enum")) {
23+
return emptyList();
24+
}
25+
EnumSchema.Builder builder = EnumSchema.builder();
26+
Set<Object> possibleValues = new HashSet<>();
27+
ls.schemaJson().require("enum").requireArray().forEach((i, item) -> possibleValues.add(item.unwrap()));
28+
builder.possibleValues(possibleValues);
29+
return asList(builder);
30+
}
31+
32+
}

core/src/main/java/org/everit/json/schema/loader/SchemaLoader.java

Lines changed: 37 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import static java.util.Arrays.asList;
55
import static java.util.Collections.emptyList;
66
import static java.util.Objects.requireNonNull;
7+
import static java.util.stream.Collectors.toList;
78
import static org.everit.json.schema.loader.SpecificationVersion.DRAFT_4;
89
import static org.everit.json.schema.loader.SpecificationVersion.DRAFT_6;
910
import static org.everit.json.schema.loader.SpecificationVersion.DRAFT_7;
@@ -19,6 +20,7 @@
1920
import java.util.Objects;
2021
import java.util.Optional;
2122
import java.util.Set;
23+
import java.util.function.Supplier;
2224

2325
import org.everit.json.schema.ArraySchema;
2426
import org.everit.json.schema.BooleanSchema;
@@ -410,23 +412,44 @@ private Schema.Builder loadSchemaBoolean(Boolean rawBoolean) {
410412
}
411413

412414
private Schema.Builder loadSchemaObject(JsonObject o) {
415+
List<Schema.Builder> extractedSchemas = new ArrayList<>(1);
416+
List<SchemaExtractor> extractors = asList(new EnumSchemaExtractor(), new CombinedSchemaLoader(this));
417+
extractors.stream().map(extractor -> extractor.extract(ls)).forEach(extractedSchemas::addAll);
418+
//////////////////////
413419
Schema.Builder builder;
414-
if (ls.schemaJson().containsKey("enum")) {
415-
builder = buildEnumSchema();
416-
} else if (ls.schemaJson().containsKey("const") && (config.specVersion != DRAFT_4)) {
420+
if (ls.schemaJson().containsKey("const") && (config.specVersion != DRAFT_4)) {
417421
builder = buildConstSchema();
418422
} else {
419-
builder = new CombinedSchemaLoader(ls, this).load()
420-
.orElseGet(() -> {
421-
if (!ls.schemaJson().containsKey("type") || ls.schemaJson().containsKey("$ref")) {
422-
return buildSchemaWithoutExplicitType();
423-
} else {
424-
return loadForType(ls.schemaJson().require("type"));
425-
}
426-
});
427-
}
428-
loadCommonSchemaProperties(builder);
429-
return builder;
423+
Supplier<Schema.Builder> supplier = () -> {
424+
if (!ls.schemaJson().containsKey("type") || ls.schemaJson().containsKey("$ref")) {
425+
return buildSchemaWithoutExplicitType();
426+
} else {
427+
return loadForType(ls.schemaJson().require("type"));
428+
}
429+
};
430+
builder = supplier.get();
431+
}
432+
// loadCommonSchemaProperties(builder);
433+
//////////////////////
434+
if (!(builder instanceof EmptySchema.Builder)) {
435+
extractedSchemas.add(builder);
436+
}
437+
Schema.Builder effectiveReturnedSchema;
438+
if (extractedSchemas.isEmpty()) {
439+
effectiveReturnedSchema = EmptySchema.builder();
440+
} else if (extractedSchemas.size() == 1) {
441+
effectiveReturnedSchema = extractedSchemas.get(0);
442+
} else {
443+
Collection<Schema> built = extractedSchemas.stream()
444+
.map(Schema.Builder::build)
445+
.map(Schema.class::cast)
446+
.collect(toList());
447+
effectiveReturnedSchema = CombinedSchema.allOf(built);
448+
// combined
449+
}
450+
loadCommonSchemaProperties(effectiveReturnedSchema);
451+
return effectiveReturnedSchema;
452+
// return builder;
430453
}
431454

432455
private void loadCommonSchemaProperties(Schema.Builder builder) {

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

Lines changed: 24 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,25 @@
11
package org.everit.json.schema.loader;
22

3+
import static java.util.Arrays.asList;
4+
import static java.util.Collections.emptyList;
5+
import static java.util.Collections.emptyMap;
6+
import static java.util.Collections.singletonList;
7+
import static java.util.stream.Collectors.toList;
8+
import static org.junit.Assert.assertEquals;
9+
import static org.junit.Assert.assertTrue;
10+
11+
import java.util.HashSet;
12+
import java.util.Set;
13+
14+
import org.everit.json.schema.BooleanSchema;
315
import org.everit.json.schema.CombinedSchema;
416
import org.everit.json.schema.ResourceLoader;
517
import org.everit.json.schema.Schema;
6-
import org.everit.json.schema.SchemaException;
718
import org.everit.json.schema.StringSchema;
819
import org.json.JSONObject;
920
import org.junit.Assert;
1021
import org.junit.Test;
1122

12-
import static org.junit.Assert.assertEquals;
13-
import static org.junit.Assert.assertTrue;
14-
1523
/**
1624
* @author erosb
1725
*/
@@ -23,7 +31,6 @@ private static JSONObject get(final String schemaName) {
2331
return ALL_SCHEMAS.getJSONObject(schemaName);
2432
}
2533

26-
2734
@Test
2835
public void combinedSchemaLoading() {
2936
CombinedSchema actual = (CombinedSchema) SchemaLoader.load(get("combinedSchema"));
@@ -55,12 +62,18 @@ public void combinedSchemaWithMultipleBaseSchemas() {
5562
assertTrue(actual instanceof CombinedSchema);
5663
}
5764

58-
@Test public void multipleKeywordsFailure() {
59-
try {
60-
SchemaLoader.load(get("multipleKeywordsFailure"));
61-
} catch (SchemaException e) {
62-
assertEquals("#/properties/wrapper/items", e.getSchemaLocation());
63-
}
65+
@Test
66+
public void multipleCombinedSchemasAtTheSameNestingLevel() {
67+
SchemaLoader defaultLoader = SchemaLoader.builder().schemaJson(get("multipleKeywords")).build();
68+
JsonValue json = JsonValue.of(get("multipleKeywords"));
69+
new LoadingState(LoaderConfig.defaultV4Config(), emptyMap(), json, json, null, emptyList());
70+
CombinedSchemaLoader subject = new CombinedSchemaLoader(defaultLoader);
71+
Set<Schema> actual = new HashSet<>(subject.extract(json.ls).stream().map(builder -> builder.build()).collect(toList()));
72+
HashSet<CombinedSchema> expected = new HashSet<>(asList(
73+
CombinedSchema.allOf(singletonList(BooleanSchema.INSTANCE)).build(),
74+
CombinedSchema.anyOf(singletonList(StringSchema.builder().build())).build()
75+
));
76+
assertEquals(expected, actual);
6477
}
6578

6679
}

core/src/test/resources/org/everit/jsonvalidator/combinedtestschemas.json

Lines changed: 10 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -46,23 +46,16 @@
4646
}
4747
]
4848
},
49-
"multipleKeywordsFailure": {
50-
"properties": {
51-
"wrapper": {
52-
"type": "array",
53-
"items": {
54-
"allOf": [
55-
{
56-
"type": "boolean"
57-
}
58-
],
59-
"anyOf": [
60-
{
61-
"type": "string"
62-
}
63-
]
64-
}
49+
"multipleKeywords": {
50+
"allOf": [
51+
{
52+
"type": "boolean"
53+
}
54+
],
55+
"anyOf": [
56+
{
57+
"type": "string"
6558
}
66-
}
59+
]
6760
}
6861
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
{
2+
"title": "JSON schema for JSONPatch files",
3+
"$schema": "http://json-schema.org/draft-04/schema#",
4+
"type": "object",
5+
"properties": {
6+
"prop": {
7+
"allOf": [
8+
{
9+
"multipleOf": 2
10+
},
11+
{
12+
"multipleOf": 3
13+
}
14+
],
15+
"oneOf": [
16+
{
17+
"enum": [
18+
1,
19+
2
20+
],
21+
"minimum": 3
22+
},
23+
{
24+
"const": 1,
25+
"maximum": 0
26+
},
27+
{
28+
"not": {
29+
"const": 2
30+
},
31+
"const": 2
32+
}
33+
]
34+
}
35+
}
36+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"prop": 4
3+
}

0 commit comments

Comments
 (0)