Skip to content
This repository was archived by the owner on Aug 13, 2020. It is now read-only.

Commit 1043010

Browse files
authored
Merge pull request #424 from jasouyris/master
Added Schema Property Matcher
2 parents de1e1ca + b5a90e6 commit 1043010

File tree

3 files changed

+351
-0
lines changed

3 files changed

+351
-0
lines changed
Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
package uk.gov.justice.services.test.utils.core.matchers;
2+
3+
import com.google.common.base.Strings;
4+
import com.google.common.io.Files;
5+
import com.google.common.io.Resources;
6+
import org.everit.json.schema.ArraySchema;
7+
import org.everit.json.schema.ObjectSchema;
8+
import org.everit.json.schema.ReferenceSchema;
9+
import org.everit.json.schema.Schema;
10+
import org.everit.json.schema.loader.SchemaLoader;
11+
import org.hamcrest.Description;
12+
import org.hamcrest.Matcher;
13+
import org.hamcrest.TypeSafeMatcher;
14+
import org.json.JSONObject;
15+
import org.json.JSONTokener;
16+
17+
import java.io.File;
18+
import java.io.IOException;
19+
import java.nio.file.Paths;
20+
import java.util.Optional;
21+
22+
import static com.google.common.base.Charsets.UTF_8;
23+
import static java.lang.String.format;
24+
25+
public class JsonSchemaPropertyMatcher {
26+
27+
private static final String JSON_PATH_SEPARATOR = "\\.";
28+
29+
/**
30+
* Matcher to validate if a given property exists in the given json schema
31+
*
32+
* @param jsonpathToRequiredProperty json path of the property that should exist in the schema (i.e. case.offences.offenceId)
33+
* @return matcher
34+
*/
35+
public static Matcher<String> hasProperty(final String jsonpathToRequiredProperty) {
36+
37+
return new TypeSafeMatcher<String>() {
38+
private String pathToJsonFile = null;
39+
40+
@Override
41+
protected boolean matchesSafely(final String pathToJsonFile) {
42+
this.pathToJsonFile = pathToJsonFile;
43+
final JsonPath jsonPath = JsonPath.of(jsonpathToRequiredProperty);
44+
final ObjectSchema parentObjectSchema = getObjectSchemaFromFile(pathToJsonFile);
45+
return getObjectSchemaWithPath(parentObjectSchema, jsonPath).getPropertySchemas().containsKey(jsonPath.getProperty());
46+
}
47+
48+
@Override
49+
public void describeTo(final Description description) {
50+
description.appendText("property ").appendValue(jsonpathToRequiredProperty)
51+
.appendText(" should exist in JSON Schema ").appendValue(pathToJsonFile);
52+
}
53+
54+
@Override
55+
protected void describeMismatchSafely(final String pathToJsonFile, final Description mismatchDescription) {
56+
mismatchDescription.appendText("property ").appendValue(jsonpathToRequiredProperty).appendText(" doesn't exist in schema");
57+
}
58+
};
59+
}
60+
61+
/**
62+
* Matcher to validate if a given property is a required (non-optional) property in the given json schema
63+
*
64+
* @param jsonpathToRequiredProperty json path of the property that should be a required property in the schema (i.e. case.offences.offenceId)
65+
* @return matcher
66+
*/
67+
public static Matcher<String> hasRequiredProperty(final String jsonpathToRequiredProperty) {
68+
69+
return new TypeSafeMatcher<String>() {
70+
private String pathToJsonFile = null;
71+
72+
@Override
73+
protected boolean matchesSafely(final String pathToJsonFile) {
74+
this.pathToJsonFile = pathToJsonFile;
75+
final JsonPath jsonPath = JsonPath.of(jsonpathToRequiredProperty);
76+
final ObjectSchema parentObjectSchema = getObjectSchemaFromFile(pathToJsonFile);
77+
return getObjectSchemaWithPath(parentObjectSchema, jsonPath).getRequiredProperties().contains(jsonPath.getProperty());
78+
}
79+
80+
@Override
81+
public void describeTo(final Description description) {
82+
description.appendText("property ").appendValue(jsonpathToRequiredProperty)
83+
.appendText(" should be a required property in JSON Schema ").appendValue(pathToJsonFile);
84+
}
85+
86+
@Override
87+
protected void describeMismatchSafely(final String pathToJsonFile, final Description mismatchDescription) {
88+
mismatchDescription.appendText("property ").appendValue(jsonpathToRequiredProperty).appendText(" is not a required property");
89+
}
90+
};
91+
}
92+
93+
private static ObjectSchema getObjectSchemaFromFile(final String pathToJsonSchema) {
94+
final String jsonSchema = getJsonContentFrom(pathToJsonSchema);
95+
final Schema rawSchema = SchemaLoader.load(new JSONObject(new JSONTokener(jsonSchema)));
96+
return Optional.of(rawSchema)
97+
.filter(ObjectSchema.class::isInstance)
98+
.map(ObjectSchema.class::cast)
99+
.orElseThrow(() -> new IllegalArgumentException(format("Schema found in file %s is invalid.", pathToJsonSchema)));
100+
}
101+
102+
private static String getJsonContentFrom(final String pathToJsonSchema) {
103+
try {
104+
if (Paths.get(pathToJsonSchema).isAbsolute()) {
105+
return Files.toString(new File(pathToJsonSchema), UTF_8);
106+
} else {
107+
return Resources.toString(Resources.getResource(pathToJsonSchema), UTF_8);
108+
}
109+
} catch (IOException ex) {
110+
throw new IllegalArgumentException(format("Schema file %s not found.", pathToJsonSchema));
111+
}
112+
}
113+
114+
private static ObjectSchema getObjectSchemaWithPath(final ObjectSchema parentObjectSchema, final JsonPath jsonPath) {
115+
return jsonPath.getRemainder().map(s -> getObjectSchemaWithPathRecursive(parentObjectSchema, jsonPath)).orElse(parentObjectSchema);
116+
}
117+
118+
private static ObjectSchema getObjectSchemaWithPathRecursive(final Schema rawSchema, final JsonPath jsonPath) {
119+
if (rawSchema == null) {
120+
throw new IllegalArgumentException("Invalid XPath to property.");
121+
} else if (rawSchema instanceof ObjectSchema) {
122+
final ObjectSchema objectSchema = (ObjectSchema) rawSchema;
123+
return jsonPath.getRemainder().map(remainder -> getObjectSchemaWithPathRecursive(objectSchema.getPropertySchemas().get(jsonPath.getParent()), JsonPath.of(remainder))).orElse(objectSchema);
124+
} else if (rawSchema instanceof ArraySchema) {
125+
final ArraySchema arraySchema = (ArraySchema) rawSchema;
126+
return getObjectSchemaWithPathRecursive(arraySchema.getAllItemSchema(), jsonPath);
127+
} else if (rawSchema instanceof ReferenceSchema) {
128+
final ReferenceSchema referenceSchema = (ReferenceSchema) rawSchema;
129+
return getObjectSchemaWithPathRecursive(referenceSchema.getReferredSchema(), jsonPath);
130+
}
131+
throw new IllegalArgumentException("Unsupported property type.");
132+
}
133+
134+
private static class JsonPath {
135+
private final String property;
136+
private final String parent;
137+
private final String remainder;
138+
139+
private static JsonPath of(final String jsonPath) {
140+
final String[] jsonPathSplit = jsonPath.split(JSON_PATH_SEPARATOR);
141+
return new JsonPath(
142+
jsonPathSplit[jsonPathSplit.length - 1],
143+
jsonPathSplit[0],
144+
jsonPathSplit.length >= 2 ? Strings.emptyToNull(jsonPath.split(JSON_PATH_SEPARATOR, 2)[1]) : null);
145+
}
146+
147+
private JsonPath(final String property, final String parent, final String remainder) {
148+
this.parent = parent;
149+
this.remainder = remainder;
150+
this.property = property;
151+
}
152+
153+
public String getProperty() {
154+
return property;
155+
}
156+
157+
public String getParent() {
158+
return parent;
159+
}
160+
161+
public Optional<String> getRemainder() {
162+
return Optional.ofNullable(remainder);
163+
}
164+
}
165+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
package uk.gov.justice.services.test.utils.core.matchers;
2+
3+
import org.junit.Test;
4+
5+
import static org.hamcrest.core.IsNot.not;
6+
import static org.junit.Assert.assertThat;
7+
import static uk.gov.justice.services.test.utils.core.matchers.JsonSchemaPropertyMatcher.hasProperty;
8+
import static uk.gov.justice.services.test.utils.core.matchers.JsonSchemaPropertyMatcher.hasRequiredProperty;
9+
10+
public class JsonSchemaPropertyMatcherTest {
11+
12+
private static final String PATH_TO_SCHEMA = "json/schema/example.schema-property-matcher.json";
13+
14+
@Test
15+
public void matchesOneLevelRequiredProperty() {
16+
assertThat(PATH_TO_SCHEMA, hasRequiredProperty("urn"));
17+
}
18+
19+
@Test
20+
public void doesntMatchOneLevelRequiredProperty() {
21+
assertThat(PATH_TO_SCHEMA, not(hasRequiredProperty("anotherProperty")));
22+
}
23+
24+
@Test
25+
public void doesntMatchNonExistingOneLevelRequiredProperty() {
26+
assertThat(PATH_TO_SCHEMA, not(hasRequiredProperty("thisPropertyDoesNotExist")));
27+
}
28+
29+
@Test
30+
public void matchesThreeLevelRequiredProperty() {
31+
assertThat(PATH_TO_SCHEMA, hasRequiredProperty("schemaArray.schemaSubArray.subArrayId"));
32+
}
33+
34+
@Test
35+
public void doesntMatchThreeLevelRequiredProperty() {
36+
assertThat(PATH_TO_SCHEMA, not(hasRequiredProperty("schemaArray.schemaSubArray.subArrayProperty")));
37+
}
38+
39+
@Test
40+
public void matchesRefRequiredProperty() {
41+
assertThat(PATH_TO_SCHEMA, hasRequiredProperty("address"));
42+
}
43+
44+
@Test
45+
public void matchesRequiredPropertyInsideRef() {
46+
assertThat(PATH_TO_SCHEMA, hasRequiredProperty("address.postCode"));
47+
}
48+
49+
@Test
50+
public void doesntMatchRequiredPropertyInsideRef() {
51+
assertThat(PATH_TO_SCHEMA, not(hasRequiredProperty("address.addressLine2")));
52+
}
53+
54+
@Test(expected = IllegalArgumentException.class)
55+
public void invalidXpathShouldThrowIllegalArgument() {
56+
assertThat(PATH_TO_SCHEMA, hasRequiredProperty("schemaArray.invalidProperty.subArrayId"));
57+
}
58+
59+
@Test(expected = IllegalArgumentException.class)
60+
public void invalidFileShouldThrowIllegalArgument() {
61+
assertThat("json/schema/invalid.file.json", hasRequiredProperty("urn"));
62+
}
63+
64+
@Test
65+
public void matchesOneLevelProperty() {
66+
assertThat(PATH_TO_SCHEMA, hasProperty("anotherProperty"));
67+
}
68+
69+
@Test
70+
public void doesntMatchOneLevelProperty() {
71+
assertThat(PATH_TO_SCHEMA, not(hasProperty("thisPropertyDoesNotExist")));
72+
}
73+
74+
@Test
75+
public void matchesMultiLevelProperty() {
76+
assertThat(PATH_TO_SCHEMA, hasProperty("schemaArray.schemaSubArray.offence.wording"));
77+
}
78+
79+
@Test
80+
public void doesntMatchMultiLevelProperty() {
81+
assertThat(PATH_TO_SCHEMA, not(hasProperty("schemaArray.schemaSubArray.offence.thisPropertyDoesNotExist")));
82+
}
83+
}
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
{
2+
"$schema": "http://json-schema.org/draft-04/schema#",
3+
"definitions": {
4+
"address": {
5+
"type": "object",
6+
"properties": {
7+
"addressLine1": {
8+
"type": "string"
9+
},
10+
"addressLine2": {
11+
"type": "string"
12+
},
13+
"postCode": {
14+
"type": "string"
15+
}
16+
},
17+
"additionalProperties": false,
18+
"required": [
19+
"addressLine1",
20+
"postCode"
21+
]
22+
},
23+
"offence": {
24+
"type": "object",
25+
"properties": {
26+
"offenceId": {
27+
"type": "string"
28+
},
29+
"wording": {
30+
"type": "string"
31+
}
32+
},
33+
"additionalProperties": false,
34+
"required": [
35+
"offenceId"
36+
]
37+
}
38+
},
39+
"type": "object",
40+
"properties": {
41+
"urn": {
42+
"id": "/urn",
43+
"type": "string"
44+
},
45+
"id": {
46+
"id": "/id",
47+
"type": "string"
48+
},
49+
"anotherProperty": {
50+
"id": "/anotherProperty",
51+
"type": "string"
52+
},
53+
"address": {
54+
"$ref": "#/definitions/address"
55+
},
56+
"schemaArray": {
57+
"type": "array",
58+
"items": {
59+
"type": "object",
60+
"properties": {
61+
"schemaArrayId": {
62+
"type": "string"
63+
},
64+
"schemaArrayReqProperty": {
65+
"type": "string"
66+
},
67+
"schemaArrayProperty": {
68+
"type": "integer"
69+
},
70+
"schemaSubArray": {
71+
"type": "array",
72+
"items": {
73+
"type": "object",
74+
"properties": {
75+
"subArrayId": {
76+
"type": "string"
77+
},
78+
"subArrayProperty": {
79+
"type": "string"
80+
},
81+
"offence": {
82+
"$ref": "#/definitions/offence"
83+
}
84+
},
85+
"required" : [
86+
"subArrayId"
87+
]
88+
}
89+
}
90+
},
91+
"required" : [
92+
"schemaArrayId",
93+
"schemaArrayReqProperty"
94+
]
95+
}
96+
}
97+
},
98+
"required": [
99+
"urn",
100+
"id",
101+
"address"
102+
]
103+
}

0 commit comments

Comments
 (0)