Skip to content

Commit

Permalink
Display user friendly messages for Json Schema validation failures (#…
Browse files Browse the repository at this point in the history
…1662)

* Display user friendly messages for Json Schema validation failures

* Review Comments

Co-authored-by: Rishi Agarwal <rishi.agarwal@verizonmedia.com>
  • Loading branch information
2 people authored and Aaron Klish committed Nov 17, 2020
1 parent a369eaf commit bff9eae
Show file tree
Hide file tree
Showing 20 changed files with 1,009 additions and 489 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/*
* Copyright 2020, Yahoo Inc.
* Licensed under the Apache License, Version 2.0
* See LICENSE file in project root for terms.
*/
package com.yahoo.elide.contrib.dynamicconfighelpers;

import com.yahoo.elide.contrib.dynamicconfighelpers.jsonformats.ElideCardinalityFormatAttr;
import com.yahoo.elide.contrib.dynamicconfighelpers.jsonformats.ElideFieldNameFormatAttr;
import com.yahoo.elide.contrib.dynamicconfighelpers.jsonformats.ElideFieldTypeFormatAttr;
import com.yahoo.elide.contrib.dynamicconfighelpers.jsonformats.ElideGrainTypeFormatAttr;
import com.yahoo.elide.contrib.dynamicconfighelpers.jsonformats.ElideJoinTypeFormatAttr;
import com.yahoo.elide.contrib.dynamicconfighelpers.jsonformats.ElideNameFormatAttr;
import com.yahoo.elide.contrib.dynamicconfighelpers.jsonformats.ElideTimeFieldTypeFormatAttr;

import com.github.fge.jsonschema.library.DraftV4Library;
import com.github.fge.jsonschema.library.Library;
import com.github.fge.jsonschema.library.LibraryBuilder;

import lombok.NoArgsConstructor;

@NoArgsConstructor(access = lombok.AccessLevel.PRIVATE)
public class DraftV4LibraryWithElideFormatAttr {
private static Library LIBRARY = null;

public static Library getInstance() {
if (LIBRARY == null) {
LibraryBuilder builder = DraftV4Library.get().thaw();

builder.addFormatAttribute(ElideFieldNameFormatAttr.FORMAT_NAME,
ElideFieldNameFormatAttr.getInstance());
builder.addFormatAttribute(ElideCardinalityFormatAttr.FORMAT_NAME,
ElideCardinalityFormatAttr.getInstance());
builder.addFormatAttribute(ElideFieldTypeFormatAttr.FORMAT_NAME,
ElideFieldTypeFormatAttr.getInstance());
builder.addFormatAttribute(ElideGrainTypeFormatAttr.FORMAT_NAME,
ElideGrainTypeFormatAttr.getInstance());
builder.addFormatAttribute(ElideJoinTypeFormatAttr.FORMAT_NAME,
ElideJoinTypeFormatAttr.getInstance());
builder.addFormatAttribute(ElideTimeFieldTypeFormatAttr.FORMAT_NAME,
ElideTimeFieldTypeFormatAttr.getInstance());
builder.addFormatAttribute(ElideNameFormatAttr.FORMAT_NAME,
ElideNameFormatAttr.getInstance());

LIBRARY = builder.freeze();
}

return LIBRARY;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
import com.yahoo.elide.contrib.dynamicconfighelpers.model.ElideSecurityConfig;
import com.yahoo.elide.contrib.dynamicconfighelpers.model.ElideTableConfig;
import com.yahoo.elide.contrib.dynamicconfighelpers.parser.handlebars.HandlebarsHydrator;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.MapperFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,18 @@
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.github.fge.jsonschema.cfg.ValidationConfiguration;
import com.github.fge.jsonschema.core.exceptions.ProcessingException;
import com.github.fge.jsonschema.core.report.LogLevel;
import com.github.fge.jsonschema.core.report.ProcessingReport;
import com.github.fge.jsonschema.library.Library;
import com.github.fge.jsonschema.main.JsonSchema;
import com.github.fge.jsonschema.main.JsonSchemaFactory;
import com.github.fge.msgsimple.bundle.MessageBundle;

import lombok.extern.slf4j.Slf4j;

import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Reader;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
Expand All @@ -33,20 +34,34 @@
@Slf4j
public class DynamicConfigSchemaValidator {

private static final JsonSchemaFactory FACTORY = JsonSchemaFactory.byDefault();
private JsonSchema tableSchema;
private JsonSchema securitySchema;
private JsonSchema variableSchema;
private JsonSchema dbConfigSchema;

public DynamicConfigSchemaValidator() {
tableSchema = loadSchema(Config.TABLE.getConfigSchema());
securitySchema = loadSchema(Config.SECURITY.getConfigSchema());
variableSchema = loadSchema(Config.MODELVARIABLE.getConfigSchema());
dbConfigSchema = loadSchema(Config.SQLDBConfig.getConfigSchema());

Library library = DraftV4LibraryWithElideFormatAttr.getInstance();

MessageBundle bundle = MessageBundleWithElideMessages.getInstance();

ValidationConfiguration cfg = ValidationConfiguration.newBuilder()
.setDefaultLibrary("http://my.site/myschema#", library)
.setValidationMessages(bundle)
.freeze();

JsonSchemaFactory factory = JsonSchemaFactory.newBuilder()
.setValidationConfiguration(cfg)
.freeze();

tableSchema = loadSchema(factory, Config.TABLE.getConfigSchema());
securitySchema = loadSchema(factory, Config.SECURITY.getConfigSchema());
variableSchema = loadSchema(factory, Config.MODELVARIABLE.getConfigSchema());
dbConfigSchema = loadSchema(factory, Config.SQLDBConfig.getConfigSchema());
}

/**
* Verify config against schema.
* Verify config against schema.
* @param configType
* @param jsonConfig
* @param fileName
Expand All @@ -61,17 +76,17 @@ public boolean verifySchema(Config configType, String jsonConfig, String fileNam

switch (configType) {
case TABLE :
results = this.tableSchema.validate(new ObjectMapper().readTree(jsonConfig));
results = this.tableSchema.validate(new ObjectMapper().readTree(jsonConfig), true);
break;
case SECURITY :
results = this.securitySchema.validate(new ObjectMapper().readTree(jsonConfig));
results = this.securitySchema.validate(new ObjectMapper().readTree(jsonConfig), true);
break;
case MODELVARIABLE :
case DBVARIABLE :
results = this.variableSchema.validate(new ObjectMapper().readTree(jsonConfig));
results = this.variableSchema.validate(new ObjectMapper().readTree(jsonConfig), true);
break;
case SQLDBConfig :
results = this.dbConfigSchema.validate(new ObjectMapper().readTree(jsonConfig));
results = this.dbConfigSchema.validate(new ObjectMapper().readTree(jsonConfig), true);
break;
default :
log.error("Not a valid config type :" + configType);
Expand Down Expand Up @@ -99,15 +114,15 @@ private static void addEmbeddedMessages(JsonNode root, List<String> list, int de

if (level.equalsIgnoreCase(LogLevel.ERROR.name()) || level.equalsIgnoreCase(LogLevel.FATAL.name())) {
String msg = root.get("message").asText();
String pointer = null;
if (root.has("instance")) {
JsonNode instanceNode = root.get("instance");
if (instanceNode.has("pointer")) {
pointer = instanceNode.get("pointer").asText();
}
String instancePointer = extractPointer(root, "instance");
String schemaPointer = extractPointer(root, "schema");

if (!(isNullOrEmpty(instancePointer) || isNullOrEmpty(schemaPointer))) {
msg = "Instance[" + instancePointer + "] failed to validate against schema[" + schemaPointer + "]. "
+ msg;
}
msg = (isNullOrEmpty(pointer)) ? msg : msg + " at node: " + pointer;
list.add(String.format("%" + (4 * depth + msg.length()) + "s", msg));
list.add((depth == 0) ? "[ERROR]" + NEWLINE + msg
: String.format("%" + (4 * depth + msg.length()) + "s", msg));

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

private JsonSchema loadSchema(String resource) {
ObjectMapper objectMapper = new ObjectMapper();
Reader reader = new InputStreamReader(DynamicConfigHelpers.class.getResourceAsStream(resource));
private static String extractPointer(JsonNode root, String fieldName) {
String pointer = null;
if (root.has(fieldName)) {
JsonNode node = root.get(fieldName);
if (node.has("pointer")) {
pointer = node.get("pointer").asText();
}
}

return pointer;
}

private static JsonSchema loadSchema(JsonSchemaFactory factory, String resource) {

try {
return FACTORY.getJsonSchema(objectMapper.readTree(reader));
return factory.getJsonSchema(
new ObjectMapper().readTree(DynamicConfigHelpers.class.getResourceAsStream(resource)));
} catch (IOException | ProcessingException e) {
log.error("Error loading schema file " + resource + " to verify");
throw new IllegalStateException(e.getMessage());
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/*
* Copyright 2020, Yahoo Inc.
* Licensed under the Apache License, Version 2.0
* See LICENSE file in project root for terms.
*/
package com.yahoo.elide.contrib.dynamicconfighelpers;

import com.yahoo.elide.contrib.dynamicconfighelpers.jsonformats.ElideCardinalityFormatAttr;
import com.yahoo.elide.contrib.dynamicconfighelpers.jsonformats.ElideFieldNameFormatAttr;
import com.yahoo.elide.contrib.dynamicconfighelpers.jsonformats.ElideFieldTypeFormatAttr;
import com.yahoo.elide.contrib.dynamicconfighelpers.jsonformats.ElideGrainTypeFormatAttr;
import com.yahoo.elide.contrib.dynamicconfighelpers.jsonformats.ElideJoinTypeFormatAttr;
import com.yahoo.elide.contrib.dynamicconfighelpers.jsonformats.ElideNameFormatAttr;
import com.yahoo.elide.contrib.dynamicconfighelpers.jsonformats.ElideTimeFieldTypeFormatAttr;

import com.github.fge.jsonschema.messages.JsonSchemaValidationBundle;
import com.github.fge.msgsimple.bundle.MessageBundle;
import com.github.fge.msgsimple.load.MessageBundles;
import com.github.fge.msgsimple.source.MapMessageSource;
import com.github.fge.msgsimple.source.MapMessageSource.Builder;

import lombok.NoArgsConstructor;

@NoArgsConstructor(access = lombok.AccessLevel.PRIVATE)
public class MessageBundleWithElideMessages {
private static MessageBundle BUNDLE = null;

public static MessageBundle getInstance() {
if (BUNDLE == null) {
Builder msgSourceBuilder = MapMessageSource.newBuilder();

msgSourceBuilder.put(ElideFieldNameFormatAttr.FORMAT_KEY, ElideFieldNameFormatAttr.FORMAT_MSG);
msgSourceBuilder.put(ElideFieldNameFormatAttr.NAME_KEY, ElideFieldNameFormatAttr.NAME_MSG);
msgSourceBuilder.put(ElideCardinalityFormatAttr.TYPE_KEY, ElideCardinalityFormatAttr.TYPE_MSG);
msgSourceBuilder.put(ElideFieldTypeFormatAttr.TYPE_KEY, ElideFieldTypeFormatAttr.TYPE_MSG);
msgSourceBuilder.put(ElideGrainTypeFormatAttr.TYPE_KEY, ElideGrainTypeFormatAttr.TYPE_MSG);
msgSourceBuilder.put(ElideJoinTypeFormatAttr.TYPE_KEY, ElideJoinTypeFormatAttr.TYPE_MSG);
msgSourceBuilder.put(ElideTimeFieldTypeFormatAttr.TYPE_KEY, ElideTimeFieldTypeFormatAttr.TYPE_MSG);
msgSourceBuilder.put(ElideNameFormatAttr.FORMAT_KEY, ElideNameFormatAttr.FORMAT_MSG);

BUNDLE = MessageBundles.getBundle(JsonSchemaValidationBundle.class).thaw()
.appendSource(msgSourceBuilder.build())
.freeze();
}

return BUNDLE;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/*
* Copyright 2020, Yahoo Inc.
* Licensed under the Apache License, Version 2.0
* See LICENSE file in project root for terms.
*/
package com.yahoo.elide.contrib.dynamicconfighelpers.jsonformats;

import com.github.fge.jackson.NodeType;
import com.github.fge.jsonschema.core.exceptions.ProcessingException;
import com.github.fge.jsonschema.core.report.ProcessingReport;
import com.github.fge.jsonschema.format.AbstractFormatAttribute;
import com.github.fge.jsonschema.format.FormatAttribute;
import com.github.fge.jsonschema.processors.data.FullData;
import com.github.fge.msgsimple.bundle.MessageBundle;

public class ElideCardinalityFormatAttr extends AbstractFormatAttribute {
private static final FormatAttribute INSTANCE = new ElideCardinalityFormatAttr();
private static final String CARDINALITY_REGEX = "^(?i)(Tiny|Small|Medium|Large|Huge)$";

public static final String FORMAT_NAME = "elideCardiality";
public static final String TYPE_KEY = "elideCardiality.error.enum";
public static final String TYPE_MSG = "Cardinality type [%s] is not allowed. Supported value is one of "
+ "[Tiny, Small, Medium, Large, Huge].";

private ElideCardinalityFormatAttr() {
super(FORMAT_NAME, NodeType.STRING);
}

public static FormatAttribute getInstance() {
return INSTANCE;
}

@Override
public void validate(final ProcessingReport report, final MessageBundle bundle, final FullData data)
throws ProcessingException {
final String input = data.getInstance().getNode().textValue();

if (!input.matches(CARDINALITY_REGEX)) {
report.error(newMsg(data, bundle, TYPE_KEY).putArgument("value", input));
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/*
* Copyright 2020, Yahoo Inc.
* Licensed under the Apache License, Version 2.0
* See LICENSE file in project root for terms.
*/
package com.yahoo.elide.contrib.dynamicconfighelpers.jsonformats;

import com.github.fge.jackson.NodeType;
import com.github.fge.jsonschema.core.exceptions.ProcessingException;
import com.github.fge.jsonschema.core.report.ProcessingReport;
import com.github.fge.jsonschema.format.AbstractFormatAttribute;
import com.github.fge.jsonschema.format.FormatAttribute;
import com.github.fge.jsonschema.processors.data.FullData;
import com.github.fge.msgsimple.bundle.MessageBundle;

public class ElideFieldNameFormatAttr extends AbstractFormatAttribute {
private static final FormatAttribute INSTANCE = new ElideFieldNameFormatAttr();
private static final String FIELD_NAME_FORMAT_REGEX = "^[A-Za-z][0-9A-Za-z_]*$";

public static final String FORMAT_NAME = "elideFieldName";
public static final String NAME_KEY = "elideFieldName.error.name";
public static final String NAME_MSG = "Field name [%s] is not allowed. Field name cannot be 'id'";
public static final String FORMAT_KEY = "elideFieldName.error.format";
public static final String FORMAT_MSG = "Field name [%s] is not allowed. Field name must start with "
+ "an alphabet and can include alaphabets, numbers and '_' only.";

private ElideFieldNameFormatAttr() {
super(FORMAT_NAME, NodeType.STRING);
}

public static FormatAttribute getInstance() {
return INSTANCE;
}

@Override
public void validate(final ProcessingReport report, final MessageBundle bundle, final FullData data)
throws ProcessingException {
final String input = data.getInstance().getNode().textValue();

if (!input.matches(FIELD_NAME_FORMAT_REGEX)) {
report.error(newMsg(data, bundle, FORMAT_KEY).putArgument("value", input));
}

if (input.equalsIgnoreCase("id")) {
report.error(newMsg(data, bundle, NAME_KEY).putArgument("value", input));
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/*
* Copyright 2020, Yahoo Inc.
* Licensed under the Apache License, Version 2.0
* See LICENSE file in project root for terms.
*/
package com.yahoo.elide.contrib.dynamicconfighelpers.jsonformats;

import com.github.fge.jackson.NodeType;
import com.github.fge.jsonschema.core.exceptions.ProcessingException;
import com.github.fge.jsonschema.core.report.ProcessingReport;
import com.github.fge.jsonschema.format.AbstractFormatAttribute;
import com.github.fge.jsonschema.format.FormatAttribute;
import com.github.fge.jsonschema.processors.data.FullData;
import com.github.fge.msgsimple.bundle.MessageBundle;

public class ElideFieldTypeFormatAttr extends AbstractFormatAttribute {
private static final FormatAttribute INSTANCE = new ElideFieldTypeFormatAttr();
private static final String FIELD_TYPE_REGEX = "^(?i)(Integer|Decimal|Money|Text|Coordinate|Boolean)$";

public static final String FORMAT_NAME = "elideFieldType";
public static final String TYPE_KEY = "elideFieldType.error.enum";
public static final String TYPE_MSG = "Field type [%s] is not allowed. Supported value is one of "
+ "[Integer, Decimal, Money, Text, Coordinate, Boolean].";

private ElideFieldTypeFormatAttr() {
super(FORMAT_NAME, NodeType.STRING);
}

public static FormatAttribute getInstance() {
return INSTANCE;
}

@Override
public void validate(final ProcessingReport report, final MessageBundle bundle, final FullData data)
throws ProcessingException {
final String input = data.getInstance().getNode().textValue();

if (!input.matches(FIELD_TYPE_REGEX)) {
report.error(newMsg(data, bundle, TYPE_KEY).putArgument("value", input));
}
}
}
Loading

0 comments on commit bff9eae

Please sign in to comment.