Skip to content

Commit bff9eae

Browse files
rishi-agaRishi Agarwal
authored andcommitted
Display user friendly messages for Json Schema validation failures (#1662)
* Display user friendly messages for Json Schema validation failures * Review Comments Co-authored-by: Rishi Agarwal <[email protected]>
1 parent a369eaf commit bff9eae

20 files changed

+1009
-489
lines changed
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
/*
2+
* Copyright 2020, Yahoo Inc.
3+
* Licensed under the Apache License, Version 2.0
4+
* See LICENSE file in project root for terms.
5+
*/
6+
package com.yahoo.elide.contrib.dynamicconfighelpers;
7+
8+
import com.yahoo.elide.contrib.dynamicconfighelpers.jsonformats.ElideCardinalityFormatAttr;
9+
import com.yahoo.elide.contrib.dynamicconfighelpers.jsonformats.ElideFieldNameFormatAttr;
10+
import com.yahoo.elide.contrib.dynamicconfighelpers.jsonformats.ElideFieldTypeFormatAttr;
11+
import com.yahoo.elide.contrib.dynamicconfighelpers.jsonformats.ElideGrainTypeFormatAttr;
12+
import com.yahoo.elide.contrib.dynamicconfighelpers.jsonformats.ElideJoinTypeFormatAttr;
13+
import com.yahoo.elide.contrib.dynamicconfighelpers.jsonformats.ElideNameFormatAttr;
14+
import com.yahoo.elide.contrib.dynamicconfighelpers.jsonformats.ElideTimeFieldTypeFormatAttr;
15+
16+
import com.github.fge.jsonschema.library.DraftV4Library;
17+
import com.github.fge.jsonschema.library.Library;
18+
import com.github.fge.jsonschema.library.LibraryBuilder;
19+
20+
import lombok.NoArgsConstructor;
21+
22+
@NoArgsConstructor(access = lombok.AccessLevel.PRIVATE)
23+
public class DraftV4LibraryWithElideFormatAttr {
24+
private static Library LIBRARY = null;
25+
26+
public static Library getInstance() {
27+
if (LIBRARY == null) {
28+
LibraryBuilder builder = DraftV4Library.get().thaw();
29+
30+
builder.addFormatAttribute(ElideFieldNameFormatAttr.FORMAT_NAME,
31+
ElideFieldNameFormatAttr.getInstance());
32+
builder.addFormatAttribute(ElideCardinalityFormatAttr.FORMAT_NAME,
33+
ElideCardinalityFormatAttr.getInstance());
34+
builder.addFormatAttribute(ElideFieldTypeFormatAttr.FORMAT_NAME,
35+
ElideFieldTypeFormatAttr.getInstance());
36+
builder.addFormatAttribute(ElideGrainTypeFormatAttr.FORMAT_NAME,
37+
ElideGrainTypeFormatAttr.getInstance());
38+
builder.addFormatAttribute(ElideJoinTypeFormatAttr.FORMAT_NAME,
39+
ElideJoinTypeFormatAttr.getInstance());
40+
builder.addFormatAttribute(ElideTimeFieldTypeFormatAttr.FORMAT_NAME,
41+
ElideTimeFieldTypeFormatAttr.getInstance());
42+
builder.addFormatAttribute(ElideNameFormatAttr.FORMAT_NAME,
43+
ElideNameFormatAttr.getInstance());
44+
45+
LIBRARY = builder.freeze();
46+
}
47+
48+
return LIBRARY;
49+
}
50+
}

elide-model-config/src/main/java/com/yahoo/elide/contrib/dynamicconfighelpers/DynamicConfigHelpers.java

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99
import com.yahoo.elide.contrib.dynamicconfighelpers.model.ElideSecurityConfig;
1010
import com.yahoo.elide.contrib.dynamicconfighelpers.model.ElideTableConfig;
1111
import com.yahoo.elide.contrib.dynamicconfighelpers.parser.handlebars.HandlebarsHydrator;
12-
1312
import com.fasterxml.jackson.core.JsonProcessingException;
1413
import com.fasterxml.jackson.databind.MapperFeature;
1514
import com.fasterxml.jackson.databind.ObjectMapper;

elide-model-config/src/main/java/com/yahoo/elide/contrib/dynamicconfighelpers/DynamicConfigSchemaValidator.java

Lines changed: 51 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -11,17 +11,18 @@
1111
import com.fasterxml.jackson.databind.JsonNode;
1212
import com.fasterxml.jackson.databind.ObjectMapper;
1313
import com.fasterxml.jackson.databind.node.ArrayNode;
14+
import com.github.fge.jsonschema.cfg.ValidationConfiguration;
1415
import com.github.fge.jsonschema.core.exceptions.ProcessingException;
1516
import com.github.fge.jsonschema.core.report.LogLevel;
1617
import com.github.fge.jsonschema.core.report.ProcessingReport;
18+
import com.github.fge.jsonschema.library.Library;
1719
import com.github.fge.jsonschema.main.JsonSchema;
1820
import com.github.fge.jsonschema.main.JsonSchemaFactory;
21+
import com.github.fge.msgsimple.bundle.MessageBundle;
1922

2023
import lombok.extern.slf4j.Slf4j;
2124

2225
import java.io.IOException;
23-
import java.io.InputStreamReader;
24-
import java.io.Reader;
2526
import java.util.ArrayList;
2627
import java.util.Iterator;
2728
import java.util.List;
@@ -33,20 +34,34 @@
3334
@Slf4j
3435
public class DynamicConfigSchemaValidator {
3536

36-
private static final JsonSchemaFactory FACTORY = JsonSchemaFactory.byDefault();
3737
private JsonSchema tableSchema;
3838
private JsonSchema securitySchema;
3939
private JsonSchema variableSchema;
4040
private JsonSchema dbConfigSchema;
4141

4242
public DynamicConfigSchemaValidator() {
43-
tableSchema = loadSchema(Config.TABLE.getConfigSchema());
44-
securitySchema = loadSchema(Config.SECURITY.getConfigSchema());
45-
variableSchema = loadSchema(Config.MODELVARIABLE.getConfigSchema());
46-
dbConfigSchema = loadSchema(Config.SQLDBConfig.getConfigSchema());
43+
44+
Library library = DraftV4LibraryWithElideFormatAttr.getInstance();
45+
46+
MessageBundle bundle = MessageBundleWithElideMessages.getInstance();
47+
48+
ValidationConfiguration cfg = ValidationConfiguration.newBuilder()
49+
.setDefaultLibrary("http://my.site/myschema#", library)
50+
.setValidationMessages(bundle)
51+
.freeze();
52+
53+
JsonSchemaFactory factory = JsonSchemaFactory.newBuilder()
54+
.setValidationConfiguration(cfg)
55+
.freeze();
56+
57+
tableSchema = loadSchema(factory, Config.TABLE.getConfigSchema());
58+
securitySchema = loadSchema(factory, Config.SECURITY.getConfigSchema());
59+
variableSchema = loadSchema(factory, Config.MODELVARIABLE.getConfigSchema());
60+
dbConfigSchema = loadSchema(factory, Config.SQLDBConfig.getConfigSchema());
4761
}
62+
4863
/**
49-
* Verify config against schema.
64+
* Verify config against schema.
5065
* @param configType
5166
* @param jsonConfig
5267
* @param fileName
@@ -61,17 +76,17 @@ public boolean verifySchema(Config configType, String jsonConfig, String fileNam
6176

6277
switch (configType) {
6378
case TABLE :
64-
results = this.tableSchema.validate(new ObjectMapper().readTree(jsonConfig));
79+
results = this.tableSchema.validate(new ObjectMapper().readTree(jsonConfig), true);
6580
break;
6681
case SECURITY :
67-
results = this.securitySchema.validate(new ObjectMapper().readTree(jsonConfig));
82+
results = this.securitySchema.validate(new ObjectMapper().readTree(jsonConfig), true);
6883
break;
6984
case MODELVARIABLE :
7085
case DBVARIABLE :
71-
results = this.variableSchema.validate(new ObjectMapper().readTree(jsonConfig));
86+
results = this.variableSchema.validate(new ObjectMapper().readTree(jsonConfig), true);
7287
break;
7388
case SQLDBConfig :
74-
results = this.dbConfigSchema.validate(new ObjectMapper().readTree(jsonConfig));
89+
results = this.dbConfigSchema.validate(new ObjectMapper().readTree(jsonConfig), true);
7590
break;
7691
default :
7792
log.error("Not a valid config type :" + configType);
@@ -99,15 +114,15 @@ private static void addEmbeddedMessages(JsonNode root, List<String> list, int de
99114

100115
if (level.equalsIgnoreCase(LogLevel.ERROR.name()) || level.equalsIgnoreCase(LogLevel.FATAL.name())) {
101116
String msg = root.get("message").asText();
102-
String pointer = null;
103-
if (root.has("instance")) {
104-
JsonNode instanceNode = root.get("instance");
105-
if (instanceNode.has("pointer")) {
106-
pointer = instanceNode.get("pointer").asText();
107-
}
117+
String instancePointer = extractPointer(root, "instance");
118+
String schemaPointer = extractPointer(root, "schema");
119+
120+
if (!(isNullOrEmpty(instancePointer) || isNullOrEmpty(schemaPointer))) {
121+
msg = "Instance[" + instancePointer + "] failed to validate against schema[" + schemaPointer + "]. "
122+
+ msg;
108123
}
109-
msg = (isNullOrEmpty(pointer)) ? msg : msg + " at node: " + pointer;
110-
list.add(String.format("%" + (4 * depth + msg.length()) + "s", msg));
124+
list.add((depth == 0) ? "[ERROR]" + NEWLINE + msg
125+
: String.format("%" + (4 * depth + msg.length()) + "s", msg));
111126

112127
if (root.has("reports")) {
113128
Iterator<Entry<String, JsonNode>> fields = root.get("reports").fields();
@@ -122,11 +137,23 @@ private static void addEmbeddedMessages(JsonNode root, List<String> list, int de
122137
}
123138
}
124139

125-
private JsonSchema loadSchema(String resource) {
126-
ObjectMapper objectMapper = new ObjectMapper();
127-
Reader reader = new InputStreamReader(DynamicConfigHelpers.class.getResourceAsStream(resource));
140+
private static String extractPointer(JsonNode root, String fieldName) {
141+
String pointer = null;
142+
if (root.has(fieldName)) {
143+
JsonNode node = root.get(fieldName);
144+
if (node.has("pointer")) {
145+
pointer = node.get("pointer").asText();
146+
}
147+
}
148+
149+
return pointer;
150+
}
151+
152+
private static JsonSchema loadSchema(JsonSchemaFactory factory, String resource) {
153+
128154
try {
129-
return FACTORY.getJsonSchema(objectMapper.readTree(reader));
155+
return factory.getJsonSchema(
156+
new ObjectMapper().readTree(DynamicConfigHelpers.class.getResourceAsStream(resource)));
130157
} catch (IOException | ProcessingException e) {
131158
log.error("Error loading schema file " + resource + " to verify");
132159
throw new IllegalStateException(e.getMessage());
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
/*
2+
* Copyright 2020, Yahoo Inc.
3+
* Licensed under the Apache License, Version 2.0
4+
* See LICENSE file in project root for terms.
5+
*/
6+
package com.yahoo.elide.contrib.dynamicconfighelpers;
7+
8+
import com.yahoo.elide.contrib.dynamicconfighelpers.jsonformats.ElideCardinalityFormatAttr;
9+
import com.yahoo.elide.contrib.dynamicconfighelpers.jsonformats.ElideFieldNameFormatAttr;
10+
import com.yahoo.elide.contrib.dynamicconfighelpers.jsonformats.ElideFieldTypeFormatAttr;
11+
import com.yahoo.elide.contrib.dynamicconfighelpers.jsonformats.ElideGrainTypeFormatAttr;
12+
import com.yahoo.elide.contrib.dynamicconfighelpers.jsonformats.ElideJoinTypeFormatAttr;
13+
import com.yahoo.elide.contrib.dynamicconfighelpers.jsonformats.ElideNameFormatAttr;
14+
import com.yahoo.elide.contrib.dynamicconfighelpers.jsonformats.ElideTimeFieldTypeFormatAttr;
15+
16+
import com.github.fge.jsonschema.messages.JsonSchemaValidationBundle;
17+
import com.github.fge.msgsimple.bundle.MessageBundle;
18+
import com.github.fge.msgsimple.load.MessageBundles;
19+
import com.github.fge.msgsimple.source.MapMessageSource;
20+
import com.github.fge.msgsimple.source.MapMessageSource.Builder;
21+
22+
import lombok.NoArgsConstructor;
23+
24+
@NoArgsConstructor(access = lombok.AccessLevel.PRIVATE)
25+
public class MessageBundleWithElideMessages {
26+
private static MessageBundle BUNDLE = null;
27+
28+
public static MessageBundle getInstance() {
29+
if (BUNDLE == null) {
30+
Builder msgSourceBuilder = MapMessageSource.newBuilder();
31+
32+
msgSourceBuilder.put(ElideFieldNameFormatAttr.FORMAT_KEY, ElideFieldNameFormatAttr.FORMAT_MSG);
33+
msgSourceBuilder.put(ElideFieldNameFormatAttr.NAME_KEY, ElideFieldNameFormatAttr.NAME_MSG);
34+
msgSourceBuilder.put(ElideCardinalityFormatAttr.TYPE_KEY, ElideCardinalityFormatAttr.TYPE_MSG);
35+
msgSourceBuilder.put(ElideFieldTypeFormatAttr.TYPE_KEY, ElideFieldTypeFormatAttr.TYPE_MSG);
36+
msgSourceBuilder.put(ElideGrainTypeFormatAttr.TYPE_KEY, ElideGrainTypeFormatAttr.TYPE_MSG);
37+
msgSourceBuilder.put(ElideJoinTypeFormatAttr.TYPE_KEY, ElideJoinTypeFormatAttr.TYPE_MSG);
38+
msgSourceBuilder.put(ElideTimeFieldTypeFormatAttr.TYPE_KEY, ElideTimeFieldTypeFormatAttr.TYPE_MSG);
39+
msgSourceBuilder.put(ElideNameFormatAttr.FORMAT_KEY, ElideNameFormatAttr.FORMAT_MSG);
40+
41+
BUNDLE = MessageBundles.getBundle(JsonSchemaValidationBundle.class).thaw()
42+
.appendSource(msgSourceBuilder.build())
43+
.freeze();
44+
}
45+
46+
return BUNDLE;
47+
}
48+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
/*
2+
* Copyright 2020, Yahoo Inc.
3+
* Licensed under the Apache License, Version 2.0
4+
* See LICENSE file in project root for terms.
5+
*/
6+
package com.yahoo.elide.contrib.dynamicconfighelpers.jsonformats;
7+
8+
import com.github.fge.jackson.NodeType;
9+
import com.github.fge.jsonschema.core.exceptions.ProcessingException;
10+
import com.github.fge.jsonschema.core.report.ProcessingReport;
11+
import com.github.fge.jsonschema.format.AbstractFormatAttribute;
12+
import com.github.fge.jsonschema.format.FormatAttribute;
13+
import com.github.fge.jsonschema.processors.data.FullData;
14+
import com.github.fge.msgsimple.bundle.MessageBundle;
15+
16+
public class ElideCardinalityFormatAttr extends AbstractFormatAttribute {
17+
private static final FormatAttribute INSTANCE = new ElideCardinalityFormatAttr();
18+
private static final String CARDINALITY_REGEX = "^(?i)(Tiny|Small|Medium|Large|Huge)$";
19+
20+
public static final String FORMAT_NAME = "elideCardiality";
21+
public static final String TYPE_KEY = "elideCardiality.error.enum";
22+
public static final String TYPE_MSG = "Cardinality type [%s] is not allowed. Supported value is one of "
23+
+ "[Tiny, Small, Medium, Large, Huge].";
24+
25+
private ElideCardinalityFormatAttr() {
26+
super(FORMAT_NAME, NodeType.STRING);
27+
}
28+
29+
public static FormatAttribute getInstance() {
30+
return INSTANCE;
31+
}
32+
33+
@Override
34+
public void validate(final ProcessingReport report, final MessageBundle bundle, final FullData data)
35+
throws ProcessingException {
36+
final String input = data.getInstance().getNode().textValue();
37+
38+
if (!input.matches(CARDINALITY_REGEX)) {
39+
report.error(newMsg(data, bundle, TYPE_KEY).putArgument("value", input));
40+
}
41+
}
42+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
/*
2+
* Copyright 2020, Yahoo Inc.
3+
* Licensed under the Apache License, Version 2.0
4+
* See LICENSE file in project root for terms.
5+
*/
6+
package com.yahoo.elide.contrib.dynamicconfighelpers.jsonformats;
7+
8+
import com.github.fge.jackson.NodeType;
9+
import com.github.fge.jsonschema.core.exceptions.ProcessingException;
10+
import com.github.fge.jsonschema.core.report.ProcessingReport;
11+
import com.github.fge.jsonschema.format.AbstractFormatAttribute;
12+
import com.github.fge.jsonschema.format.FormatAttribute;
13+
import com.github.fge.jsonschema.processors.data.FullData;
14+
import com.github.fge.msgsimple.bundle.MessageBundle;
15+
16+
public class ElideFieldNameFormatAttr extends AbstractFormatAttribute {
17+
private static final FormatAttribute INSTANCE = new ElideFieldNameFormatAttr();
18+
private static final String FIELD_NAME_FORMAT_REGEX = "^[A-Za-z][0-9A-Za-z_]*$";
19+
20+
public static final String FORMAT_NAME = "elideFieldName";
21+
public static final String NAME_KEY = "elideFieldName.error.name";
22+
public static final String NAME_MSG = "Field name [%s] is not allowed. Field name cannot be 'id'";
23+
public static final String FORMAT_KEY = "elideFieldName.error.format";
24+
public static final String FORMAT_MSG = "Field name [%s] is not allowed. Field name must start with "
25+
+ "an alphabet and can include alaphabets, numbers and '_' only.";
26+
27+
private ElideFieldNameFormatAttr() {
28+
super(FORMAT_NAME, NodeType.STRING);
29+
}
30+
31+
public static FormatAttribute getInstance() {
32+
return INSTANCE;
33+
}
34+
35+
@Override
36+
public void validate(final ProcessingReport report, final MessageBundle bundle, final FullData data)
37+
throws ProcessingException {
38+
final String input = data.getInstance().getNode().textValue();
39+
40+
if (!input.matches(FIELD_NAME_FORMAT_REGEX)) {
41+
report.error(newMsg(data, bundle, FORMAT_KEY).putArgument("value", input));
42+
}
43+
44+
if (input.equalsIgnoreCase("id")) {
45+
report.error(newMsg(data, bundle, NAME_KEY).putArgument("value", input));
46+
}
47+
}
48+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
/*
2+
* Copyright 2020, Yahoo Inc.
3+
* Licensed under the Apache License, Version 2.0
4+
* See LICENSE file in project root for terms.
5+
*/
6+
package com.yahoo.elide.contrib.dynamicconfighelpers.jsonformats;
7+
8+
import com.github.fge.jackson.NodeType;
9+
import com.github.fge.jsonschema.core.exceptions.ProcessingException;
10+
import com.github.fge.jsonschema.core.report.ProcessingReport;
11+
import com.github.fge.jsonschema.format.AbstractFormatAttribute;
12+
import com.github.fge.jsonschema.format.FormatAttribute;
13+
import com.github.fge.jsonschema.processors.data.FullData;
14+
import com.github.fge.msgsimple.bundle.MessageBundle;
15+
16+
public class ElideFieldTypeFormatAttr extends AbstractFormatAttribute {
17+
private static final FormatAttribute INSTANCE = new ElideFieldTypeFormatAttr();
18+
private static final String FIELD_TYPE_REGEX = "^(?i)(Integer|Decimal|Money|Text|Coordinate|Boolean)$";
19+
20+
public static final String FORMAT_NAME = "elideFieldType";
21+
public static final String TYPE_KEY = "elideFieldType.error.enum";
22+
public static final String TYPE_MSG = "Field type [%s] is not allowed. Supported value is one of "
23+
+ "[Integer, Decimal, Money, Text, Coordinate, Boolean].";
24+
25+
private ElideFieldTypeFormatAttr() {
26+
super(FORMAT_NAME, NodeType.STRING);
27+
}
28+
29+
public static FormatAttribute getInstance() {
30+
return INSTANCE;
31+
}
32+
33+
@Override
34+
public void validate(final ProcessingReport report, final MessageBundle bundle, final FullData data)
35+
throws ProcessingException {
36+
final String input = data.getInstance().getNode().textValue();
37+
38+
if (!input.matches(FIELD_TYPE_REGEX)) {
39+
report.error(newMsg(data, bundle, TYPE_KEY).putArgument("value", input));
40+
}
41+
}
42+
}

0 commit comments

Comments
 (0)