/* * Copyright 2016-present Facebook, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); you may * not use this file except in compliance with the License. You may obtain * a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations * under the License. */ package com.facebook.buck.testutil; import com.facebook.buck.util.ObjectMappers; import com.fasterxml.jackson.databind.JsonNode; import com.google.common.collect.Sets; import java.io.IOException; import java.util.HashSet; import org.hamcrest.Description; import org.hamcrest.TypeSafeDiagnosingMatcher; public class JsonMatcher extends TypeSafeDiagnosingMatcher<String> { private String expectedJson; public JsonMatcher(String json) { this.expectedJson = json; } @Override protected boolean matchesSafely(String actualJson, Description description) { try { JsonNode expectedObject = ObjectMappers.READER.readTree(ObjectMappers.createParser(String.format(expectedJson))); JsonNode actualObject = ObjectMappers.READER.readTree(ObjectMappers.createParser(String.format(actualJson))); if (!matchJsonObjects("/", expectedObject, actualObject, description)) { description.appendText(String.format(" in <%s>", actualJson)); return false; } } catch (IOException e) { description.appendText( String.format("could not parse the following into a json object: <%s>", actualJson)); return false; } return true; } /** * Static method which tries to match 2 JsonNode objects recursively. * * @param path: Path to start matching the objects from. * @param expected: First JsonNode. * @param actual: Second JsonNode. * @param description: The Description to be appended to. * @return true if the 2 objects match, false otherwise. */ private static boolean matchJsonObjects( String path, JsonNode expected, JsonNode actual, Description description) { if (expected != null && actual != null && expected.isObject()) { if (!actual.isObject()) { description.appendText(String.format("the JsonNodeType is not OBJECT at path [%s]", path)); return false; } HashSet<String> expectedFields = Sets.newHashSet(expected.fieldNames()); HashSet<String> actualFields = Sets.newHashSet(actual.fieldNames()); for (String field : expectedFields) { if (!actualFields.contains(field)) { description.appendText(String.format("expecting field [%s] at path [%s]", field, path)); return false; } if (!matchJsonObjects( path + "/" + field, expected.get(field), actual.get(field), description)) { return false; } } if (!Sets.newHashSet().equals(Sets.difference(actualFields, expectedFields))) { description.appendText( String.format( "found unexpected fields %s at path [%s]", Sets.difference(actualFields, expectedFields).toString(), path)); return false; } } if (!expected.equals(actual)) { description.appendText(String.format("mismatch at path [%s]", path)); return false; } return true; } @Override public void describeTo(Description description) { description.appendText(String.format("Json string: <%s>", expectedJson)); } }