package com.sas.unravl.assertions; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ObjectNode; import com.fasterxml.jackson.databind.node.TextNode; import com.github.fge.jsonschema.core.exceptions.ProcessingException; import com.github.fge.jsonschema.core.report.ProcessingReport; import com.github.fge.jsonschema.main.JsonSchema; import com.github.fge.jsonschema.main.JsonSchemaFactory; import com.github.fge.jsonschema.processors.syntax.SyntaxValidator; import com.sas.unravl.ApiCall; import com.sas.unravl.UnRAVL; import com.sas.unravl.UnRAVLException; import com.sas.unravl.annotations.UnRAVLAssertionPlugin; import com.sas.unravl.generators.Text; import com.sas.unravl.util.Json; import java.io.IOException; import java.util.Iterator; /** * Asserts that one or more JSON structures conform to a JSON schema. There are * several possible forms for this assertion: * * <pre> * * { "schema" : <var>schema</var> } * { "schema" : <var>schema</var>,"values" : <var>values</var> } * </pre> * * <var>schema</var> may be: * <ol> * <li>a JSON object which represents an embedded JSON schema</li> * <li>the name of a variable that contains a JSON object</li> * <li>a string in the form of "@location" where <var>location</var> is the URI * of the JSON schema. (Environment variables are expanded within the * <var>location</var> location string.)</li> * </ol> * <p> * <var>values</var> may be * <ol> * <li>a string containing a single variable (the key <code>"value"</code> may * be used instead of <code>"values"</code>) * <li>an array of variable names * </ol> * For forms 1 and 2, each such variable must be bound to a JSON object or * array. The JSON value of the variable is validated against the above * referenced JSON schema. * <p> * If <code>"values"</code> is omitted, the default value is the current * response body which is assumed to be JSON. * <p> * TThe assertion fails if any value does not conform to the JSON schema, or if * the elements do not have the forms described above or if the referenced JSON * schema is not a valid schema. * * @author David.Biesack@sas.com */ @UnRAVLAssertionPlugin("schema") public class SchemaAssertion extends BaseUnRAVLAssertion { @Override public void check(UnRAVL current, ObjectNode assertion, Stage when, ApiCall call) throws UnRAVLAssertionException, UnRAVLException { super.check(current, assertion, when, call); JsonNode schemaRef = Json.firstFieldValue(assertion); JsonNode jsonSchema = resolveSchema(current, schemaRef); JsonSchema validatingSchema = validateSchema(jsonSchema); JsonNode values = assertion.get("values"); if (values == null) { values = assertion.get("value"); } if (values == null) { JsonNode responseBody = Json.parse(Text.utf8ToString(call .getResponseBody().toByteArray())); validateValueAgainstSchema(responseBody, validatingSchema); } else if (values.isArray()) { Iterator<JsonNode> iter = values.elements(); while (iter.hasNext()) { assertSchema(call, validatingSchema, iter.next()); } } else if (values.isTextual()) { assertSchema(call, validatingSchema, values); } else { // Should we allow an object or array and validate it? throw new UnRAVLException(String.format( "Value '%s' is not a variable name in schema assertion", values)); } return; } private void assertSchema(ApiCall call, JsonSchema validatingSchema, JsonNode item) throws UnRAVLException { if (!item.isTextual()) throw new UnRAVLException(String.format( "Value '%s' is not a variable name in schema assertion", item)); String varName = item.textValue(); validateVarAgainstSchema(call, varName, validatingSchema); } private JsonNode resolveSchema(UnRAVL current, JsonNode schemaRef) throws UnRAVLException { JsonNode jsonSchema = schemaRef; // assume default - a schema object if (schemaRef.isTextual()) { String request = schemaRef.textValue(); if (request.startsWith(UnRAVL.REDIRECT_PREFIX)) { Text text; try { TextNode expanded = new TextNode(current.expand(schemaRef .textValue())); text = new Text(current, expanded); jsonSchema = Json.parse(text.text()); } catch (IOException e) { throw new UnRAVLException(String.format( "Unable to load schema from @ reference %s", schemaRef.textValue()), e); } } else { Object val = current.binding(request); if (val instanceof JsonNode) { jsonSchema = (JsonNode) val; } } } if (!jsonSchema.isObject()) { throw new UnRAVLException( String.format( "schema value %s in schema assertion is not a \"@location\", the name of a variable holding a JSON obejct, or a JSON object.", schemaRef)); } return jsonSchema; } private JsonSchema validateSchema(JsonNode jsonSchema) throws UnRAVLException { try { final JsonSchemaFactory factory = JsonSchemaFactory.byDefault(); final JsonSchema schema = factory.getJsonSchema(jsonSchema); SyntaxValidator syntaxValidator = factory.getSyntaxValidator(); if (!syntaxValidator.schemaIsValid(jsonSchema)) { throw new UnRAVLException("JSON schema is invalid"); } ProcessingReport report = syntaxValidator .validateSchema(jsonSchema); boolean success = report.isSuccess(); if (!success) { throw new UnRAVLAssertionException(report.toString()); } return schema; } catch (ProcessingException e) { throw new UnRAVLException(e); } } private void validateVarAgainstSchema(ApiCall call, String varName, JsonSchema jsonSchema) throws UnRAVLException { Object value = call.getScript().binding(varName); if (value == null || !(value instanceof JsonNode)) { throw new UnRAVLException( "responseBody is not a JSON value in schema assertion"); } validateValueAgainstSchema((JsonNode) value, jsonSchema); } private void validateValueAgainstSchema(JsonNode node, JsonSchema schema) throws UnRAVLException { try { ProcessingReport report = schema.validate(node, true); boolean success = report.isSuccess(); if (!success) { throw new UnRAVLAssertionException(report.toString()); } } catch (ProcessingException e) { throw new UnRAVLException(e); } } }