├── .editorconfig ├── .github ├── ISSUE_TEMPLATE │ └── general-issue.md └── workflows │ └── build-actions.yml ├── .gitignore ├── .mvn └── wrapper │ ├── MavenWrapperDownloader.java │ ├── maven-wrapper.jar │ └── maven-wrapper.properties ├── License.txt ├── README.md ├── custom-checkstyle.xml ├── mvnw ├── mvnw.cmd ├── pom.xml └── src ├── main └── java │ └── uk │ └── org │ └── webcompere │ └── modelassert │ └── json │ ├── Condition.java │ ├── JsonAssertions.java │ ├── JsonProvider.java │ ├── JsonProviders.java │ ├── PathWildCard.java │ ├── Patterns.java │ ├── Result.java │ ├── assertjson │ └── AssertJson.java │ ├── condition │ ├── AndCondition.java │ ├── ArrayContains.java │ ├── ConditionList.java │ ├── HasSize.java │ ├── HasValue.java │ ├── HasValueWithLooseType.java │ ├── Ignore.java │ ├── IsEmpty.java │ ├── JsonAt.java │ ├── JsonIsNotNull.java │ ├── MatcherCondition.java │ ├── MatchesTextCondition.java │ ├── MissingCondition.java │ ├── Not.java │ ├── NullCondition.java │ ├── NumberCondition.java │ ├── ObjectContainsKeys.java │ ├── OrCondition.java │ ├── PredicateWrappedCondition.java │ ├── array │ │ ├── ArrayElementCondition.java │ │ ├── ArrayElementConditionAdapter.java │ │ ├── LooseComparison.java │ │ └── Multiset.java │ └── tree │ │ ├── ArrayComparisonElementCondition.java │ │ ├── Location.java │ │ ├── PathMatch.java │ │ ├── PathMatcher.java │ │ ├── PathRule.java │ │ ├── RegexPathMatcher.java │ │ ├── StringPathMatcher.java │ │ ├── TreeComparisonCondition.java │ │ ├── TreeRule.java │ │ └── WildCardPathMatcher.java │ ├── dsl │ ├── JsonAssertDslBuilders.java │ ├── JsonNodeAssertDsl.java │ ├── Satisfies.java │ ├── SubsetDsl.java │ └── nodespecific │ │ ├── ArrayNodeDsl.java │ │ ├── ArrayNodes.java │ │ ├── BooleanNodeDsl.java │ │ ├── BooleanNodes.java │ │ ├── NumberComparisonDsl.java │ │ ├── NumberNodeDsl.java │ │ ├── NumberNodes.java │ │ ├── ObjectNodeDsl.java │ │ ├── ObjectNodes.java │ │ ├── Sizeable.java │ │ ├── TextNodeDsl.java │ │ ├── TextNodes.java │ │ ├── TreeComparisonDsl.java │ │ └── tree │ │ ├── IsEqualToDsl.java │ │ ├── PathDsl.java │ │ └── WhereDsl.java │ ├── hamcrest │ ├── HamcrestJsonAssertion.java │ └── HamcrestJsonAssertionBuilder.java │ └── impl │ ├── CoreJsonAssertion.java │ └── MemoizedSupplier.java └── test ├── java └── uk │ └── org │ └── webcompere │ └── modelassert │ └── json │ ├── ExamplesTest.java │ ├── JsonAssertionsTest.java │ ├── OverrideObjectMapper.java │ ├── OverrideObjectMapperTest.java │ ├── PatternsTest.java │ ├── TestAssertions.java │ ├── condition │ ├── AndConditionTest.java │ ├── ConditionListTest.java │ ├── HasSizeTest.java │ ├── IsEmptyTest.java │ ├── OrConditionTest.java │ └── tree │ │ ├── PathMatchTest.java │ │ └── PathMatcherTest.java │ ├── dsl │ └── nodespecific │ │ ├── ArrayNodeDslTest.java │ │ ├── BooleanNodeDslTest.java │ │ ├── JsonNodeDslTest.java │ │ ├── NumericNodeDslTest.java │ │ ├── ObjectNodeDslTest.java │ │ ├── TextNodeDslTest.java │ │ └── TreeComparisonDslTest.java │ ├── examples │ ├── PathAsserterTest.java │ └── WholeTreeUseCaseTest.java │ └── impl │ ├── CoreJsonAssertionTest.java │ └── MockitoArgumentMatcherTest.java └── resources ├── examples ├── json-alphabetic.json ├── json-array-of-objects-in-order.json ├── json-array-of-objects-out-of-order.json ├── json-array-of-objects-with-additional.json ├── json-scrambled.json ├── json-string-array-alphabetic.json ├── json-string-array-scrambled-and-additional.json ├── json-string-array-scrambled-and-missing.json ├── json-string-array-scrambled.json ├── yaml-tree-2.yml └── yaml-tree.yml ├── simple-copy.json ├── simple.json └── simple.yaml /.editorconfig: -------------------------------------------------------------------------------- 1 | # Configuration file for EditorConfig: http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | end_of_line = lf 7 | indent_style = space 8 | indent_size = 4 9 | insert_final_newline = true 10 | trim_trailing_whitespace = true 11 | 12 | [*.{xml,yml}] 13 | indent_size = 2 14 | 15 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/general-issue.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: General Issue 3 | about: Raise a bug or feature request 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | ## Summary 11 | Explain in a couple of sentences the problem/goal you have. 12 | 13 | ## Json Examples 14 | 15 | ### Simplest Input JSON 16 | Provide the simplest JSON that can illustrate the problem/goal 17 | ```json 18 | {} 19 | ``` 20 | 21 | ### Simplest comparison 22 | Provide other relevant JSON, or JSON Pointer paths 23 | 24 | ### Expected green or red test 25 | 26 | Show the simplest test code that you think should pass or fail for this to work. 27 | 28 | ```java 29 | // test code 30 | ``` 31 | 32 | ## Any other information? 33 | 34 | Do you know how to fix this? PRs welcome. 35 | -------------------------------------------------------------------------------- /.github/workflows/build-actions.yml: -------------------------------------------------------------------------------- 1 | name: Build on Push 2 | run-name: Build commit 3 | on: [push] 4 | jobs: 5 | build: 6 | runs-on: ubuntu-latest 7 | steps: 8 | - uses: actions/checkout@v4 9 | - uses: actions/setup-java@v4 10 | with: 11 | distribution: adopt-openj9 12 | java-version: 11 13 | cache: maven 14 | - run: ./mvnw clean package verify 15 | - run: CODECOV_TOKEN='6a02e79f-7d28-4a25-a863-64d9762ba037' bash <(curl -s https://codecov.io/bash) 16 | 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | .DS_Store 3 | target/ 4 | *.iml 5 | -------------------------------------------------------------------------------- /.mvn/wrapper/MavenWrapperDownloader.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2007-present the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | import java.net.*; 17 | import java.io.*; 18 | import java.nio.channels.*; 19 | import java.util.Properties; 20 | 21 | public class MavenWrapperDownloader { 22 | 23 | private static final String WRAPPER_VERSION = "0.5.6"; 24 | /** 25 | * Default URL to download the maven-wrapper.jar from, if no 'downloadUrl' is provided. 26 | */ 27 | private static final String DEFAULT_DOWNLOAD_URL = "https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/" 28 | + WRAPPER_VERSION + "/maven-wrapper-" + WRAPPER_VERSION + ".jar"; 29 | 30 | /** 31 | * Path to the maven-wrapper.properties file, which might contain a downloadUrl property to 32 | * use instead of the default one. 33 | */ 34 | private static final String MAVEN_WRAPPER_PROPERTIES_PATH = 35 | ".mvn/wrapper/maven-wrapper.properties"; 36 | 37 | /** 38 | * Path where the maven-wrapper.jar will be saved to. 39 | */ 40 | private static final String MAVEN_WRAPPER_JAR_PATH = 41 | ".mvn/wrapper/maven-wrapper.jar"; 42 | 43 | /** 44 | * Name of the property which should be used to override the default download url for the wrapper. 45 | */ 46 | private static final String PROPERTY_NAME_WRAPPER_URL = "wrapperUrl"; 47 | 48 | public static void main(String args[]) { 49 | System.out.println("- Downloader started"); 50 | File baseDirectory = new File(args[0]); 51 | System.out.println("- Using base directory: " + baseDirectory.getAbsolutePath()); 52 | 53 | // If the maven-wrapper.properties exists, read it and check if it contains a custom 54 | // wrapperUrl parameter. 55 | File mavenWrapperPropertyFile = new File(baseDirectory, MAVEN_WRAPPER_PROPERTIES_PATH); 56 | String url = DEFAULT_DOWNLOAD_URL; 57 | if(mavenWrapperPropertyFile.exists()) { 58 | FileInputStream mavenWrapperPropertyFileInputStream = null; 59 | try { 60 | mavenWrapperPropertyFileInputStream = new FileInputStream(mavenWrapperPropertyFile); 61 | Properties mavenWrapperProperties = new Properties(); 62 | mavenWrapperProperties.load(mavenWrapperPropertyFileInputStream); 63 | url = mavenWrapperProperties.getProperty(PROPERTY_NAME_WRAPPER_URL, url); 64 | } catch (IOException e) { 65 | System.out.println("- ERROR loading '" + MAVEN_WRAPPER_PROPERTIES_PATH + "'"); 66 | } finally { 67 | try { 68 | if(mavenWrapperPropertyFileInputStream != null) { 69 | mavenWrapperPropertyFileInputStream.close(); 70 | } 71 | } catch (IOException e) { 72 | // Ignore ... 73 | } 74 | } 75 | } 76 | System.out.println("- Downloading from: " + url); 77 | 78 | File outputFile = new File(baseDirectory.getAbsolutePath(), MAVEN_WRAPPER_JAR_PATH); 79 | if(!outputFile.getParentFile().exists()) { 80 | if(!outputFile.getParentFile().mkdirs()) { 81 | System.out.println( 82 | "- ERROR creating output directory '" + outputFile.getParentFile().getAbsolutePath() + "'"); 83 | } 84 | } 85 | System.out.println("- Downloading to: " + outputFile.getAbsolutePath()); 86 | try { 87 | downloadFileFromURL(url, outputFile); 88 | System.out.println("Done"); 89 | System.exit(0); 90 | } catch (Throwable e) { 91 | System.out.println("- Error downloading"); 92 | e.printStackTrace(); 93 | System.exit(1); 94 | } 95 | } 96 | 97 | private static void downloadFileFromURL(String urlString, File destination) throws Exception { 98 | if (System.getenv("MVNW_USERNAME") != null && System.getenv("MVNW_PASSWORD") != null) { 99 | String username = System.getenv("MVNW_USERNAME"); 100 | char[] password = System.getenv("MVNW_PASSWORD").toCharArray(); 101 | Authenticator.setDefault(new Authenticator() { 102 | @Override 103 | protected PasswordAuthentication getPasswordAuthentication() { 104 | return new PasswordAuthentication(username, password); 105 | } 106 | }); 107 | } 108 | URL website = new URL(urlString); 109 | ReadableByteChannel rbc; 110 | rbc = Channels.newChannel(website.openStream()); 111 | FileOutputStream fos = new FileOutputStream(destination); 112 | fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE); 113 | fos.close(); 114 | rbc.close(); 115 | } 116 | 117 | } 118 | -------------------------------------------------------------------------------- /.mvn/wrapper/maven-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webcompere/model-assert/ac0c1b3fa98231218f2d4d50ac4b72e865e951ba/.mvn/wrapper/maven-wrapper.jar -------------------------------------------------------------------------------- /.mvn/wrapper/maven-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.6.3/apache-maven-3.6.3-bin.zip 2 | wrapperUrl=https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar 3 | -------------------------------------------------------------------------------- /License.txt: -------------------------------------------------------------------------------- 1 | Copyright 2021 Ashley Frieze 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /src/main/java/uk/org/webcompere/modelassert/json/Condition.java: -------------------------------------------------------------------------------- 1 | package uk.org.webcompere.modelassert.json; 2 | 3 | import com.fasterxml.jackson.databind.JsonNode; 4 | import uk.org.webcompere.modelassert.json.condition.AndCondition; 5 | import uk.org.webcompere.modelassert.json.condition.OrCondition; 6 | 7 | import static uk.org.webcompere.modelassert.json.condition.Not.not; 8 | 9 | /** 10 | * A condition that the data must meet in order to pass the test 11 | */ 12 | public interface Condition { 13 | 14 | /** 15 | * Execute the test of the condition 16 | * @param json the json to test 17 | * @return a {@link Result} explaining whether the condition was met and if not, why not 18 | */ 19 | Result test(JsonNode json); 20 | 21 | /** 22 | * Describe the condition 23 | * @return description of the condition 24 | */ 25 | String describe(); 26 | 27 | /** 28 | * Add a second condition to this one using and logic. Short-circuits 29 | * so the second condition only evaluated if the first is true 30 | * @param other the other one 31 | * @return the combined condition 32 | */ 33 | default Condition and(Condition other) { 34 | return new AndCondition(this, other); 35 | } 36 | 37 | /** 38 | * Add a second condition to this one using or logic. Short-circuits 39 | * so the second condition only evaluated if the first is false 40 | * @param other the other one 41 | * @return the combined condition 42 | */ 43 | default Condition or(Condition other) { 44 | return new OrCondition(this, other); 45 | } 46 | 47 | /** 48 | * Invert the condition - i.e. wrap it in a not 49 | * @return the opposite of the condition 50 | */ 51 | default Condition inverted() { 52 | return not(this); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/main/java/uk/org/webcompere/modelassert/json/JsonAssertions.java: -------------------------------------------------------------------------------- 1 | package uk.org.webcompere.modelassert.json; 2 | 3 | import com.fasterxml.jackson.databind.JsonNode; 4 | import uk.org.webcompere.modelassert.json.assertjson.AssertJson; 5 | import uk.org.webcompere.modelassert.json.hamcrest.HamcrestJsonAssertionBuilder; 6 | 7 | import java.io.File; 8 | import java.nio.file.Path; 9 | 10 | import static uk.org.webcompere.modelassert.json.JsonProviders.*; 11 | 12 | /** 13 | * Jumping on point - create either an assertJson or a hamcrest. Facade/factory methods. 14 | */ 15 | public final class JsonAssertions { 16 | 17 | /** 18 | * Begin an assertJson style assertion 19 | * @param json the String json to assert 20 | * @return an {@link AssertJson} object for adding assertions to 21 | */ 22 | public static AssertJson assertJson(String json) { 23 | return new AssertJson<>(jsonStringProvider(), json); 24 | } 25 | 26 | /** 27 | * Begin an assertJson style assertion 28 | * @param jsonNode the already loaded {@link JsonNode} with the JSON to assert 29 | * @return an {@link AssertJson} object for adding assertions to 30 | */ 31 | public static AssertJson assertJson(JsonNode jsonNode) { 32 | return new AssertJson<>(node -> node, jsonNode); 33 | } 34 | 35 | /** 36 | * Begin an assertJson style assertion 37 | * @param file the file containing the json to assert 38 | * @return an {@link AssertJson} object for adding assertions to 39 | */ 40 | public static AssertJson assertJson(File file) { 41 | return new AssertJson<>(jsonFileProvider(), file); 42 | } 43 | 44 | /** 45 | * Begin an assertJson style assertion 46 | * @param path the path to the file containing the json to assert 47 | * @return an {@link AssertJson} object for adding assertions to 48 | */ 49 | public static AssertJson assertJson(Path path) { 50 | return assertJson(path.toFile()); 51 | } 52 | 53 | /** 54 | * Begin an assertJson style assertion 55 | * @param object an object to convert to JSON to start with 56 | * @return an {@link AssertJson} object for adding assertions to 57 | */ 58 | public static AssertJson assertJson(Object object) { 59 | return new AssertJson<>(jsonObjectProvider(), object); 60 | } 61 | 62 | /** 63 | * Begin a hamcrest matcher based on a json String 64 | * @return the matcher 65 | */ 66 | public static HamcrestJsonAssertionBuilder json() { 67 | return new HamcrestJsonAssertionBuilder<>(jsonStringProvider()); 68 | } 69 | 70 | /** 71 | * Begin a hamcrest matcher based on an object 72 | * @return the matcher 73 | */ 74 | public static HamcrestJsonAssertionBuilder jsonObject() { 75 | return new HamcrestJsonAssertionBuilder<>(jsonObjectProvider()); 76 | } 77 | 78 | /** 79 | * Begin a hamcrest matcher based on a json node 80 | * @return the matcher 81 | */ 82 | public static HamcrestJsonAssertionBuilder jsonNode() { 83 | return new HamcrestJsonAssertionBuilder<>(node -> node); 84 | } 85 | 86 | /** 87 | * Begin a hamcrest matcher based on a json file 88 | * @return the matcher 89 | */ 90 | public static HamcrestJsonAssertionBuilder jsonFile() { 91 | return new HamcrestJsonAssertionBuilder<>(jsonFileProvider()); 92 | } 93 | 94 | /** 95 | * Begin a hamcrest matcher based on a json file via {@link Path} 96 | * @return the matcher 97 | */ 98 | public static HamcrestJsonAssertionBuilder jsonFilePath() { 99 | return new HamcrestJsonAssertionBuilder<>(jsonPathProvider()); 100 | } 101 | 102 | /** 103 | * Begin an assertYaml style assertion 104 | * @param yaml the String yaml to assert 105 | * @return an {@link AssertJson} object for adding assertions to 106 | */ 107 | public static AssertJson assertYaml(String yaml) { 108 | return new AssertJson<>(yamlStringProvider(), yaml); 109 | } 110 | 111 | /** 112 | * Begin an assertYaml style assertion 113 | * @param file the file containing the yaml to assert 114 | * @return an {@link AssertJson} object for adding assertions to 115 | */ 116 | public static AssertJson assertYaml(File file) { 117 | return new AssertJson<>(yamlFileProvider(), file); 118 | } 119 | 120 | /** 121 | * Begin an assertYaml style assertion 122 | * @param path the path to the file containing the yaml to assert 123 | * @return an {@link AssertJson} object for adding assertions to 124 | */ 125 | public static AssertJson assertYaml(Path path) { 126 | return new AssertJson<>(yamlPathProvider(), path); 127 | } 128 | 129 | /** 130 | * Begin a hamcrest matcher based on a yaml String 131 | * @return the matcher 132 | */ 133 | public static HamcrestJsonAssertionBuilder yaml() { 134 | return new HamcrestJsonAssertionBuilder<>(yamlStringProvider()); 135 | } 136 | 137 | /** 138 | * Begin a hamcrest matcher based on a yaml file 139 | * @return the matcher 140 | */ 141 | public static HamcrestJsonAssertionBuilder yamlFile() { 142 | return new HamcrestJsonAssertionBuilder<>(yamlFileProvider()); 143 | } 144 | 145 | /** 146 | * Begin a hamcrest matcher based on a yaml file via {@link Path} 147 | * @return the matcher 148 | */ 149 | public static HamcrestJsonAssertionBuilder yamlFilePath() { 150 | return new HamcrestJsonAssertionBuilder<>(yamlPathProvider()); 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /src/main/java/uk/org/webcompere/modelassert/json/JsonProvider.java: -------------------------------------------------------------------------------- 1 | package uk.org.webcompere.modelassert.json; 2 | 3 | import com.fasterxml.jackson.databind.JsonNode; 4 | import org.opentest4j.AssertionFailedError; 5 | 6 | import java.io.IOException; 7 | 8 | /** 9 | * Provides conversion to JSON of the input 10 | * @param type of input 11 | */ 12 | @FunctionalInterface 13 | public interface JsonProvider { 14 | /** 15 | * Convert the input object into JSON 16 | * @param json the json source 17 | * @return a loaded {@link JsonNode} from the source 18 | * @throws IOException on load error 19 | */ 20 | JsonNode from(T json) throws IOException; 21 | 22 | /** 23 | * Execute the provider, wrapping exceptions up as assertion failures 24 | * @param item the item to unpack - this will be of type T 25 | * but we allow it to be Object as Hamcrest is poorly behaved with type safety 26 | * @return the loaded {@link JsonNode} or an assertion failure if the load failed, or the input is null 27 | */ 28 | default JsonNode jsonFrom(T item) { 29 | if (item == null) { 30 | return null; 31 | } 32 | try { 33 | return from((T)item); 34 | } catch (ClassCastException | IOException e) { 35 | throw new AssertionFailedError("Cannot read json of actual", e); 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/main/java/uk/org/webcompere/modelassert/json/JsonProviders.java: -------------------------------------------------------------------------------- 1 | package uk.org.webcompere.modelassert.json; 2 | 3 | import com.fasterxml.jackson.core.JsonParser; 4 | import com.fasterxml.jackson.databind.JsonNode; 5 | import com.fasterxml.jackson.databind.ObjectMapper; 6 | import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; 7 | 8 | import java.io.File; 9 | import java.nio.file.Path; 10 | import java.util.Optional; 11 | 12 | /** 13 | * Collection of built in convertions to {@link JsonNode} 14 | */ 15 | public class JsonProviders { 16 | private static final ObjectMapper DEFAULT_OBJECT_MAPPER = defaultObjectMapper(); 17 | 18 | private static final ObjectMapper DEFAULT_YAML_MAPPER = defaultYamlMapper(); 19 | 20 | public static ObjectMapper defaultObjectMapper() { 21 | return new ObjectMapper() 22 | .configure(JsonParser.Feature.ALLOW_UNQUOTED_FIELD_NAMES, true); 23 | } 24 | 25 | public static ObjectMapper defaultYamlMapper() { 26 | return new ObjectMapper(new YAMLFactory()); 27 | } 28 | 29 | private static ThreadLocal overrideObjectMapper = new ThreadLocal<>(); 30 | private static ThreadLocal overrideYamlObjectMapper = new ThreadLocal<>(); 31 | 32 | private static ObjectMapper getObjectMapper() { 33 | return Optional.ofNullable(overrideObjectMapper.get()).orElse(DEFAULT_OBJECT_MAPPER); 34 | } 35 | 36 | private static ObjectMapper getYamlObjectMapper() { 37 | return Optional.ofNullable(overrideYamlObjectMapper.get()).orElse(DEFAULT_YAML_MAPPER); 38 | } 39 | 40 | public static void overrideObjectMapper(ObjectMapper mapper) { 41 | overrideObjectMapper.set(mapper); 42 | } 43 | 44 | public static void clearObjectMapperOverride() { 45 | overrideObjectMapper.remove(); 46 | } 47 | 48 | public static void overrideYamlObjectMapper(ObjectMapper mapper) { 49 | overrideYamlObjectMapper.set(mapper); 50 | } 51 | 52 | public static void clearYamlObjectMapperOverride() { 53 | overrideYamlObjectMapper.remove(); 54 | } 55 | 56 | /** 57 | * A provider which parses String to {@link JsonNode} 58 | * @return the provider 59 | */ 60 | public static JsonProvider jsonStringProvider() { 61 | return getObjectMapper()::readTree; 62 | } 63 | 64 | /** 65 | * A provider which parses Object to {@link JsonNode} 66 | * @return the provider 67 | */ 68 | public static JsonProvider jsonObjectProvider() { 69 | return object -> getObjectMapper().convertValue(object, JsonNode.class); 70 | } 71 | 72 | /** 73 | * A provider which parses the contents of a file to {@link JsonNode} 74 | * @return the provider 75 | */ 76 | public static JsonProvider jsonFileProvider() { 77 | return getObjectMapper()::readTree; 78 | } 79 | 80 | /** 81 | * A provider which parses the contents of a file to {@link JsonNode} 82 | * @return the provider 83 | */ 84 | public static JsonProvider jsonPathProvider() { 85 | return path -> getObjectMapper().readTree(path.toFile()); 86 | } 87 | 88 | /** 89 | * A provider which parses a yaml string to {@link JsonNode} 90 | * @return the provider 91 | */ 92 | public static JsonProvider yamlStringProvider() { 93 | return getYamlObjectMapper()::readTree; 94 | } 95 | 96 | /** 97 | * A provider which parses a yaml file to {@link JsonNode} 98 | * @return the provider 99 | */ 100 | public static JsonProvider yamlFileProvider() { 101 | return getYamlObjectMapper()::readTree; 102 | } 103 | 104 | /** 105 | * A provider which parses a yaml file to {@link JsonNode} 106 | * @return the provider 107 | */ 108 | public static JsonProvider yamlPathProvider() { 109 | return path -> getYamlObjectMapper().readTree(path.toFile()); 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/main/java/uk/org/webcompere/modelassert/json/PathWildCard.java: -------------------------------------------------------------------------------- 1 | package uk.org.webcompere.modelassert.json; 2 | 3 | public enum PathWildCard { 4 | /** 5 | * Can be replaced by any single node in the tree - a field or array index 6 | */ 7 | ANY, 8 | 9 | /** 10 | * Can be replaced by any amount of subtree (0 - n) in the path 11 | */ 12 | ANY_SUBTREE, 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/uk/org/webcompere/modelassert/json/Patterns.java: -------------------------------------------------------------------------------- 1 | package uk.org.webcompere.modelassert.json; 2 | 3 | import java.util.regex.Pattern; 4 | 5 | /** 6 | * Common regular expressions for changeable patterns 7 | */ 8 | public class Patterns { 9 | 10 | /** 11 | * The standard GUID pattern, supporting upper and lowercase hex 12 | */ 13 | public static final Pattern GUID_PATTERN = 14 | Pattern.compile("[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}"); 15 | 16 | /** 17 | * ISO 8601 plain date format - e.g. 2021-06-09 18 | */ 19 | public static final Pattern ISO_8601_DATE = 20 | Pattern.compile("\\d{4}-\\d{2}-\\d{2}"); 21 | 22 | /** 23 | * ISO 8601 date/time format - e.g. 2021-06-09T20:00:09Z 24 | */ 25 | public static final Pattern ISO_8601_DATE_TIME = 26 | Pattern.compile("\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}Z"); 27 | 28 | /** 29 | * ISO 8601 zoned date time format - e.g. 2021-06-09T20:00:09+00:00 30 | */ 31 | public static final Pattern ISO_8601_ZONED_DATE_TIME = 32 | Pattern.compile("\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}[-+]\\d{2}:\\d{2}"); 33 | 34 | /** 35 | * Matches with any of the above formats 36 | */ 37 | public static final Pattern ISO_8601_DATE_ANY = 38 | Pattern.compile("\\d{4}-\\d{2}-\\d{2}(T\\d{2}:\\d{2}:\\d{2}([-+]\\d{2}:\\d{2}|Z))?"); 39 | 40 | /** 41 | * Match any integer, positive or negative 42 | */ 43 | public static final Pattern ANY_INTEGER = Pattern.compile("-?\\d+"); 44 | } 45 | -------------------------------------------------------------------------------- /src/main/java/uk/org/webcompere/modelassert/json/Result.java: -------------------------------------------------------------------------------- 1 | package uk.org.webcompere.modelassert.json; 2 | 3 | import uk.org.webcompere.modelassert.json.impl.MemoizedSupplier; 4 | 5 | import java.util.function.Supplier; 6 | 7 | /** 8 | * The result of comparison. A mutable object passed through conditions to reach a pass/fail 9 | * with some explanation of why 10 | */ 11 | public class Result { 12 | private MemoizedSupplier conditionSupplier; 13 | private String was; 14 | private boolean isPassed; 15 | 16 | /** 17 | * Construct a result 18 | * @param condition what was the condition being evaluated 19 | * @param was what the answer actually was 20 | * @param isPassed is this a success? 21 | */ 22 | public Result(String condition, String was, boolean isPassed) { 23 | this.conditionSupplier = MemoizedSupplier.of(() -> condition); 24 | this.was = was; 25 | this.isPassed = isPassed; 26 | } 27 | 28 | /** 29 | * Construct a result 30 | * @param conditionSupplier supplies the condition being evaluated 31 | * @param was what the answer actually was 32 | * @param isPassed is this a success? 33 | */ 34 | public Result(Supplier conditionSupplier, String was, boolean isPassed) { 35 | this.conditionSupplier = MemoizedSupplier.of(conditionSupplier); 36 | this.was = was; 37 | this.isPassed = isPassed; 38 | } 39 | 40 | public String getCondition() { 41 | return conditionSupplier.get(); 42 | } 43 | 44 | /** 45 | * Add a prefix to the condition description 46 | * @param condition the prefix to add 47 | * @return this for fluent calls 48 | */ 49 | public Result withPreCondition(String condition) { 50 | Supplier previousSupplier = conditionSupplier; 51 | this.conditionSupplier = MemoizedSupplier.of(() -> condition + " " + previousSupplier.get()); 52 | return this; 53 | } 54 | 55 | /** 56 | * Change the description of the condition 57 | * @param condition new description 58 | * @return this for fluent calls 59 | */ 60 | public Result withNewDescription(String condition) { 61 | this.conditionSupplier = MemoizedSupplier.of(() -> condition); 62 | return this; 63 | } 64 | 65 | public String getWas() { 66 | return was; 67 | } 68 | 69 | public boolean isPassed() { 70 | return isPassed; 71 | } 72 | 73 | /** 74 | * Flip the result 75 | * @return this for fluent calls 76 | */ 77 | public Result invert() { 78 | isPassed = !isPassed; 79 | return this; 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/main/java/uk/org/webcompere/modelassert/json/assertjson/AssertJson.java: -------------------------------------------------------------------------------- 1 | package uk.org.webcompere.modelassert.json.assertjson; 2 | 3 | import com.fasterxml.jackson.databind.JsonNode; 4 | import org.opentest4j.AssertionFailedError; 5 | import uk.org.webcompere.modelassert.json.Condition; 6 | import uk.org.webcompere.modelassert.json.JsonProvider; 7 | import uk.org.webcompere.modelassert.json.Result; 8 | import uk.org.webcompere.modelassert.json.impl.CoreJsonAssertion; 9 | 10 | /** 11 | * The assertJson form of the assertion 12 | * @param the type of JSON source passed in 13 | */ 14 | public class AssertJson extends CoreJsonAssertion> { 15 | private JsonNode converted; 16 | 17 | /** 18 | * Constructs an assertion - use the assertJson factory method instead 19 | * @param jsonProvider the provider of the json, converting from source type to {@link JsonNode} 20 | * @param source the source value 21 | */ 22 | public AssertJson(JsonProvider jsonProvider, T source) { 23 | super(jsonProvider); 24 | 25 | converted = jsonProvider.jsonFrom(source); 26 | } 27 | 28 | @Override 29 | public AssertJson satisfies(Condition condition) { 30 | // execute this comparison 31 | Result result = condition.test(converted); 32 | if (!result.isPassed()) { 33 | throw new AssertionFailedError("Expected: " + condition.describe() + 34 | "\n but: " + result.getCondition() + " was " + result.getWas(), 35 | result.getCondition(), result.getWas()); 36 | } 37 | 38 | return super.satisfies(condition); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/main/java/uk/org/webcompere/modelassert/json/condition/AndCondition.java: -------------------------------------------------------------------------------- 1 | package uk.org.webcompere.modelassert.json.condition; 2 | 3 | import com.fasterxml.jackson.databind.JsonNode; 4 | import uk.org.webcompere.modelassert.json.Condition; 5 | import uk.org.webcompere.modelassert.json.Result; 6 | 7 | /** 8 | * Ands two conditions together from left to right, with short-circuiting 9 | */ 10 | public class AndCondition implements Condition { 11 | private Condition first; 12 | private Condition second; 13 | 14 | public AndCondition(Condition first, Condition second) { 15 | this.first = first; 16 | this.second = second; 17 | } 18 | 19 | @Override 20 | public Result test(JsonNode json) { 21 | Result firstResult = first.test(json); 22 | if (!firstResult.isPassed()) { 23 | return firstResult.withNewDescription(describe()); 24 | } 25 | return second.test(json).withNewDescription(describe()); 26 | } 27 | 28 | @Override 29 | public String describe() { 30 | return first.describe() + " and " + second.describe(); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/uk/org/webcompere/modelassert/json/condition/ArrayContains.java: -------------------------------------------------------------------------------- 1 | package uk.org.webcompere.modelassert.json.condition; 2 | 3 | import com.fasterxml.jackson.databind.JsonNode; 4 | import com.fasterxml.jackson.databind.node.ArrayNode; 5 | import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; 6 | import uk.org.webcompere.modelassert.json.Condition; 7 | import uk.org.webcompere.modelassert.json.Result; 8 | import uk.org.webcompere.modelassert.json.condition.array.LooseComparison; 9 | 10 | import java.util.Arrays; 11 | import java.util.List; 12 | import java.util.Objects; 13 | import java.util.stream.IntStream; 14 | import java.util.stream.Stream; 15 | 16 | import static java.util.stream.Collectors.joining; 17 | import static java.util.stream.Collectors.toList; 18 | 19 | /** 20 | * Array content conditions 21 | */ 22 | public class ArrayContains implements Condition { 23 | private String description; 24 | private List arrayElementConditions; 25 | private boolean requireStrict; 26 | 27 | /** 28 | * Construct the array contains condition 29 | * @param description the name to use - as simple as possible 30 | * @param arrayElementConditions the per element conditions 31 | * @param requireStrict whether the conditions are executed in strict order 32 | */ 33 | @SuppressFBWarnings("EI_EXPOSE_REP2") 34 | public ArrayContains(String description, List arrayElementConditions, boolean requireStrict) { 35 | this.description = description; 36 | this.arrayElementConditions = arrayElementConditions; 37 | this.requireStrict = requireStrict; 38 | } 39 | 40 | /** 41 | * Create the condition to match the provided values in the array 42 | * @param first first value 43 | * @param rest remaining values 44 | * @return the {@link ArrayContains} to find these values within the array 45 | */ 46 | public static ArrayContains containsValues(Object first, Object... rest) { 47 | Object[] remainder = correctRestForNulls(rest); 48 | return new ArrayContains(describe(first, remainder), 49 | toConditions(first, remainder), false); 50 | } 51 | 52 | /** 53 | * Create the condition to match the provided conditions in the array 54 | * @param conditions the conditions 55 | * @return the {@link ArrayContains} to find elements matching these conditions 56 | */ 57 | public static ArrayContains containsValues(ConditionList conditions) { 58 | if (conditions == null) { 59 | // accidental leak into ConditionList overload when the caller must have meant a null value 60 | return containsValues((Object)null); 61 | } 62 | return new ArrayContains(describe(conditions), 63 | conditions.getConditionList(), false); 64 | } 65 | 66 | /** 67 | * Create the condition to match the provided values in the array in exact order 68 | * @param first first value 69 | * @param rest remaining values 70 | * @return the {@link ArrayContains} to match the array with these values 71 | */ 72 | public static ArrayContains containsValuesExactly(Object first, Object... rest) { 73 | Object[] remainder = correctRestForNulls(rest); 74 | return new ArrayContains(describe(first, remainder), 75 | toConditions(first, remainder), true); 76 | } 77 | 78 | /** 79 | * Create the condition to match the provided condition list in the array in exact order 80 | * @param conditions conditions 81 | * @return the {@link ArrayContains} to match the array with these conditions 82 | */ 83 | public static ArrayContains containsValuesExactly(ConditionList conditions) { 84 | return new ArrayContains(describe(conditions), 85 | conditions.getConditionList(), true); 86 | } 87 | 88 | private static Object[] correctRestForNulls(Object[] rest) { 89 | if (rest != null) { 90 | return rest; 91 | } 92 | return new Object[] { null }; 93 | } 94 | 95 | /** 96 | * Execute the test of the condition 97 | * 98 | * @param json the json to test 99 | * @return a {@link Result} explaining whether the condition was met and if not, why not 100 | */ 101 | @Override 102 | public Result test(JsonNode json) { 103 | // must be an array node 104 | if (!json.isArray() || !(json instanceof ArrayNode)) { 105 | return new Result(describe(), json.getNodeType().toString(), false); 106 | } 107 | 108 | ArrayNode arrayNode = (ArrayNode) json; 109 | if (requireStrict) { 110 | return strictComparison(arrayNode); 111 | } 112 | return looseComparison(arrayNode); 113 | } 114 | 115 | private Result looseComparison(ArrayNode arrayNode) { 116 | return LooseComparison.fromConditions(arrayElementConditions, () -> description) 117 | .looseComparison(arrayNode); 118 | } 119 | 120 | private Result strictComparison(ArrayNode arrayNode) { 121 | if (arrayNode.size() != arrayElementConditions.size()) { 122 | return new Result(describe(), "different number of elements: found " + arrayNode.size() + 123 | " rather than expected " + arrayElementConditions.size(), false); 124 | } 125 | 126 | Result[] results = new Result[arrayElementConditions.size()]; 127 | for (int i = 0; i < arrayElementConditions.size(); i++) { 128 | results[i] = arrayElementConditions.get(i).test(arrayNode.get(i)); 129 | } 130 | 131 | if (Arrays.stream(results).allMatch(Result::isPassed)) { 132 | return new Result(describe(), "all matched", true); 133 | } 134 | 135 | return new Result(describe(), explainFailures(results), false); 136 | } 137 | 138 | private String explainFailures(Result[] results) { 139 | return IntStream.range(0, results.length) 140 | .filter(index -> !results[index].isPassed()) 141 | .mapToObj(index -> "Index " + index + " expected " + 142 | results[index].getCondition() + " but was " + results[index].getWas()) 143 | .collect(joining("\n")); 144 | } 145 | 146 | /** 147 | * Describe the condition 148 | * 149 | * @return description of the condition 150 | */ 151 | @Override 152 | public String describe() { 153 | return description + (requireStrict ? " in exact order" : ""); 154 | } 155 | 156 | private static String describe(Object first, Object... rest) { 157 | return "Array with values " + Stream.concat(Stream.of(first), Arrays.stream(rest)) 158 | .map(Objects::toString) 159 | .collect(toList()); 160 | } 161 | 162 | private static String describe(ConditionList conditionList) { 163 | return "Array where:\n" + IntStream.range(0, conditionList.getConditionList().size()) 164 | .mapToObj(idx -> "Index " + idx + ": " + conditionList.getConditionList().get(idx).describe()) 165 | .collect(joining("\n")); 166 | } 167 | 168 | private static List toConditions(Object first, Object... rest) { 169 | return Stream.concat(Stream.of(first), Arrays.stream(rest)) 170 | .map(HasValueWithLooseType::new) 171 | .collect(toList()); 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /src/main/java/uk/org/webcompere/modelassert/json/condition/ConditionList.java: -------------------------------------------------------------------------------- 1 | package uk.org.webcompere.modelassert.json.condition; 2 | 3 | import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; 4 | import uk.org.webcompere.modelassert.json.Condition; 5 | import uk.org.webcompere.modelassert.json.dsl.JsonNodeAssertDsl; 6 | 7 | import java.util.LinkedList; 8 | import java.util.List; 9 | 10 | /** 11 | * A set of conditions for matching 12 | */ 13 | public class ConditionList implements JsonNodeAssertDsl { 14 | private List conditionList = new LinkedList<>(); 15 | 16 | /** 17 | * Create a list of conditions using the DSL 18 | * @return the conditions 19 | */ 20 | public static ConditionList conditions() { 21 | return new ConditionList(); 22 | } 23 | 24 | /** 25 | * Get the conditions 26 | * @return 27 | */ 28 | @SuppressFBWarnings("EI_EXPOSE_REP") 29 | public List getConditionList() { 30 | return conditionList; 31 | } 32 | 33 | @Override 34 | public ConditionList satisfies(Condition condition) { 35 | conditionList.add(condition); 36 | return this; 37 | } 38 | 39 | /** 40 | * Convert the list into a composite AND condition 41 | * @return {@link Condition} 42 | */ 43 | public Condition toCondition() { 44 | if (conditionList.isEmpty()) { 45 | throw new IllegalArgumentException("Cannot create an empty condition"); 46 | } 47 | 48 | if (conditionList.size() == 1) { 49 | return conditionList.get(0); 50 | } 51 | 52 | Condition lastCondition = conditionList.get(0); 53 | for (int i = 1; i < conditionList.size(); i++) { 54 | lastCondition = lastCondition.and(conditionList.get(i)); 55 | } 56 | return lastCondition; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/main/java/uk/org/webcompere/modelassert/json/condition/HasSize.java: -------------------------------------------------------------------------------- 1 | package uk.org.webcompere.modelassert.json.condition; 2 | 3 | import com.fasterxml.jackson.databind.JsonNode; 4 | import com.fasterxml.jackson.databind.node.ArrayNode; 5 | import com.fasterxml.jackson.databind.node.IntNode; 6 | import com.fasterxml.jackson.databind.node.ObjectNode; 7 | import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; 8 | import uk.org.webcompere.modelassert.json.Condition; 9 | import uk.org.webcompere.modelassert.json.Result; 10 | import uk.org.webcompere.modelassert.json.dsl.Satisfies; 11 | import uk.org.webcompere.modelassert.json.dsl.nodespecific.NumberComparisonDsl; 12 | 13 | /** 14 | * A size condition - works out the size of the target 15 | */ 16 | public class HasSize implements Condition { 17 | private Condition sizeCondition; 18 | 19 | public HasSize(int requiredSize) { 20 | this.sizeCondition = new NumberCondition<>(requiredSize, NumberCondition.Comparison.EQUAL_TO); 21 | } 22 | 23 | private HasSize(Condition sizeCondition) { 24 | this.sizeCondition = sizeCondition; 25 | } 26 | 27 | /** 28 | * Switch to Size Condition building context to allow the size to be spec 29 | * @param parentAssertion the parent assertion to resolve to, when the size is provided 30 | * @param the type of resulting assertion 31 | * @return a {@link NumberComparisonDsl} for stipulating the size 32 | */ 33 | public static NumberComparisonDsl sizeOf(Satisfies parentAssertion) { 34 | return condition -> parentAssertion.satisfies(new HasSize(condition)); 35 | } 36 | 37 | @SuppressFBWarnings(value = "BC_UNCONFIRMED_CAST", 38 | justification = "Node type is used in place of instanceof for the switch") 39 | @Override 40 | public Result test(JsonNode json) { 41 | switch (json.getNodeType()) { 42 | case ARRAY: 43 | ArrayNode arrayNode = (ArrayNode) json; 44 | return new Result(describe(), "Array of size " + arrayNode.size(), testNumber(arrayNode.size())); 45 | case OBJECT: 46 | ObjectNode objectNode = (ObjectNode) json; 47 | return new Result(describe(), "Object of size " + objectNode.size(), testNumber(objectNode.size())); 48 | case STRING: 49 | return new Result(describe(), "Text of length " + json.asText().length(), 50 | testNumber(json.asText().length())); 51 | default: 52 | return new Result(describe(), json.getNodeType().toString(), false); 53 | } 54 | } 55 | 56 | private boolean testNumber(int number) { 57 | return sizeCondition.test(new IntNode(number)).isPassed(); 58 | } 59 | 60 | @Override 61 | public String describe() { 62 | return "string, array or object where size " + sizeCondition.describe(); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/main/java/uk/org/webcompere/modelassert/json/condition/HasValue.java: -------------------------------------------------------------------------------- 1 | package uk.org.webcompere.modelassert.json.condition; 2 | 3 | import com.fasterxml.jackson.databind.JsonNode; 4 | import uk.org.webcompere.modelassert.json.Condition; 5 | import uk.org.webcompere.modelassert.json.Result; 6 | 7 | import java.util.Objects; 8 | import java.util.function.Function; 9 | 10 | /** 11 | * Specific typed "hasValue" comparison 12 | * @param the type of value 13 | */ 14 | public class HasValue implements Condition { 15 | private Function valueExtractor; 16 | private V expected; 17 | 18 | /** 19 | * Construct the condition 20 | * @param valueExtractor how to extract the right sort of value from the {@link JsonNode} 21 | * @param expected the expected value 22 | */ 23 | public HasValue(Function valueExtractor, V expected) { 24 | this.valueExtractor = valueExtractor; 25 | this.expected = expected; 26 | } 27 | 28 | @Override 29 | public Result test(JsonNode json) { 30 | V extracted = valueExtractor.apply(json); 31 | return new Result(describe(), extracted.toString(), Objects.equals(expected, extracted)); 32 | } 33 | 34 | @Override 35 | public String describe() { 36 | return "equal to " + expected.toString(); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/main/java/uk/org/webcompere/modelassert/json/condition/HasValueWithLooseType.java: -------------------------------------------------------------------------------- 1 | package uk.org.webcompere.modelassert.json.condition; 2 | 3 | import com.fasterxml.jackson.databind.JsonNode; 4 | import uk.org.webcompere.modelassert.json.Condition; 5 | import uk.org.webcompere.modelassert.json.Result; 6 | 7 | /** 8 | * Performs the hasValue operation with flexibility and type flexibility. It works 9 | * out the likely type of node from the input. 10 | */ 11 | public class HasValueWithLooseType implements Condition { 12 | private Object expected; 13 | 14 | /** 15 | * Construct with the expected value 16 | * @param expected the value expected 17 | */ 18 | public HasValueWithLooseType(Object expected) { 19 | this.expected = expected; 20 | } 21 | 22 | @Override 23 | public Result test(JsonNode node) { 24 | String was = node.toString(); 25 | if (expected == null && node.isNull()) { 26 | return new Result(describe(), was, true); 27 | } 28 | 29 | if (expected == null) { 30 | return new Result(describe(), was, false); 31 | } 32 | 33 | if (expected instanceof Number) { 34 | if (node.isNumber()) { 35 | return new Result(describe(), was, node.numberValue().equals(expected)); 36 | } 37 | return new Result(describe(), was, false); 38 | } 39 | 40 | if (expected instanceof String && node.isTextual()) { 41 | return new Result(describe(), was, node.textValue().equals(expected)); 42 | } 43 | 44 | if (expected instanceof Boolean && node.isBoolean()) { 45 | return new Result(describe(), was, node.booleanValue() == (Boolean)expected); 46 | } 47 | 48 | return new Result(describe(), was, false); 49 | } 50 | 51 | @Override 52 | public String describe() { 53 | return "is equal to " + expected; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/main/java/uk/org/webcompere/modelassert/json/condition/Ignore.java: -------------------------------------------------------------------------------- 1 | package uk.org.webcompere.modelassert.json.condition; 2 | 3 | import com.fasterxml.jackson.databind.JsonNode; 4 | import uk.org.webcompere.modelassert.json.Condition; 5 | import uk.org.webcompere.modelassert.json.Result; 6 | 7 | /** 8 | * This condition always passes, ignoring its input 9 | */ 10 | public class Ignore implements Condition { 11 | 12 | @Override 13 | public Result test(JsonNode json) { 14 | return new Result(describe(), "", true); 15 | } 16 | 17 | @Override 18 | public String describe() { 19 | return "Ignored"; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/uk/org/webcompere/modelassert/json/condition/IsEmpty.java: -------------------------------------------------------------------------------- 1 | package uk.org.webcompere.modelassert.json.condition; 2 | 3 | import com.fasterxml.jackson.databind.JsonNode; 4 | import com.fasterxml.jackson.databind.node.ArrayNode; 5 | import com.fasterxml.jackson.databind.node.ObjectNode; 6 | import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; 7 | import uk.org.webcompere.modelassert.json.Condition; 8 | import uk.org.webcompere.modelassert.json.Result; 9 | 10 | /** 11 | * Detect whether a node is present, but empty 12 | */ 13 | public class IsEmpty implements Condition { 14 | 15 | @SuppressFBWarnings(value = "BC_UNCONFIRMED_CAST", 16 | justification = "Node type is used in place of instanceof for the switch") 17 | @Override 18 | public Result test(JsonNode json) { 19 | switch (json.getNodeType()) { 20 | case ARRAY: 21 | ArrayNode arrayNode = (ArrayNode) json; 22 | return new Result(describe(), "Array of size " + arrayNode.size(), arrayNode.size() == 0); 23 | case OBJECT: 24 | ObjectNode objectNode = (ObjectNode) json; 25 | return new Result(describe(), "Object of size " + objectNode.size(), objectNode.size() == 0); 26 | case STRING: 27 | return new Result(describe(), json.asText(), json.asText().isEmpty()); 28 | default: 29 | return new Result(describe(), json.getNodeType().toString(), false); 30 | } 31 | } 32 | 33 | @Override 34 | public String describe() { 35 | return "empty string, array or object"; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/uk/org/webcompere/modelassert/json/condition/JsonAt.java: -------------------------------------------------------------------------------- 1 | package uk.org.webcompere.modelassert.json.condition; 2 | 3 | import com.fasterxml.jackson.databind.JsonNode; 4 | import uk.org.webcompere.modelassert.json.Condition; 5 | import uk.org.webcompere.modelassert.json.Result; 6 | 7 | /** 8 | * The assertion of some JSON at a node within the tree 9 | */ 10 | public class JsonAt implements Condition { 11 | private String path; 12 | private Condition condition; 13 | 14 | /** 15 | * Construct with the location in the tree to at 16 | * along with the condition to meet 17 | * @param path the path in the tree - JSON Pointer 18 | * @param condition the {@link Condition} to satisfy 19 | */ 20 | public JsonAt(String path, Condition condition) { 21 | this.path = path; 22 | this.condition = condition; 23 | } 24 | 25 | @Override 26 | public Result test(JsonNode json) { 27 | JsonNode node = json.at(path); 28 | return condition.test(node).withPreCondition(path); 29 | } 30 | 31 | @Override 32 | public String describe() { 33 | return "Path at " + path + " " + condition.describe(); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/main/java/uk/org/webcompere/modelassert/json/condition/JsonIsNotNull.java: -------------------------------------------------------------------------------- 1 | package uk.org.webcompere.modelassert.json.condition; 2 | 3 | import com.fasterxml.jackson.databind.JsonNode; 4 | import uk.org.webcompere.modelassert.json.Condition; 5 | import uk.org.webcompere.modelassert.json.Result; 6 | 7 | public class JsonIsNotNull implements Condition { 8 | /** 9 | * Execute the test of the condition 10 | * 11 | * @param json the json to test 12 | * @return a {@link Result} explaining whether the condition was met and if not, why not 13 | */ 14 | @Override 15 | public Result test(JsonNode json) { 16 | return new Result(describe(), json == null ? "null" : "non null", json != null); 17 | } 18 | 19 | /** 20 | * Describe the condition 21 | * 22 | * @return description of the condition 23 | */ 24 | @Override 25 | public String describe() { 26 | return "Json is not null"; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/uk/org/webcompere/modelassert/json/condition/MatcherCondition.java: -------------------------------------------------------------------------------- 1 | package uk.org.webcompere.modelassert.json.condition; 2 | 3 | import com.fasterxml.jackson.databind.JsonNode; 4 | import org.hamcrest.Matcher; 5 | import org.hamcrest.StringDescription; 6 | import uk.org.webcompere.modelassert.json.Condition; 7 | import uk.org.webcompere.modelassert.json.Result; 8 | 9 | public class MatcherCondition implements Condition { 10 | private Matcher matcher; 11 | 12 | public MatcherCondition(Matcher matcher) { 13 | this.matcher = matcher; 14 | } 15 | 16 | /** 17 | * Execute the test of the condition 18 | * 19 | * @param json the json to test 20 | * @return a {@link Result} explaining whether the condition was met and if not, why not 21 | */ 22 | @Override 23 | public Result test(JsonNode json) { 24 | boolean result = matcher.matches(json); 25 | StringDescription misMatchDescription = new StringDescription(); 26 | if (!result) { 27 | matcher.describeMismatch(json, misMatchDescription); 28 | } 29 | return new Result(describe(), misMatchDescription.toString(), result); 30 | } 31 | 32 | /** 33 | * Describe the condition 34 | * 35 | * @return description of the condition 36 | */ 37 | @Override 38 | public String describe() { 39 | StringDescription description = new StringDescription(); 40 | matcher.describeTo(description); 41 | return description.toString(); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/main/java/uk/org/webcompere/modelassert/json/condition/MatchesTextCondition.java: -------------------------------------------------------------------------------- 1 | package uk.org.webcompere.modelassert.json.condition; 2 | 3 | import com.fasterxml.jackson.databind.JsonNode; 4 | import uk.org.webcompere.modelassert.json.Condition; 5 | import uk.org.webcompere.modelassert.json.Result; 6 | 7 | import java.util.function.Predicate; 8 | import java.util.regex.Pattern; 9 | 10 | /** 11 | * A text node matches a regular expression 12 | */ 13 | public class MatchesTextCondition implements Condition { 14 | private Predicate predicate; 15 | private String conditionName; 16 | 17 | /** 18 | * Create a condition which requires the text to meet the pattern 19 | * @param pattern regex that must match 20 | */ 21 | public MatchesTextCondition(Pattern pattern) { 22 | this(pattern, "Text must match " + pattern.pattern()); 23 | } 24 | 25 | /** 26 | * Create a condition which requires the text to meet the pattern 27 | * @param pattern regex that must match 28 | * @param conditionName the name of the condition for the description 29 | */ 30 | public MatchesTextCondition(Pattern pattern, String conditionName) { 31 | this.predicate = text -> pattern.matcher(text).matches(); 32 | this.conditionName = conditionName; 33 | } 34 | 35 | /** 36 | * Create a custom text condition 37 | * @param conditionName the name of the condition 38 | * @param predicate the predicate to meet on the text 39 | */ 40 | public MatchesTextCondition(String conditionName, Predicate predicate) { 41 | this.predicate = predicate; 42 | this.conditionName = conditionName; 43 | } 44 | 45 | /** 46 | * Build a text contains condition 47 | * @param contains the substring to search 48 | * @return a new {@link MatchesTextCondition} condition 49 | */ 50 | public static MatchesTextCondition textContains(String contains) { 51 | return new MatchesTextCondition(Pattern.compile(".*" + Pattern.quote(contains) + ".*"), 52 | "Text contains " + contains); 53 | } 54 | 55 | /** 56 | * Build a text starts with condition 57 | * @param substring the substring to search 58 | * @return a new {@link MatchesTextCondition} condition 59 | */ 60 | public static MatchesTextCondition textStartsWith(String substring) { 61 | return new MatchesTextCondition(Pattern.compile(Pattern.quote(substring) + ".*"), 62 | "Text starts with " + substring); 63 | } 64 | 65 | /** 66 | * Build a matcher condition 67 | * @param pattern the pattern to match 68 | * @return a new {@link MatchesTextCondition} condition 69 | */ 70 | public static MatchesTextCondition textMatches(Pattern pattern) { 71 | return new MatchesTextCondition(pattern); 72 | } 73 | 74 | /** 75 | * Execute the test of the condition 76 | * 77 | * @param json the json to test 78 | * @return a {@link Result} explaining whether the condition was met and if not, why not 79 | */ 80 | @Override 81 | public Result test(JsonNode json) { 82 | if (!json.isTextual()) { 83 | return new Result(describe(), json.getNodeType().toString(), false); 84 | } 85 | 86 | String text = json.asText(); 87 | return new Result(describe(), text, predicate.test(text)); 88 | } 89 | 90 | /** 91 | * Describe the condition 92 | * 93 | * @return description of the condition 94 | */ 95 | @Override 96 | public String describe() { 97 | return conditionName; 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/main/java/uk/org/webcompere/modelassert/json/condition/MissingCondition.java: -------------------------------------------------------------------------------- 1 | package uk.org.webcompere.modelassert.json.condition; 2 | 3 | import com.fasterxml.jackson.databind.JsonNode; 4 | import uk.org.webcompere.modelassert.json.Condition; 5 | import uk.org.webcompere.modelassert.json.Result; 6 | 7 | public class MissingCondition implements Condition { 8 | private static final MissingCondition INSTANCE = new MissingCondition(); 9 | 10 | private MissingCondition() { 11 | 12 | } 13 | 14 | /** 15 | * Access the one and only instance 16 | * @return the MissingCondition instnace 17 | */ 18 | public static MissingCondition getInstance() { 19 | return INSTANCE; 20 | } 21 | 22 | @Override 23 | public Result test(JsonNode json) { 24 | return new Result(describe(), json != null ? json.getNodeType().toString() : "null", 25 | json == null || json.isMissingNode()); 26 | } 27 | 28 | /** 29 | * Describe the condition 30 | * 31 | * @return description of the condition 32 | */ 33 | @Override 34 | public String describe() { 35 | return "missing"; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/uk/org/webcompere/modelassert/json/condition/Not.java: -------------------------------------------------------------------------------- 1 | package uk.org.webcompere.modelassert.json.condition; 2 | 3 | import com.fasterxml.jackson.databind.JsonNode; 4 | import uk.org.webcompere.modelassert.json.Condition; 5 | import uk.org.webcompere.modelassert.json.Result; 6 | 7 | /** 8 | * Inverts a condition. Create using the {@link Not#not(Condition)} factory method 9 | */ 10 | public class Not implements Condition { 11 | private static final String NOT_PREFIX = "not"; 12 | private Condition delegate; 13 | 14 | public static Not not(Condition condition) { 15 | return new Not(condition); 16 | } 17 | 18 | private Not(Condition delegate) { 19 | this.delegate = delegate; 20 | } 21 | 22 | 23 | /** 24 | * Execute the test of the condition 25 | * 26 | * @param json the json to test 27 | * @return a {@link Result} explaining whether the condition was met and if not, why not 28 | */ 29 | @Override 30 | public Result test(JsonNode json) { 31 | return delegate.test(json) 32 | .invert() 33 | .withPreCondition(NOT_PREFIX); 34 | } 35 | 36 | /** 37 | * Describe the condition 38 | * 39 | * @return description of the condition 40 | */ 41 | @Override 42 | public String describe() { 43 | return NOT_PREFIX + " " + delegate.describe(); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/main/java/uk/org/webcompere/modelassert/json/condition/NullCondition.java: -------------------------------------------------------------------------------- 1 | package uk.org.webcompere.modelassert.json.condition; 2 | 3 | import com.fasterxml.jackson.databind.JsonNode; 4 | import uk.org.webcompere.modelassert.json.Condition; 5 | import uk.org.webcompere.modelassert.json.Result; 6 | 7 | public class NullCondition implements Condition { 8 | private static final NullCondition INSTANCE = new NullCondition(); 9 | 10 | private NullCondition() { 11 | 12 | } 13 | 14 | /** 15 | * Access the one and only instance 16 | * @return the NullCondition instnace 17 | */ 18 | public static NullCondition getInstance() { 19 | return INSTANCE; 20 | } 21 | 22 | @Override 23 | public Result test(JsonNode json) { 24 | return new Result("null", json.getNodeType().toString(), json.isNull()); 25 | } 26 | 27 | /** 28 | * Describe the condition 29 | * 30 | * @return description of the condition 31 | */ 32 | @Override 33 | public String describe() { 34 | return "JSON must be null"; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/uk/org/webcompere/modelassert/json/condition/NumberCondition.java: -------------------------------------------------------------------------------- 1 | package uk.org.webcompere.modelassert.json.condition; 2 | 3 | import com.fasterxml.jackson.databind.JsonNode; 4 | import uk.org.webcompere.modelassert.json.Condition; 5 | import uk.org.webcompere.modelassert.json.Result; 6 | 7 | import java.util.Arrays; 8 | 9 | /** 10 | * Numeric comparisons 11 | * @param the type of number being compared with 12 | */ 13 | public class NumberCondition implements Condition { 14 | public enum Comparison { 15 | NONE("correct type", new Comparison[0]), 16 | EQUAL_TO("equal to", new Comparison[0]), 17 | GREATER_THAN("greater than", new Comparison[0]), 18 | GREATER_THAN_OR_EQUAL("greater than or equal to", new Comparison[] { GREATER_THAN, EQUAL_TO }), 19 | LESS_THAN("less than", new Comparison[0]), 20 | LESS_THAN_OR_EQUAL("less than or equal to", new Comparison[] { LESS_THAN, EQUAL_TO }); 21 | 22 | private String name; 23 | private Comparison[] alternatives; 24 | 25 | Comparison(String name, Comparison[] alternatives) { 26 | this.name = name; 27 | this.alternatives = alternatives; 28 | } 29 | } 30 | 31 | private Comparison comparison; 32 | private N expected; 33 | private Class requiredType; 34 | 35 | /** 36 | * Construct a number condition for any numeric type 37 | * @param expected the expected value 38 | * @param comparison the comparison to be true 39 | */ 40 | public NumberCondition(N expected, Comparison comparison) { 41 | this.comparison = comparison; 42 | this.requiredType = Number.class; 43 | this.expected = expected; 44 | } 45 | 46 | /** 47 | * Construct a number condition for a specific numeric type 48 | * @param requiredType the type of number the value must be 49 | * @param expected the expected value 50 | * @param comparison the comparison to be true 51 | */ 52 | public NumberCondition(Class requiredType, N expected, Comparison comparison) { 53 | this.comparison = comparison; 54 | this.expected = expected; 55 | this.requiredType = requiredType; 56 | } 57 | 58 | @Override 59 | public Result test(JsonNode json) { 60 | return new Result(describe(), json.asText(), nodePasses(json)); 61 | } 62 | 63 | private boolean nodePasses(JsonNode json) { 64 | return passesTypeTest(json) && passesNumericTest(json); 65 | } 66 | 67 | private boolean passesTypeTest(JsonNode json) { 68 | if (comparison == Comparison.NONE) { 69 | if (requiredType.equals(Integer.class)) { 70 | return json.isInt(); 71 | } 72 | if (requiredType.equals(Long.class)) { 73 | return json.isLong(); 74 | } 75 | } 76 | if (requiredType.equals(Integer.class)) { 77 | return json.canConvertToInt(); 78 | } 79 | if (requiredType.equals(Long.class)) { 80 | return json.canConvertToLong(); 81 | } 82 | if (requiredType.equals(Double.class)) { 83 | return json.isDouble(); 84 | } 85 | return json.isNumber(); 86 | } 87 | 88 | private boolean passesNumericTest(JsonNode json) { 89 | if (comparison == Comparison.NONE) { 90 | return true; 91 | } 92 | Comparison ordering = getNumericComparison(json); 93 | return comparison == ordering || Arrays.stream(comparison.alternatives).anyMatch(ordering::equals); 94 | } 95 | 96 | private Comparison getNumericComparison(JsonNode json) { 97 | if (json.isDouble() || requiredType.equals(Double.class)) { 98 | return doubleComparison(json.asDouble()); 99 | } 100 | if (json.isLong() || requiredType.equals(Long.class)) { 101 | return longComparison(json.asLong()); 102 | } 103 | return intComparison(json.asInt()); 104 | } 105 | 106 | private Comparison doubleComparison(double asDouble) { 107 | double expectedDouble = expected.doubleValue(); 108 | if (asDouble == expectedDouble) { 109 | return Comparison.EQUAL_TO; 110 | } 111 | if (asDouble > expectedDouble) { 112 | return Comparison.GREATER_THAN; 113 | } 114 | return Comparison.LESS_THAN; 115 | } 116 | 117 | private Comparison longComparison(long asLong) { 118 | long expectedLong = expected.longValue(); 119 | if (asLong == expectedLong) { 120 | return Comparison.EQUAL_TO; 121 | } 122 | if (asLong > expectedLong) { 123 | return Comparison.GREATER_THAN; 124 | } 125 | return Comparison.LESS_THAN; 126 | } 127 | 128 | private Comparison intComparison(long asInt) { 129 | int expectedInt = expected.intValue(); 130 | if (asInt == expectedInt) { 131 | return Comparison.EQUAL_TO; 132 | } 133 | if (asInt > expectedInt) { 134 | return Comparison.GREATER_THAN; 135 | } 136 | return Comparison.LESS_THAN; 137 | } 138 | 139 | @Override 140 | public String describe() { 141 | return "is " + requiredType.getTypeName() + " " + comparison.name + " " + expected; 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /src/main/java/uk/org/webcompere/modelassert/json/condition/ObjectContainsKeys.java: -------------------------------------------------------------------------------- 1 | package uk.org.webcompere.modelassert.json.condition; 2 | 3 | import com.fasterxml.jackson.databind.JsonNode; 4 | import com.fasterxml.jackson.databind.node.ObjectNode; 5 | import uk.org.webcompere.modelassert.json.Condition; 6 | import uk.org.webcompere.modelassert.json.Result; 7 | 8 | import java.util.Arrays; 9 | import java.util.LinkedList; 10 | import java.util.List; 11 | import java.util.stream.Stream; 12 | 13 | import static java.util.stream.Collectors.toCollection; 14 | 15 | /** 16 | * An object node contains given keys/field 17 | */ 18 | public class ObjectContainsKeys implements Condition { 19 | private boolean strictOrder; 20 | private List keys; 21 | 22 | /** 23 | * Construct with the keys to find - a varargs with a minimum of 1 argument 24 | * @param key the first key 25 | * @param keys additional keys 26 | */ 27 | public ObjectContainsKeys(String key, String... keys) { 28 | this(false, key, keys); 29 | } 30 | 31 | /** 32 | * Construct with the keys to find - a varargs with a minimum of 1 argument 33 | * @param strictOrder whether the order must be maintained between the key list and the loaded JSON 34 | * @param key the first key 35 | * @param keys additional keys 36 | */ 37 | public ObjectContainsKeys(boolean strictOrder, String key, String... keys) { 38 | this.keys = Stream.concat(Stream.of(key), Arrays.stream(keys)).collect(toCollection(LinkedList::new)); 39 | this.strictOrder = strictOrder; 40 | } 41 | 42 | /** 43 | * Execute the test of the condition 44 | * 45 | * @param json the json to test 46 | * @return a {@link Result} explaining whether the condition was met and if not, why not 47 | */ 48 | @Override 49 | public Result test(JsonNode json) { 50 | if (!json.isObject() || !(json instanceof ObjectNode)) { 51 | return new Result(describe(), json.getNodeType().toString(), false); 52 | } 53 | ObjectNode objectNode = (ObjectNode) json; 54 | List fieldNames = new LinkedList<>(); 55 | objectNode.fieldNames().forEachRemaining(fieldNames::add); 56 | 57 | if (strictOrder) { 58 | return new Result(describe(), fieldNames.toString(), keys.equals(fieldNames)); 59 | } 60 | 61 | return new Result(describe(), fieldNames.toString(), 62 | keys.stream().allMatch(key -> objectNode.get(key) != null)); 63 | } 64 | 65 | /** 66 | * Describe the condition 67 | * 68 | * @return description of the condition 69 | */ 70 | @Override 71 | public String describe() { 72 | if (keys.size() == 1) { 73 | return "object contains " + (strictOrder ? " only " : "") + 74 | "key '" + keys.get(0) + "'"; 75 | } 76 | return "object contains keys " + keys + (strictOrder ? " in exact order" : ""); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/main/java/uk/org/webcompere/modelassert/json/condition/OrCondition.java: -------------------------------------------------------------------------------- 1 | package uk.org.webcompere.modelassert.json.condition; 2 | 3 | import com.fasterxml.jackson.databind.JsonNode; 4 | import uk.org.webcompere.modelassert.json.Condition; 5 | import uk.org.webcompere.modelassert.json.Result; 6 | 7 | /** 8 | * Ors two conditions together from left to right, with short-circuiting 9 | */ 10 | public class OrCondition implements Condition { 11 | private Condition first; 12 | private Condition second; 13 | 14 | public OrCondition(Condition first, Condition second) { 15 | this.first = first; 16 | this.second = second; 17 | } 18 | 19 | @Override 20 | public Result test(JsonNode json) { 21 | Result firstResult = first.test(json); 22 | if (firstResult.isPassed()) { 23 | return firstResult.withNewDescription(describe()); 24 | } 25 | return second.test(json).withNewDescription(describe()); 26 | } 27 | 28 | @Override 29 | public String describe() { 30 | return first.describe() + " or " + second.describe(); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/uk/org/webcompere/modelassert/json/condition/PredicateWrappedCondition.java: -------------------------------------------------------------------------------- 1 | package uk.org.webcompere.modelassert.json.condition; 2 | 3 | import com.fasterxml.jackson.databind.JsonNode; 4 | import uk.org.webcompere.modelassert.json.Condition; 5 | import uk.org.webcompere.modelassert.json.Result; 6 | 7 | import java.util.Optional; 8 | import java.util.function.Predicate; 9 | 10 | /** 11 | * A condition which checks a predicate for the node and then delegates to a further condition. 12 | * This can be used without a further condition, just to test the node type 13 | */ 14 | public class PredicateWrappedCondition implements Condition { 15 | private Optional delegate; 16 | private Predicate typeDetector; 17 | private String typeName; 18 | 19 | /** 20 | * Construct the condition 21 | * @param typeName the name of the type for description 22 | * @param typeDetector detects if the node's the right type 23 | */ 24 | public PredicateWrappedCondition(String typeName, Predicate typeDetector) { 25 | this.typeDetector = typeDetector; 26 | this.delegate = Optional.empty(); 27 | this.typeName = typeName; 28 | } 29 | 30 | /** 31 | * Construct the condition 32 | * @param typeName the name of the type for description 33 | * @param typeDetector detects if the node's the right type 34 | * @param delegate the condition to hand over to, if the node's of the correct type 35 | */ 36 | public PredicateWrappedCondition(String typeName, Predicate typeDetector, Condition delegate) { 37 | this.delegate = Optional.ofNullable(delegate); 38 | this.typeDetector = typeDetector; 39 | this.typeName = typeName; 40 | } 41 | 42 | /** 43 | * Execute the test of the condition 44 | * 45 | * @param json the json to test 46 | * @return a {@link Result} explaining whether the condition was met and if not, why not 47 | */ 48 | @Override 49 | public Result test(JsonNode json) { 50 | if (json == null) { 51 | return new Result(describe(), "", false); 52 | } 53 | if (!typeDetector.test(json)) { 54 | return new Result(describe(), json.getNodeType().toString(), false); 55 | } 56 | return delegate.map(del -> del.test(json)) 57 | .orElse(new Result(describe(), json.getNodeType().toString(), true)); 58 | } 59 | 60 | /** 61 | * Describe the condition 62 | * 63 | * @return description of the condition 64 | */ 65 | @Override 66 | public String describe() { 67 | return typeName + " node" + delegate.map(del -> " " + del.describe()).orElse(""); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/main/java/uk/org/webcompere/modelassert/json/condition/array/ArrayElementCondition.java: -------------------------------------------------------------------------------- 1 | package uk.org.webcompere.modelassert.json.condition.array; 2 | 3 | import com.fasterxml.jackson.databind.JsonNode; 4 | import uk.org.webcompere.modelassert.json.Result; 5 | 6 | public interface ArrayElementCondition { 7 | /** 8 | * Execute the test of the condition 9 | * @param json the json to test 10 | * @return a {@link Result} explaining whether the condition was met and if not, why not 11 | */ 12 | Result test(JsonNode json, int arrayIndex); 13 | 14 | /** 15 | * Describe the condition 16 | * @return description of the condition 17 | */ 18 | String describe(); 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/uk/org/webcompere/modelassert/json/condition/array/ArrayElementConditionAdapter.java: -------------------------------------------------------------------------------- 1 | package uk.org.webcompere.modelassert.json.condition.array; 2 | 3 | import com.fasterxml.jackson.databind.JsonNode; 4 | import uk.org.webcompere.modelassert.json.Condition; 5 | import uk.org.webcompere.modelassert.json.Result; 6 | 7 | /** 8 | * Convert a normal element condition into an array element condition 9 | */ 10 | public class ArrayElementConditionAdapter implements ArrayElementCondition { 11 | private Condition condition; 12 | 13 | public ArrayElementConditionAdapter(Condition condition) { 14 | this.condition = condition; 15 | } 16 | 17 | @Override 18 | public Result test(JsonNode json, int arrayIndex) { 19 | return condition.test(json); 20 | } 21 | 22 | @Override 23 | public String describe() { 24 | return condition.describe(); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/uk/org/webcompere/modelassert/json/condition/array/LooseComparison.java: -------------------------------------------------------------------------------- 1 | package uk.org.webcompere.modelassert.json.condition.array; 2 | 3 | import com.fasterxml.jackson.databind.node.ArrayNode; 4 | import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; 5 | import uk.org.webcompere.modelassert.json.Condition; 6 | import uk.org.webcompere.modelassert.json.Result; 7 | 8 | import java.util.*; 9 | import java.util.function.Supplier; 10 | import java.util.stream.IntStream; 11 | import java.util.stream.Stream; 12 | 13 | import static java.util.stream.Collectors.joining; 14 | import static java.util.stream.Collectors.toList; 15 | 16 | /** 17 | * Loose array comparison 18 | */ 19 | public class LooseComparison { 20 | private static class Match { 21 | private int index; 22 | private Set counterPartIndices = new HashSet<>(); 23 | 24 | public Match(int index) { 25 | this.index = index; 26 | } 27 | 28 | public int size() { 29 | return counterPartIndices.size(); 30 | } 31 | 32 | public int getIndex() { 33 | return index; 34 | } 35 | 36 | public void add(int counterPart) { 37 | counterPartIndices.add(counterPart); 38 | } 39 | 40 | public void remove(int counterPart) { 41 | counterPartIndices.remove(counterPart); 42 | } 43 | 44 | public Stream streamCounterparts() { 45 | return counterPartIndices.stream(); 46 | } 47 | 48 | public boolean contains(int counterpart) { 49 | return counterPartIndices.contains(counterpart); 50 | } 51 | } 52 | 53 | private List arrayElementConditions; 54 | private Supplier description; 55 | 56 | 57 | @SuppressFBWarnings("EI_EXPOSE_REP2") 58 | public LooseComparison(List arrayElementConditions, Supplier description) { 59 | this.arrayElementConditions = arrayElementConditions; 60 | this.description = description; 61 | } 62 | 63 | /** 64 | * Construct the comparison from the provided regular conditions 65 | * @param arrayElementConditions the conditions 66 | * @param description the description of the assertion 67 | * @return a new {@link LooseComparison} 68 | */ 69 | public static LooseComparison fromConditions(List arrayElementConditions, Supplier description) { 70 | return new LooseComparison(arrayElementConditions.stream() 71 | .map(ArrayElementConditionAdapter::new) 72 | .collect(toList()), 73 | description); 74 | } 75 | 76 | /** 77 | * Execute the loose array comparison 78 | * @param arrayNode the node to test 79 | * @return the result of comparison 80 | */ 81 | public Result looseComparison(ArrayNode arrayNode) { 82 | // each criterion may match many other elements 83 | // try them all out 84 | List matches = IntStream.range(0, arrayElementConditions.size()) 85 | .mapToObj(index -> calcMatches(index, arrayNode)) 86 | .collect(toList()); 87 | 88 | while (!matches.isEmpty()) { 89 | matches.sort(Comparator.comparing(Match::size)); 90 | if (matches.get(0).size() == 0) { 91 | return explainMismatches(matches); 92 | } 93 | 94 | removeLeastOccurringCounterpart(matches); 95 | } 96 | 97 | return new Result(describe(), "all matched", true); 98 | } 99 | 100 | private void removeLeastOccurringCounterpart(List matches) { 101 | Multiset multiset = new Multiset<>(); 102 | matches.stream().flatMap(Match::streamCounterparts) 103 | .forEach(multiset::add); 104 | 105 | // least frequently occurring item 106 | int leastFoundIndex = multiset.entries().min(Map.Entry.comparingByValue()) 107 | .map(Map.Entry::getKey) 108 | .orElseThrow(() -> new IllegalStateException("Cannot have no elements")); 109 | 110 | // now let's find the smallest match of this 111 | Match smallestMatch = matches.stream() 112 | .filter(match -> match.contains(leastFoundIndex)).min(Comparator.comparing(Match::size)) 113 | .orElseThrow(() -> new IllegalStateException("Cannot have no elements")); 114 | 115 | // we can remove this one 116 | matches.remove(smallestMatch); 117 | 118 | // then take the least found index out of the remainder 119 | matches.forEach(match -> match.remove(leastFoundIndex)); 120 | } 121 | 122 | private Result explainMismatches(List matches) { 123 | matches.sort(Comparator.comparing(Match::getIndex)); 124 | return new Result(describe(), "No matches for:\n" + matches.stream() 125 | .filter(match -> match.size() == 0) 126 | .map(match -> "Index " + match.getIndex() + ": " + 127 | arrayElementConditions.get(match.getIndex()).describe()) 128 | .collect(joining("\n")), false); 129 | } 130 | 131 | private Match calcMatches(int index, ArrayNode arrayNode) { 132 | Match match = new Match(index); 133 | for (int i = 0; i < arrayNode.size(); i++) { 134 | if (arrayElementConditions.get(index).test(arrayNode.get(i), index).isPassed()) { 135 | match.add(i); 136 | } 137 | } 138 | return match; 139 | } 140 | 141 | private Supplier describe() { 142 | return description; 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /src/main/java/uk/org/webcompere/modelassert/json/condition/array/Multiset.java: -------------------------------------------------------------------------------- 1 | package uk.org.webcompere.modelassert.json.condition.array; 2 | 3 | import java.util.HashMap; 4 | import java.util.Map; 5 | import java.util.stream.Stream; 6 | 7 | /** 8 | * Cheap version of multiset to avoid importing all of guava! 9 | * @param the type of value in the set 10 | */ 11 | public class Multiset { 12 | private Map items = new HashMap<>(); 13 | 14 | public void add(T item) { 15 | items.merge(item, 1, Integer::sum); 16 | } 17 | 18 | public Stream> entries() { 19 | return items.entrySet().stream(); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/uk/org/webcompere/modelassert/json/condition/tree/ArrayComparisonElementCondition.java: -------------------------------------------------------------------------------- 1 | package uk.org.webcompere.modelassert.json.condition.tree; 2 | 3 | import com.fasterxml.jackson.databind.JsonNode; 4 | import uk.org.webcompere.modelassert.json.Result; 5 | import uk.org.webcompere.modelassert.json.condition.array.ArrayElementCondition; 6 | 7 | import java.util.LinkedList; 8 | import java.util.List; 9 | 10 | /** 11 | * Allow the {@link uk.org.webcompere.modelassert.json.condition.array.LooseComparison} algorithm to 12 | * use elements of the expected as conditions, while still honouring the various rules that guide how matching 13 | * is performed 14 | */ 15 | public class ArrayComparisonElementCondition implements ArrayElementCondition { 16 | private JsonNode elementInExpected; 17 | private int indexOfExpected; 18 | private Location pathToHere; 19 | private TreeComparisonCondition treeComparisonCondition; 20 | 21 | /** 22 | * Compare an element in the target array 23 | * @param elementInExpected which target element is being matched 24 | * @param indexOfExpected what is its index 25 | * @param pathToHere what is the path in the tree to this point 26 | * @param treeComparisonCondition what is the parent tree comparer - used to recurse into for child elements 27 | */ 28 | public ArrayComparisonElementCondition(JsonNode elementInExpected, int indexOfExpected, 29 | Location pathToHere, TreeComparisonCondition treeComparisonCondition) { 30 | this.elementInExpected = elementInExpected; 31 | this.indexOfExpected = indexOfExpected; 32 | this.pathToHere = pathToHere; 33 | this.treeComparisonCondition = treeComparisonCondition; 34 | } 35 | 36 | @Override 37 | public Result test(JsonNode json, int arrayIndex) { 38 | List failures = new LinkedList<>(); 39 | treeComparisonCondition.compareTrees(json, elementInExpected, 40 | pathToHere.child(Integer.toString(arrayIndex)), failures); 41 | return new Result(this::describe, failures.toString(), failures.isEmpty()); 42 | } 43 | 44 | @Override 45 | public String describe() { 46 | return "Has match for " + pathToHere.child(Integer.toString(indexOfExpected)).toString() + " in expected"; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/main/java/uk/org/webcompere/modelassert/json/condition/tree/Location.java: -------------------------------------------------------------------------------- 1 | package uk.org.webcompere.modelassert.json.condition.tree; 2 | 3 | import java.util.Collections; 4 | import java.util.LinkedList; 5 | import java.util.List; 6 | import java.util.stream.Collectors; 7 | import java.util.stream.Stream; 8 | 9 | import static java.util.stream.Collectors.joining; 10 | 11 | /** 12 | * Describes the JSON pointer up to a position in the tree 13 | */ 14 | public class Location { 15 | private List path; 16 | 17 | /** 18 | * An empty location 19 | */ 20 | public Location() { 21 | this.path = new LinkedList<>(); 22 | } 23 | 24 | private Location(List pathSoFar, String child) { 25 | this.path = Stream.concat(pathSoFar.stream(), Stream.of(child)) 26 | .collect(Collectors.toCollection(LinkedList::new)); 27 | } 28 | 29 | private Location(List path) { 30 | this.path = Collections.unmodifiableList(path); 31 | } 32 | 33 | /** 34 | * The location with a child 35 | * @param child the child to add 36 | * @return a new {@link Location} with the child attached 37 | */ 38 | public Location child(String child) { 39 | return new Location(path, child); 40 | } 41 | 42 | /** 43 | * Slice the front off the location, and produce the rest of the path 44 | * @return the remainder of the location 45 | */ 46 | public Location peelOffFirst() { 47 | return new Location(path.subList(1, path.size())); 48 | } 49 | 50 | 51 | @Override 52 | public String toString() { 53 | return path.stream().collect(joining("/", "/", "")); 54 | } 55 | 56 | /** 57 | * Is there nothing left of the path - are we at the root? or have we peeled off all the first 58 | * @return true if there's no path in here 59 | */ 60 | public boolean isEmpty() { 61 | return path.isEmpty(); 62 | } 63 | 64 | /** 65 | * Get the first part of the path 66 | * @return the first part of the path or null if there isn't any 67 | */ 68 | public String first() { 69 | return !path.isEmpty() ? path.get(0) : null; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/main/java/uk/org/webcompere/modelassert/json/condition/tree/PathMatch.java: -------------------------------------------------------------------------------- 1 | package uk.org.webcompere.modelassert.json.condition.tree; 2 | 3 | import uk.org.webcompere.modelassert.json.PathWildCard; 4 | 5 | import java.util.Arrays; 6 | import java.util.List; 7 | import java.util.regex.Pattern; 8 | import java.util.stream.Stream; 9 | 10 | import static java.util.stream.Collectors.toList; 11 | 12 | /** 13 | * Describes how a path may be matched 14 | */ 15 | public class PathMatch { 16 | private static final String JSON_POINTER_DELIMITER = "/"; 17 | 18 | private List matchers; 19 | 20 | /** 21 | * Constructed with at least one value which may be {@link String}, {@link Pattern} or {@link PathWildCard} 22 | * @param pathStart first part of the path expression 23 | * @param pathRemainder remaining elements of the path expression 24 | */ 25 | public PathMatch(Object pathStart, Object... pathRemainder) { 26 | this.matchers = Stream.concat(Stream.of(pathStart), Arrays.stream(pathRemainder)) 27 | .map(PathMatcher::of) 28 | .collect(toList()); 29 | } 30 | 31 | private PathMatch(String[] fixedPath) { 32 | this.matchers = Arrays.stream(fixedPath) 33 | .map(PathMatcher::of) 34 | .collect(toList()); 35 | } 36 | 37 | /** 38 | * A path match that matches everywhere 39 | * @return matches everything 40 | */ 41 | public static PathMatch all() { 42 | return new PathMatch(PathWildCard.ANY_SUBTREE); 43 | } 44 | 45 | /** 46 | * Convert from JSON Pointer to path match 47 | * @param jsonPointer the JSON pointer express 48 | * @return a new {@link PathMatch} 49 | */ 50 | public static PathMatch ofJsonPointer(String jsonPointer) { 51 | if (!jsonPointer.startsWith(JSON_POINTER_DELIMITER)) { 52 | throw new IllegalArgumentException("Invalid JSON Pointer, must start with " + JSON_POINTER_DELIMITER); 53 | } 54 | 55 | String[] parts = jsonPointer.split(JSON_POINTER_DELIMITER); 56 | if (Arrays.stream(parts) 57 | .skip(1) 58 | .anyMatch(part -> !part.trim().equals(part) || part.isEmpty())) { 59 | throw new IllegalArgumentException("Invalid spacing or blanks in " + jsonPointer); 60 | } 61 | 62 | return new PathMatch(Arrays.stream(parts).skip(1).toArray(String[]::new)); 63 | } 64 | 65 | /** 66 | * Find out whether this path match fits the location 67 | * @param location the location to check 68 | * @return true if the path matches 69 | */ 70 | public boolean matches(Location location) { 71 | if (matchers.isEmpty()) { 72 | return location.isEmpty(); 73 | } 74 | return matchers.get(0).matches(location, matchers.subList(1, matchers.size())); 75 | } 76 | 77 | @Override 78 | public String toString() { 79 | return matchers.toString(); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/main/java/uk/org/webcompere/modelassert/json/condition/tree/PathMatcher.java: -------------------------------------------------------------------------------- 1 | package uk.org.webcompere.modelassert.json.condition.tree; 2 | 3 | import uk.org.webcompere.modelassert.json.PathWildCard; 4 | 5 | import java.util.List; 6 | import java.util.regex.Pattern; 7 | 8 | public interface PathMatcher { 9 | 10 | /** 11 | * Does this path matcher prevent a match, or do any of its successors prevent a match 12 | * @param location the location 13 | * @param remaining the remaining {@link PathMatcher}s 14 | * @return true if there's a match 15 | */ 16 | boolean matches(Location location, List remaining); 17 | 18 | /** 19 | * Factory method - convert an object into its path matcher 20 | * @param value the value to convert 21 | * @return a path matcher or {@link IllegalArgumentException} if not known 22 | */ 23 | static PathMatcher of(Object value) { 24 | if (value instanceof String) { 25 | return new StringPathMatcher((String) value); 26 | } 27 | 28 | if (value instanceof PathWildCard) { 29 | return new WildCardPathMatcher((PathWildCard) value); 30 | } 31 | 32 | if (value instanceof Pattern) { 33 | return new RegexPathMatcher((Pattern) value); 34 | } 35 | 36 | throw new IllegalArgumentException("Unexpected path part: " + value + 37 | ". Expecting String, Pattern or PathWildCard"); 38 | } 39 | 40 | /** 41 | * Work out whether the rest of the location meets the rest of the remaining matchers 42 | * @param location the location so far 43 | * @param remaining the remaining matchers 44 | * @return true if matches the remainder 45 | */ 46 | static boolean matchesTheRest(Location location, List remaining) { 47 | if (remaining.isEmpty() && location.isEmpty()) { 48 | return true; 49 | } 50 | if (!location.isEmpty() && remaining.isEmpty()) { 51 | return false; 52 | } 53 | return remaining.get(0) 54 | .matches(location, remaining.subList(1, remaining.size())); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/main/java/uk/org/webcompere/modelassert/json/condition/tree/PathRule.java: -------------------------------------------------------------------------------- 1 | package uk.org.webcompere.modelassert.json.condition.tree; 2 | 3 | import uk.org.webcompere.modelassert.json.Condition; 4 | 5 | /** 6 | * Dictates a {@link TreeRule} that applies at a given path match and any operand for that rule 7 | */ 8 | public class PathRule { 9 | private PathMatch pathMatch; 10 | private TreeRule rule; 11 | private Condition ruleCondition; 12 | 13 | /** 14 | * Constructed with a stateless tree rule for everywhere 15 | * @param rule the rule 16 | */ 17 | public PathRule(TreeRule rule) { 18 | this(PathMatch.all(), rule); 19 | } 20 | 21 | /** 22 | * Construct to apply a rule to a path 23 | * @param pathMatch the path the rule applies to 24 | * @param rule the rule to apply - no operand 25 | */ 26 | public PathRule(PathMatch pathMatch, TreeRule rule) { 27 | this.pathMatch = pathMatch; 28 | this.rule = rule; 29 | this.ruleCondition = null; 30 | } 31 | 32 | /** 33 | * Construct to apply a condition rule to a path 34 | * @param pathMatch the path the rule applies to 35 | * @param ruleCondition the condition to apply 36 | */ 37 | public PathRule(PathMatch pathMatch, Condition ruleCondition) { 38 | this.pathMatch = pathMatch; 39 | this.rule = TreeRule.CONDITION; 40 | this.ruleCondition = ruleCondition; 41 | } 42 | 43 | /** 44 | * Does this rule apply to this location 45 | * @param location the location to test 46 | * @return true when the rule applies 47 | */ 48 | public boolean matches(Location location) { 49 | return pathMatch.matches(location); 50 | } 51 | 52 | /** 53 | * Get the rule type 54 | * @return the type of rule 55 | */ 56 | public TreeRule getRule() { 57 | return rule; 58 | } 59 | 60 | /** 61 | * Get any condition associated with the rule 62 | * @return condition 63 | */ 64 | public Condition getRuleCondition() { 65 | return ruleCondition; 66 | } 67 | 68 | @Override 69 | public String toString() { 70 | return "at " + pathMatch + " " + rule + " " + (ruleCondition != null ? ruleCondition.describe() : ""); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/main/java/uk/org/webcompere/modelassert/json/condition/tree/RegexPathMatcher.java: -------------------------------------------------------------------------------- 1 | package uk.org.webcompere.modelassert.json.condition.tree; 2 | 3 | import java.util.List; 4 | import java.util.regex.Pattern; 5 | 6 | import static uk.org.webcompere.modelassert.json.condition.tree.PathMatcher.matchesTheRest; 7 | 8 | /** 9 | * Matches a Json pointer sub path on the tree using a regular expression 10 | */ 11 | public class RegexPathMatcher implements PathMatcher { 12 | public static final Pattern ANY_FIELD_PATTERN = Pattern.compile(".*"); 13 | 14 | private Pattern pattern; 15 | 16 | public RegexPathMatcher(Pattern pattern) { 17 | this.pattern = pattern; 18 | } 19 | 20 | @Override 21 | public boolean matches(Location location, List remaining) { 22 | String first = location.first(); 23 | if (first == null) { 24 | return false; 25 | } 26 | return pattern.matcher(first).matches() && matchesTheRest(location.peelOffFirst(), remaining); 27 | } 28 | 29 | @Override 30 | public String toString() { 31 | return "{" + pattern.pattern() + "}"; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/main/java/uk/org/webcompere/modelassert/json/condition/tree/StringPathMatcher.java: -------------------------------------------------------------------------------- 1 | package uk.org.webcompere.modelassert.json.condition.tree; 2 | 3 | import java.util.List; 4 | 5 | import static uk.org.webcompere.modelassert.json.condition.tree.PathMatcher.matchesTheRest; 6 | 7 | public class StringPathMatcher implements PathMatcher { 8 | private String value; 9 | 10 | public StringPathMatcher(String value) { 11 | this.value = value; 12 | } 13 | 14 | @Override 15 | public boolean matches(Location location, List remaining) { 16 | return value.equals(location.first()) && matchesTheRest(location.peelOffFirst(), remaining); 17 | } 18 | 19 | @Override 20 | public String toString() { 21 | return "\"" + value + "\""; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/uk/org/webcompere/modelassert/json/condition/tree/TreeRule.java: -------------------------------------------------------------------------------- 1 | package uk.org.webcompere.modelassert.json.condition.tree; 2 | 3 | public enum TreeRule { 4 | /** 5 | * Ignore the order of keys in an object 6 | */ 7 | IGNORE_KEY_ORDER, 8 | 9 | /** 10 | * When the key order is important 11 | */ 12 | REQUIRE_KEY_ORDER, 13 | 14 | /** 15 | * Apply a specific condition instead of the tree comparison 16 | */ 17 | CONDITION, 18 | 19 | /** 20 | * Allow the array to be in any order 21 | */ 22 | IGNORE_ARRAY_ORDER, 23 | 24 | /** 25 | * Allow the array to contain the elements, rather than match perfectly 26 | * can be combined with allowing any order 27 | */ 28 | ARRAY_CONTAINS, 29 | 30 | /** 31 | * Skip over fields that are missing in the other object - implies keys in any order 32 | */ 33 | OBJECT_CONTAINS, 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/uk/org/webcompere/modelassert/json/condition/tree/WildCardPathMatcher.java: -------------------------------------------------------------------------------- 1 | package uk.org.webcompere.modelassert.json.condition.tree; 2 | 3 | import uk.org.webcompere.modelassert.json.PathWildCard; 4 | 5 | import java.util.List; 6 | 7 | import static uk.org.webcompere.modelassert.json.condition.tree.RegexPathMatcher.ANY_FIELD_PATTERN; 8 | 9 | public class WildCardPathMatcher implements PathMatcher { 10 | private PathWildCard pathWildCard; 11 | 12 | public WildCardPathMatcher(PathWildCard pathWildCard) { 13 | this.pathWildCard = pathWildCard; 14 | } 15 | 16 | @Override 17 | public boolean matches(Location location, List remaining) { 18 | switch (pathWildCard) { 19 | case ANY: 20 | return new RegexPathMatcher(ANY_FIELD_PATTERN).matches(location, remaining); 21 | case ANY_SUBTREE: 22 | Location currentLocation = location; 23 | if (remaining.isEmpty()) { 24 | return true; 25 | } 26 | while (!currentLocation.isEmpty()) { 27 | if (PathMatcher.matchesTheRest(currentLocation, remaining)) { 28 | return true; 29 | } 30 | 31 | // otherwise, the subtree can absorb this level of the location and try again 32 | currentLocation = currentLocation.peelOffFirst(); 33 | } 34 | return false; 35 | default: 36 | return false; 37 | } 38 | } 39 | 40 | @Override 41 | public String toString() { 42 | return pathWildCard.toString(); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/main/java/uk/org/webcompere/modelassert/json/dsl/JsonAssertDslBuilders.java: -------------------------------------------------------------------------------- 1 | package uk.org.webcompere.modelassert.json.dsl; 2 | 3 | import uk.org.webcompere.modelassert.json.Condition; 4 | import uk.org.webcompere.modelassert.json.condition.JsonAt; 5 | 6 | public class JsonAssertDslBuilders { 7 | 8 | /** 9 | * Builder of an at condition, for JSON Pointer comparison 10 | * @param the type of assertion 11 | */ 12 | public static class At implements JsonNodeAssertDsl { 13 | private Satisfies satisfies; 14 | private String path; 15 | 16 | /** 17 | * Construct an at builder on a given assertion 18 | * @param satisfies the target for more conditions 19 | * @param path the JSON pointer to the path being asserted 20 | * @see com.fasterxml.jackson.databind.JsonNode#at 21 | */ 22 | public At(Satisfies satisfies, String path) { 23 | this.satisfies = satisfies; 24 | this.path = path; 25 | } 26 | 27 | @Override 28 | public A satisfies(Condition condition) { 29 | return satisfies.satisfies(new JsonAt(path, condition)); 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/uk/org/webcompere/modelassert/json/dsl/Satisfies.java: -------------------------------------------------------------------------------- 1 | package uk.org.webcompere.modelassert.json.dsl; 2 | 3 | import uk.org.webcompere.modelassert.json.Condition; 4 | import uk.org.webcompere.modelassert.json.condition.ConditionList; 5 | 6 | @FunctionalInterface 7 | public interface Satisfies { 8 | /** 9 | * Add a condition that the input JSON must satisfy 10 | * @param condition the condition 11 | * @return this for fluent calling 12 | */ 13 | A satisfies(Condition condition); 14 | 15 | /** 16 | * Add multiple conditions in a list 17 | * @param conditionList the list of conditions 18 | * @return this for fluent calling 19 | */ 20 | default A satisfies(ConditionList conditionList) { 21 | return satisfies(conditionList.toCondition()); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/uk/org/webcompere/modelassert/json/dsl/SubsetDsl.java: -------------------------------------------------------------------------------- 1 | package uk.org.webcompere.modelassert.json.dsl; 2 | 3 | import uk.org.webcompere.modelassert.json.Condition; 4 | 5 | /** 6 | * Reduce the DSL down to a subset 7 | * @param the type of assertion 8 | */ 9 | public class SubsetDsl implements Satisfies { 10 | private Satisfies satisfies; 11 | 12 | public SubsetDsl(Satisfies satisfies) { 13 | this.satisfies = satisfies; 14 | } 15 | 16 | /** 17 | * Add a condition that the input JSON must satisfy 18 | * 19 | * @param condition the condition 20 | * @return this for fluent calling 21 | */ 22 | @Override 23 | public A satisfies(Condition condition) { 24 | return satisfies.satisfies(condition); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/uk/org/webcompere/modelassert/json/dsl/nodespecific/ArrayNodeDsl.java: -------------------------------------------------------------------------------- 1 | package uk.org.webcompere.modelassert.json.dsl.nodespecific; 2 | 3 | import com.fasterxml.jackson.databind.JsonNode; 4 | import uk.org.webcompere.modelassert.json.Condition; 5 | import uk.org.webcompere.modelassert.json.condition.ArrayContains; 6 | import uk.org.webcompere.modelassert.json.condition.ConditionList; 7 | import uk.org.webcompere.modelassert.json.condition.HasSize; 8 | import uk.org.webcompere.modelassert.json.condition.PredicateWrappedCondition; 9 | import uk.org.webcompere.modelassert.json.dsl.Satisfies; 10 | 11 | /** 12 | * Assertions specific to being an array node 13 | * @param the assertion type 14 | */ 15 | public interface ArrayNodeDsl extends Satisfies, Sizeable { 16 | /** 17 | * Assert that the value is an array, meeting an additional condition 18 | * @param condition the number condition 19 | * @return the assertion for fluent assertions, with this condition added 20 | */ 21 | default A satisfiesArrayCondition(Condition condition) { 22 | return satisfies(new PredicateWrappedCondition("Array", JsonNode::isArray, condition)); 23 | } 24 | 25 | /** 26 | * The array contains given values. 27 | * @param first the first element 28 | * @param rest the remaining elements 29 | * @return the assertion for fluent assertions, with this condition added 30 | */ 31 | default A isArrayContaining(Object first, Object... rest) { 32 | return satisfiesArrayCondition(ArrayContains.containsValues(first, rest)); 33 | } 34 | 35 | /** 36 | * The array contains values matching the conditions 37 | * @param conditions the conditions to match 38 | * @return the assertion for fluent assertions, with this condition added 39 | * @see ConditionList 40 | */ 41 | default A isArrayContaining(ConditionList conditions) { 42 | return satisfiesArrayCondition(ArrayContains.containsValues(conditions)); 43 | } 44 | 45 | /** 46 | * The array contains given values, in the given order, with no extra ones 47 | * @param first the first element 48 | * @param rest the remaining elements 49 | * @return the assertion for fluent assertions, with this condition added 50 | */ 51 | default A isArrayContainingExactly(Object first, Object... rest) { 52 | return satisfiesArrayCondition(ArrayContains.containsValuesExactly(first, rest)); 53 | } 54 | 55 | /** 56 | * The array contains given values, in the given order, with no extra ones 57 | * @param conditions the conditions to match 58 | * @return the assertion for fluent assertions, with this condition added 59 | * @see ConditionList 60 | */ 61 | default A isArrayContainingExactly(ConditionList conditions) { 62 | return satisfiesArrayCondition(ArrayContains.containsValuesExactly(conditions)); 63 | } 64 | 65 | /** 66 | * The array contains given values, in any order, with no extra ones 67 | * @param first the first element 68 | * @param rest the remaining elements 69 | * @return the assertion for fluent assertions, with this condition added 70 | */ 71 | default A isArrayContainingExactlyInAnyOrder(Object first, Object... rest) { 72 | return satisfiesArrayCondition(new HasSize(1 + rest.length) 73 | .and(ArrayContains.containsValues(first, rest))); 74 | } 75 | 76 | /** 77 | * The array contains values meeting the given conditions, in any order, with no extra ones 78 | * @param conditions the conditions 79 | * @return the assertion for fluent assertions, with this condition added 80 | * @see ConditionList 81 | */ 82 | default A isArrayContainingExactlyInAnyOrder(ConditionList conditions) { 83 | return satisfiesArrayCondition(new HasSize(conditions.getConditionList().size()) 84 | .and(ArrayContains.containsValues(conditions))); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/main/java/uk/org/webcompere/modelassert/json/dsl/nodespecific/ArrayNodes.java: -------------------------------------------------------------------------------- 1 | package uk.org.webcompere.modelassert.json.dsl.nodespecific; 2 | 3 | import com.fasterxml.jackson.databind.JsonNode; 4 | import uk.org.webcompere.modelassert.json.condition.PredicateWrappedCondition; 5 | import uk.org.webcompere.modelassert.json.dsl.Satisfies; 6 | import uk.org.webcompere.modelassert.json.dsl.SubsetDsl; 7 | 8 | /** 9 | * Reduce the DSL down to just arrays 10 | * @param the type of assertion 11 | */ 12 | public class ArrayNodes extends SubsetDsl implements ArrayNodeDsl { 13 | public ArrayNodes(Satisfies satisfies) { 14 | super(isArray(satisfies)); 15 | } 16 | 17 | 18 | private static Satisfies isArray(Satisfies requirements) { 19 | requirements.satisfies(new PredicateWrappedCondition("Array", JsonNode::isArray)); 20 | return requirements; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/uk/org/webcompere/modelassert/json/dsl/nodespecific/BooleanNodeDsl.java: -------------------------------------------------------------------------------- 1 | package uk.org.webcompere.modelassert.json.dsl.nodespecific; 2 | 3 | import com.fasterxml.jackson.databind.JsonNode; 4 | import uk.org.webcompere.modelassert.json.condition.HasValue; 5 | import uk.org.webcompere.modelassert.json.condition.PredicateWrappedCondition; 6 | import uk.org.webcompere.modelassert.json.dsl.Satisfies; 7 | 8 | /** 9 | * Assertions for Boolean nodes 10 | * @param the overall type of the assertion 11 | */ 12 | public interface BooleanNodeDsl extends Satisfies { 13 | /** 14 | * Assert that the node is boolean true 15 | * @return the assertion for fluent assertions, with this condition added 16 | */ 17 | default A isTrue() { 18 | return satisfies(new PredicateWrappedCondition("Boolean", JsonNode::isBoolean, 19 | new HasValue<>(JsonNode::asBoolean, true))); 20 | } 21 | 22 | /** 23 | * Assert that the node is boolean false 24 | * @return the assertion for fluent assertions, with this condition added 25 | */ 26 | default A isFalse() { 27 | return satisfies(new PredicateWrappedCondition("Boolean", JsonNode::isBoolean, 28 | new HasValue<>(JsonNode::asBoolean, false))); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/uk/org/webcompere/modelassert/json/dsl/nodespecific/BooleanNodes.java: -------------------------------------------------------------------------------- 1 | package uk.org.webcompere.modelassert.json.dsl.nodespecific; 2 | 3 | import com.fasterxml.jackson.databind.JsonNode; 4 | import uk.org.webcompere.modelassert.json.condition.PredicateWrappedCondition; 5 | import uk.org.webcompere.modelassert.json.dsl.Satisfies; 6 | import uk.org.webcompere.modelassert.json.dsl.SubsetDsl; 7 | 8 | /** 9 | * Reduce the DSL down to just booleans 10 | * @param the type of assertion 11 | */ 12 | public class BooleanNodes extends SubsetDsl implements BooleanNodeDsl { 13 | public BooleanNodes(Satisfies satisfies) { 14 | super(isBoolean(satisfies)); 15 | } 16 | 17 | private static Satisfies isBoolean(Satisfies requirements) { 18 | requirements.satisfies(new PredicateWrappedCondition("Boolean", JsonNode::isBoolean)); 19 | return requirements; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/uk/org/webcompere/modelassert/json/dsl/nodespecific/NumberComparisonDsl.java: -------------------------------------------------------------------------------- 1 | package uk.org.webcompere.modelassert.json.dsl.nodespecific; 2 | 3 | import com.fasterxml.jackson.databind.JsonNode; 4 | import uk.org.webcompere.modelassert.json.Condition; 5 | import uk.org.webcompere.modelassert.json.condition.NumberCondition; 6 | import uk.org.webcompere.modelassert.json.condition.PredicateWrappedCondition; 7 | import uk.org.webcompere.modelassert.json.dsl.Satisfies; 8 | 9 | import static uk.org.webcompere.modelassert.json.condition.NumberCondition.Comparison.*; 10 | 11 | /** 12 | * DSL assertions specific to arbitrary numbers 13 | * @param the assertion type 14 | */ 15 | public interface NumberComparisonDsl extends Satisfies { 16 | /** 17 | * Assert that the value is a number, meeting an additional condition 18 | * @param condition the number condition 19 | * @return the assertion for fluent assertions, with this condition added 20 | */ 21 | default A satisfiesNumberCondition(Condition condition) { 22 | return satisfies(new PredicateWrappedCondition("Number", JsonNode::isNumber, condition)); 23 | } 24 | 25 | /** 26 | * Assert that the value is a number, greater than 27 | * @param number the amount 28 | * @return the assertion for fluent assertions, with this condition added 29 | */ 30 | default A isGreaterThan(Number number) { 31 | return satisfiesNumberCondition(new NumberCondition<>(number, GREATER_THAN)); 32 | } 33 | 34 | /** 35 | * Assert that the value is a number, greater than or equal to 36 | * @param number the amount 37 | * @return the assertion for fluent assertions, with this condition added 38 | */ 39 | default A isGreaterThanOrEqualTo(Number number) { 40 | return satisfiesNumberCondition(new NumberCondition<>(number, GREATER_THAN_OR_EQUAL)); 41 | } 42 | 43 | /** 44 | * Assert that the value is a number, less than the given number 45 | * @param number the number to compare with 46 | * @return the assertion for fluent assertions, with this condition added 47 | */ 48 | default A isLessThan(Number number) { 49 | return satisfiesNumberCondition(new NumberCondition<>(number, LESS_THAN)); 50 | } 51 | 52 | /** 53 | * Assert that the value is a number, less than or equal to 54 | * @param number the amount 55 | * @return the assertion for fluent assertions, with this condition added 56 | */ 57 | default A isLessThanOrEqualTo(Number number) { 58 | return satisfiesNumberCondition(new NumberCondition<>(number, LESS_THAN_OR_EQUAL)); 59 | } 60 | 61 | /** 62 | * Assert that the value lies between the limits 63 | * @param firstInclusive the first one which the value must be greater than or equal to 64 | * @param lastInclusive the last one which the value must be less than or equal to 65 | * @return the assertion for fluent assertions, with this condition added 66 | */ 67 | default A isBetween(Number firstInclusive, Number lastInclusive) { 68 | return satisfiesNumberCondition(new NumberCondition<>(firstInclusive, GREATER_THAN_OR_EQUAL) 69 | .and(new NumberCondition<>(lastInclusive, LESS_THAN_OR_EQUAL))); 70 | } 71 | 72 | /** 73 | * Assert that the value is a number, equal to 0 74 | * @return the assertion for fluent assertions, with this condition added 75 | */ 76 | default A isZero() { 77 | return satisfiesNumberCondition(new NumberCondition<>(0, EQUAL_TO)); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/main/java/uk/org/webcompere/modelassert/json/dsl/nodespecific/NumberNodeDsl.java: -------------------------------------------------------------------------------- 1 | package uk.org.webcompere.modelassert.json.dsl.nodespecific; 2 | 3 | import uk.org.webcompere.modelassert.json.condition.NumberCondition; 4 | import uk.org.webcompere.modelassert.json.dsl.Satisfies; 5 | 6 | import static uk.org.webcompere.modelassert.json.condition.Not.not; 7 | import static uk.org.webcompere.modelassert.json.condition.NumberCondition.Comparison.*; 8 | 9 | /** 10 | * DSL assertions specific to numeric nodes 11 | * @param the assertion type 12 | */ 13 | public interface NumberNodeDsl extends Satisfies, NumberComparisonDsl { 14 | 15 | /** 16 | * Assert that the value is a number, greater than 17 | * @param number the amount 18 | * @return the assertion for fluent assertions, with this condition added 19 | */ 20 | default A isGreaterThanInt(int number) { 21 | return satisfiesNumberCondition(new NumberCondition<>(Integer.class, number, GREATER_THAN)); 22 | } 23 | 24 | /** 25 | * Assert that the value is a number, greater than 26 | * @param number the amount 27 | * @return the assertion for fluent assertions, with this condition added 28 | */ 29 | default A isGreaterThanDouble(double number) { 30 | return satisfiesNumberCondition(new NumberCondition<>(Double.class, number, GREATER_THAN)); 31 | } 32 | 33 | /** 34 | * Assert that the value is a number, greater than or equal to 35 | * @param number the amount 36 | * @return the assertion for fluent assertions, with this condition added 37 | */ 38 | default A isGreaterThanOrEqualToDouble(double number) { 39 | return satisfiesNumberCondition(new NumberCondition<>(Double.class, number, GREATER_THAN_OR_EQUAL)); 40 | } 41 | 42 | /** 43 | * Assert that the value is a number, greater than or equal to 44 | * @param number the amount 45 | * @return the assertion for fluent assertions, with this condition added 46 | */ 47 | default A isGreaterThanOrEqualToInt(int number) { 48 | return satisfiesNumberCondition(new NumberCondition<>(Integer.class, number, GREATER_THAN_OR_EQUAL)); 49 | } 50 | 51 | /** 52 | * Assert that the value is a number, greater than 53 | * @param number the amount 54 | * @return the assertion for fluent assertions, with this condition added 55 | */ 56 | default A isGreaterThanLong(long number) { 57 | return satisfiesNumberCondition(new NumberCondition<>(Long.class, number, GREATER_THAN)); 58 | } 59 | 60 | /** 61 | * Assert that the value is a number, greater than or equal to 62 | * @param number the amount 63 | * @return the assertion for fluent assertions, with this condition added 64 | */ 65 | default A isGreaterThanOrEqualToLong(long number) { 66 | return satisfiesNumberCondition(new NumberCondition<>(Long.class, number, GREATER_THAN_OR_EQUAL)); 67 | } 68 | 69 | /** 70 | * Assert that the value is a number, less than or equal to 71 | * @param number the amount 72 | * @return the assertion for fluent assertions, with this condition added 73 | */ 74 | default A isLessThanOrEqualToInt(int number) { 75 | return satisfiesNumberCondition(new NumberCondition<>(Integer.class, number, LESS_THAN_OR_EQUAL)); 76 | } 77 | 78 | /** 79 | * Assert that the value is a number, less than or equal to 80 | * @param number the amount 81 | * @return the assertion for fluent assertions, with this condition added 82 | */ 83 | default A isLessThanOrEqualToLong(long number) { 84 | return satisfiesNumberCondition(new NumberCondition<>(Long.class, number, LESS_THAN_OR_EQUAL)); 85 | } 86 | 87 | /** 88 | * Assert that the value is a number, less than or equal to 89 | * @param number the amount 90 | * @return the assertion for fluent assertions, with this condition added 91 | */ 92 | default A isLessThanOrEqualToDouble(double number) { 93 | return satisfiesNumberCondition(new NumberCondition<>(Double.class, number, LESS_THAN_OR_EQUAL)); 94 | } 95 | 96 | /** 97 | * Assert that the value is a number, equal to the given number 98 | * @param number the number to compare with 99 | * @return the assertion for fluent assertions, with this condition added 100 | */ 101 | default A isNumberEqualTo(Number number) { 102 | return satisfiesNumberCondition(new NumberCondition<>(number, EQUAL_TO)); 103 | } 104 | 105 | /** 106 | * Assert that the value is a number, not equal to the given number 107 | * @param number the number to compare with 108 | * @return the assertion for fluent assertions, with this condition added 109 | */ 110 | default A isNumberNotEqualTo(Number number) { 111 | return satisfiesNumberCondition(not(new NumberCondition<>(number, EQUAL_TO))); 112 | } 113 | 114 | /** 115 | * Assert that the value is a number, less than the given number 116 | * @param number the number to compare with 117 | * @return the assertion for fluent assertions, with this condition added 118 | */ 119 | default A isLessThanInt(int number) { 120 | return satisfiesNumberCondition(new NumberCondition<>(Integer.class, number, LESS_THAN)); 121 | } 122 | 123 | /** 124 | * Assert that the value is a number, less than the given number 125 | * @param number the number to compare with 126 | * @return the assertion for fluent assertions, with this condition added 127 | */ 128 | default A isLessThanLong(long number) { 129 | return satisfiesNumberCondition(new NumberCondition<>(Long.class, number, LESS_THAN)); 130 | } 131 | 132 | /** 133 | * Assert that the value is a number, less than the given number 134 | * @param number the number to compare with 135 | * @return the assertion for fluent assertions, with this condition added 136 | */ 137 | default A isLessThanDouble(double number) { 138 | return satisfiesNumberCondition(new NumberCondition<>(Double.class, number, LESS_THAN)); 139 | } 140 | 141 | /** 142 | * Assert that the value is an number, castable to integer 143 | * @return the assertion for fluent assertions, with this condition added 144 | */ 145 | default A isInteger() { 146 | return satisfiesNumberCondition(new NumberCondition<>(Integer.class, 0, NONE)); 147 | } 148 | 149 | /** 150 | * Assert that the value is an number, castable to long 151 | * @return the assertion for fluent assertions, with this condition added 152 | */ 153 | default A isLong() { 154 | return satisfiesNumberCondition(new NumberCondition<>(Long.class, 0L, NONE)); 155 | } 156 | 157 | /** 158 | * Assert that the value is an number, castable to long 159 | * @return the assertion for fluent assertions, with this condition added 160 | */ 161 | default A isDouble() { 162 | return satisfiesNumberCondition(new NumberCondition<>(Double.class, 0d, NONE)); 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /src/main/java/uk/org/webcompere/modelassert/json/dsl/nodespecific/NumberNodes.java: -------------------------------------------------------------------------------- 1 | package uk.org.webcompere.modelassert.json.dsl.nodespecific; 2 | 3 | import com.fasterxml.jackson.databind.JsonNode; 4 | import uk.org.webcompere.modelassert.json.condition.PredicateWrappedCondition; 5 | import uk.org.webcompere.modelassert.json.dsl.Satisfies; 6 | import uk.org.webcompere.modelassert.json.dsl.SubsetDsl; 7 | 8 | /** 9 | * Reduce the DSL down to just numbers 10 | * @param the type of assertion 11 | */ 12 | public class NumberNodes extends SubsetDsl implements NumberNodeDsl { 13 | public NumberNodes(Satisfies satisfies) { 14 | super(isNumber(satisfies)); 15 | } 16 | 17 | private static Satisfies isNumber(Satisfies requirements) { 18 | requirements.satisfies(new PredicateWrappedCondition("Number", JsonNode::isNumber)); 19 | return requirements; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/uk/org/webcompere/modelassert/json/dsl/nodespecific/ObjectNodeDsl.java: -------------------------------------------------------------------------------- 1 | package uk.org.webcompere.modelassert.json.dsl.nodespecific; 2 | 3 | import com.fasterxml.jackson.databind.JsonNode; 4 | import uk.org.webcompere.modelassert.json.Condition; 5 | import uk.org.webcompere.modelassert.json.condition.HasSize; 6 | import uk.org.webcompere.modelassert.json.condition.ObjectContainsKeys; 7 | import uk.org.webcompere.modelassert.json.condition.PredicateWrappedCondition; 8 | import uk.org.webcompere.modelassert.json.dsl.Satisfies; 9 | 10 | import static uk.org.webcompere.modelassert.json.condition.Not.not; 11 | 12 | /** 13 | * Assertions specific to object nodes 14 | * @param the assertion type 15 | */ 16 | public interface ObjectNodeDsl extends Satisfies, Sizeable { 17 | /** 18 | * Assert that the value is an object, meeting an additional condition 19 | * @param condition the number condition 20 | * @return the assertion for fluent assertions, with this condition added 21 | */ 22 | default A satisfiesObjectCondition(Condition condition) { 23 | return satisfies(new PredicateWrappedCondition("Object", JsonNode::isObject, condition)); 24 | } 25 | 26 | /** 27 | * Assert that the value contains a key/field 28 | * @param key the key to search for 29 | * @return the assertion for fluent assertions, with this condition added 30 | */ 31 | default A containsKey(String key) { 32 | return satisfiesObjectCondition(new ObjectContainsKeys(key)); 33 | } 34 | 35 | /** 36 | * Assert that the value does not contain a key/field 37 | * @param key the key to search for 38 | * @return the assertion for fluent assertions, with this condition added 39 | */ 40 | default A doesNotContainKey(String key) { 41 | return satisfiesObjectCondition(not(new ObjectContainsKeys(key))); 42 | } 43 | 44 | /** 45 | * Assert that the value contains multiple keys/fields 46 | * @param key the first key to search for 47 | * @param keys the remaining keys to search for 48 | * @return the assertion for fluent assertions, with this condition added 49 | */ 50 | default A containsKeys(String key, String... keys) { 51 | return satisfiesObjectCondition(new ObjectContainsKeys(key, keys)); 52 | } 53 | 54 | /** 55 | * Assert that the value does not contain multiple keys/fields 56 | * @param key the first key to search for 57 | * @param keys the remaining keys to search for 58 | * @return the assertion for fluent assertions, with this condition added 59 | */ 60 | default A doesNotContainKeys(String key, String... keys) { 61 | return satisfiesObjectCondition(not(new ObjectContainsKeys(key, keys))); 62 | } 63 | 64 | /** 65 | * Assert that the value contains multiple keys/fields, no more, no less, order maintained. 66 | * This should be used instead of {@link ObjectNodeDsl#containsKey(String)} to mean 67 | * contains only the key. 68 | * @param key the first key to search for 69 | * @param keys the remaining keys to search for 70 | * @return the assertion for fluent assertions, with this condition added 71 | */ 72 | default A containsKeysExactly(String key, String... keys) { 73 | return satisfiesObjectCondition(new ObjectContainsKeys(true, key, keys)); 74 | } 75 | 76 | /** 77 | * Assert that the value contains multiple keys/fields, no more, no less, any order 78 | * @param key the first key to search for 79 | * @param keys the remaining keys to search for 80 | * @return the assertion for fluent assertions, with this condition added 81 | */ 82 | default A containsKeysExactlyInAnyOrder(String key, String... keys) { 83 | return satisfiesObjectCondition(new HasSize(1 + keys.length) 84 | .and(new ObjectContainsKeys(key, keys))); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/main/java/uk/org/webcompere/modelassert/json/dsl/nodespecific/ObjectNodes.java: -------------------------------------------------------------------------------- 1 | package uk.org.webcompere.modelassert.json.dsl.nodespecific; 2 | 3 | import com.fasterxml.jackson.databind.JsonNode; 4 | import uk.org.webcompere.modelassert.json.condition.PredicateWrappedCondition; 5 | import uk.org.webcompere.modelassert.json.dsl.Satisfies; 6 | import uk.org.webcompere.modelassert.json.dsl.SubsetDsl; 7 | 8 | /** 9 | * Reduce the DSL down to just object 10 | * @param the type of assertion 11 | */ 12 | public class ObjectNodes extends SubsetDsl 13 | implements ObjectNodeDsl { 14 | public ObjectNodes(Satisfies satisfies) { 15 | super(isObject(satisfies)); 16 | } 17 | 18 | private static Satisfies isObject(Satisfies requirements) { 19 | requirements.satisfies(new PredicateWrappedCondition("Object", JsonNode::isObject)); 20 | return requirements; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/uk/org/webcompere/modelassert/json/dsl/nodespecific/Sizeable.java: -------------------------------------------------------------------------------- 1 | package uk.org.webcompere.modelassert.json.dsl.nodespecific; 2 | 3 | import uk.org.webcompere.modelassert.json.condition.HasSize; 4 | import uk.org.webcompere.modelassert.json.condition.IsEmpty; 5 | import uk.org.webcompere.modelassert.json.dsl.Satisfies; 6 | import uk.org.webcompere.modelassert.json.impl.CoreJsonAssertion; 7 | 8 | import static uk.org.webcompere.modelassert.json.condition.Not.not; 9 | 10 | public interface Sizeable extends Satisfies { 11 | /** 12 | * Depending on the type of node, this will detect emptiness.
13 | * Warning! this is best used prefixed with a type assertion so know we have 14 | * a {@link Sizeable} item in the node. E.g.
assertJson(json).at("/field").array().isEmpty();
15 | * @return the {@link CoreJsonAssertion} for fluent assertions, with this condition added 16 | */ 17 | default A isEmpty() { 18 | return satisfies(new IsEmpty()); 19 | } 20 | 21 | /** 22 | * Depending on the type of node, this will detect the opposite of emptiness. At this level it 23 | * is specifically the inverse of an empty collection, object or string. 24 | * Warning! this is best used prefixed with a type assertion so know we have 25 | * a {@link Sizeable} item in the node. E.g.
assertJson(json).at("/field").array().isNotEmpty();
26 | * @return the {@link CoreJsonAssertion} for fluent assertions, with this condition added 27 | */ 28 | default A isNotEmpty() { 29 | return satisfies(not(new IsEmpty())); 30 | } 31 | 32 | /** 33 | * Depending on the type of node, this will detect size 34 | * @return the {@link CoreJsonAssertion} for fluent assertions, with this condition added 35 | */ 36 | default A hasSize(int expected) { 37 | return satisfies(new HasSize(expected)); 38 | } 39 | 40 | /** 41 | * Requiring this node to be a sizeable node, this switches to {@link NumberComparisonDsl} 42 | * to allow criteria to be specified for the size. 43 | * @return the {@link CoreJsonAssertion} for fluent assertions, with this condition added 44 | */ 45 | default NumberComparisonDsl
size() { 46 | return HasSize.sizeOf(this); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/main/java/uk/org/webcompere/modelassert/json/dsl/nodespecific/TextNodeDsl.java: -------------------------------------------------------------------------------- 1 | package uk.org.webcompere.modelassert.json.dsl.nodespecific; 2 | 3 | import com.fasterxml.jackson.databind.JsonNode; 4 | import uk.org.webcompere.modelassert.json.Condition; 5 | import uk.org.webcompere.modelassert.json.condition.HasValue; 6 | import uk.org.webcompere.modelassert.json.condition.IsEmpty; 7 | import uk.org.webcompere.modelassert.json.condition.MatchesTextCondition; 8 | import uk.org.webcompere.modelassert.json.condition.PredicateWrappedCondition; 9 | import uk.org.webcompere.modelassert.json.dsl.Satisfies; 10 | 11 | import java.util.function.Predicate; 12 | import java.util.regex.Pattern; 13 | 14 | import static uk.org.webcompere.modelassert.json.condition.Not.not; 15 | 16 | /** 17 | * Test node specific assertions 18 | * @param the final assertion 19 | */ 20 | public interface TextNodeDsl extends Satisfies, Sizeable { 21 | /** 22 | * Assert that the value is text, meeting an additional condition 23 | * @param condition the number condition 24 | * @return the assertion for fluent assertions, with this condition added 25 | */ 26 | default A satisfiesTextCondition(Condition condition) { 27 | return satisfies(new PredicateWrappedCondition("Text", JsonNode::isTextual, condition)); 28 | } 29 | 30 | /** 31 | * Assert that the node is text matching a regular expression 32 | * @param regex the expression 33 | * @return the assertion for fluent assertions, with this condition added 34 | */ 35 | default A matches(Pattern regex) { 36 | return satisfiesTextCondition(new MatchesTextCondition(regex)); 37 | } 38 | 39 | /** 40 | * Assert that the text matches a regular expression 41 | * @param regex the expression 42 | * @return the assertion for fluent assertions, with this condition added 43 | */ 44 | default A matches(String regex) { 45 | return satisfiesTextCondition(new MatchesTextCondition(Pattern.compile(regex))); 46 | } 47 | 48 | /** 49 | * Assert that the text matches a custom predicate 50 | * @param conditionName the name of the condition 51 | * @param predicate the required condition 52 | * @return the assertion for fluent assertions, with this condition added 53 | */ 54 | default A textMatches(String conditionName, Predicate predicate) { 55 | return satisfiesTextCondition(new MatchesTextCondition(conditionName, predicate)); 56 | } 57 | 58 | /** 59 | * Assert that the node is a text node with a given value 60 | * @param text the expected value 61 | * @return the assertion for fluent assertions, with this condition added 62 | */ 63 | default A isText(String text) { 64 | return satisfiesTextCondition(new HasValue<>(JsonNode::asText, text)); 65 | } 66 | 67 | /** 68 | * Assert that the node is a text node with value different to the given one 69 | * @param text the expected value 70 | * @return the assertion for fluent assertions, with this condition added 71 | */ 72 | default A isNotText(String text) { 73 | return satisfiesTextCondition(not(new HasValue<>(JsonNode::asText, text))); 74 | } 75 | 76 | /** 77 | * Assert that the node is text and empty 78 | * @return the assertion for fluent assertions, with this condition added 79 | */ 80 | default A isEmptyText() { 81 | return satisfiesTextCondition(new IsEmpty()); 82 | } 83 | 84 | /** 85 | * Assert that the node is text and is not empty 86 | * @return the assertion for fluent assertions, with this condition added 87 | */ 88 | default A isNotEmptyText() { 89 | return satisfiesTextCondition(not(new IsEmpty())); 90 | } 91 | 92 | /** 93 | * Assert that the text of this node contains a substring 94 | * @param substring the substring to find 95 | * @return the assertion for fluent assertions, with this condition added 96 | */ 97 | default A textContains(String substring) { 98 | return satisfiesTextCondition(MatchesTextCondition.textContains(substring)); 99 | } 100 | 101 | /** 102 | * Assert that the text of this node contains a substring 103 | * @param substring the substring to find 104 | * @return the assertion for fluent assertions, with this condition added 105 | */ 106 | default A textDoesNotContain(String substring) { 107 | return satisfiesTextCondition(not(MatchesTextCondition.textContains(substring))); 108 | } 109 | 110 | /** 111 | * Assert that the text of this node starts with a substring 112 | * @param substring the substring to find 113 | * @return the assertion for fluent assertions, with this condition added 114 | */ 115 | default A textStartsWith(String substring) { 116 | return satisfiesTextCondition(MatchesTextCondition.textStartsWith(substring)); 117 | } 118 | 119 | /** 120 | * Assert that the text of this node does not start with a substring 121 | * @param substring the substring to find 122 | * @return the assertion for fluent assertions, with this condition added 123 | */ 124 | default A textDoesNotStartWith(String substring) { 125 | return satisfiesTextCondition(not(MatchesTextCondition.textStartsWith(substring))); 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /src/main/java/uk/org/webcompere/modelassert/json/dsl/nodespecific/TextNodes.java: -------------------------------------------------------------------------------- 1 | package uk.org.webcompere.modelassert.json.dsl.nodespecific; 2 | 3 | import com.fasterxml.jackson.databind.JsonNode; 4 | import uk.org.webcompere.modelassert.json.condition.PredicateWrappedCondition; 5 | import uk.org.webcompere.modelassert.json.dsl.Satisfies; 6 | import uk.org.webcompere.modelassert.json.dsl.SubsetDsl; 7 | import uk.org.webcompere.modelassert.json.dsl.nodespecific.TextNodeDsl; 8 | 9 | /** 10 | * Reduce the DSL down to just string nodes 11 | * @param the type of assertion 12 | */ 13 | public class TextNodes extends SubsetDsl implements TextNodeDsl { 14 | public TextNodes(Satisfies satisfies) { 15 | super(isText(satisfies)); 16 | } 17 | 18 | private static Satisfies isText(Satisfies requirements) { 19 | requirements.satisfies(new PredicateWrappedCondition("Text", JsonNode::isTextual)); 20 | return requirements; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/uk/org/webcompere/modelassert/json/dsl/nodespecific/TreeComparisonDsl.java: -------------------------------------------------------------------------------- 1 | package uk.org.webcompere.modelassert.json.dsl.nodespecific; 2 | 3 | import uk.org.webcompere.modelassert.json.condition.tree.TreeComparisonCondition; 4 | import uk.org.webcompere.modelassert.json.dsl.Satisfies; 5 | import uk.org.webcompere.modelassert.json.dsl.nodespecific.tree.IsEqualToDsl; 6 | import uk.org.webcompere.modelassert.json.dsl.nodespecific.tree.WhereDsl; 7 | 8 | import static uk.org.webcompere.modelassert.json.condition.Not.not; 9 | 10 | /** 11 | * Assertions comparing one tree with another 12 | * @param the assertion type 13 | */ 14 | public interface TreeComparisonDsl extends Satisfies, IsEqualToDsl { 15 | 16 | default A isEqualTo(TreeComparisonCondition condition) { 17 | return satisfies(condition); 18 | } 19 | 20 | default A isNotEqualTo(TreeComparisonCondition condition) { 21 | return satisfies(not(condition)); 22 | } 23 | 24 | default WhereDsl where() { 25 | return new WhereDsl<>(this); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/uk/org/webcompere/modelassert/json/dsl/nodespecific/tree/IsEqualToDsl.java: -------------------------------------------------------------------------------- 1 | package uk.org.webcompere.modelassert.json.dsl.nodespecific.tree; 2 | 3 | import com.fasterxml.jackson.databind.JsonNode; 4 | import uk.org.webcompere.modelassert.json.condition.tree.TreeComparisonCondition; 5 | 6 | import java.io.File; 7 | import java.nio.file.Path; 8 | 9 | import static uk.org.webcompere.modelassert.json.JsonProviders.*; 10 | 11 | /** 12 | * The core DSL for isEqualTo and isNotEqualTo 13 | * @param the overall assertion type 14 | */ 15 | public interface IsEqualToDsl { 16 | 17 | /** 18 | * Terminal statement - apply the final comparison condition to the assertion. Called internally. 19 | * @param condition the condition 20 | * @return the assertion for fluent comparison 21 | */ 22 | A isEqualTo(TreeComparisonCondition condition); 23 | 24 | /** 25 | * Create isEqualTo condition 26 | * @param tree the tree to compare 27 | * @return the assertion for fluent comparison 28 | */ 29 | default A isEqualTo(JsonNode tree) { 30 | return isEqualTo(TreeComparisonCondition.isEqualTo(tree)); 31 | } 32 | 33 | /** 34 | * Create isEqualTo condition 35 | * @param json the json tree to compare 36 | * @return the assertion for fluent comparison 37 | */ 38 | default A isEqualTo(String json) { 39 | return isEqualTo(TreeComparisonCondition.isEqualTo(json, jsonStringProvider())); 40 | } 41 | 42 | /** 43 | * Create isEqualTo condition 44 | * @param object an object to convert to JSON to compare 45 | * @return the assertion for fluent comparison 46 | */ 47 | default A isEqualTo(Object object) { 48 | return isEqualTo(TreeComparisonCondition.isEqualTo(object, jsonObjectProvider())); 49 | } 50 | 51 | /** 52 | * Create isEqualTo condition 53 | * @param json the json tree to compare 54 | * @return the assertion for fluent comparison 55 | */ 56 | default A isEqualTo(File json) { 57 | return isEqualTo(TreeComparisonCondition.isEqualTo(json, jsonFileProvider())); 58 | } 59 | 60 | /** 61 | * Create isEqualTo condition 62 | * @param json the json tree to compare 63 | * @return the assertion for fluent comparison 64 | */ 65 | default A isEqualTo(Path json) { 66 | return isEqualTo(TreeComparisonCondition.isEqualTo(json, jsonPathProvider())); 67 | } 68 | 69 | /** 70 | * Terminal statement - apply the final comparison condition to the assertion. Called internally. 71 | * @param condition the condition 72 | * @return the assertion for fluent comparison 73 | */ 74 | A isNotEqualTo(TreeComparisonCondition condition); 75 | 76 | /** 77 | * Create isNotEqualTo condition 78 | * @param tree the tree to compare 79 | * @return the assertion for fluent comparison 80 | */ 81 | default A isNotEqualTo(JsonNode tree) { 82 | return isNotEqualTo(TreeComparisonCondition.isEqualTo(tree)); 83 | } 84 | 85 | /** 86 | * Create isNotEqualTo condition 87 | * @param json the json tree to compare 88 | * @return the assertion for fluent comparison 89 | */ 90 | default A isNotEqualTo(String json) { 91 | return isNotEqualTo(TreeComparisonCondition.isEqualTo(json, jsonStringProvider())); 92 | } 93 | 94 | /** 95 | * Create isNotEqualTo condition 96 | * @param json the json tree to compare 97 | * @return the assertion for fluent comparison 98 | */ 99 | default A isNotEqualTo(File json) { 100 | return isNotEqualTo(TreeComparisonCondition.isEqualTo(json, jsonFileProvider())); 101 | } 102 | 103 | /** 104 | * Create isNotEqualTo condition 105 | * @param json the json tree to compare 106 | * @return the assertion for fluent comparison 107 | */ 108 | default A isNotEqualTo(Path json) { 109 | return isNotEqualTo(TreeComparisonCondition.isEqualTo(json, jsonPathProvider())); 110 | } 111 | 112 | /** 113 | * Create isEqualToYaml condition 114 | * @param yaml the yaml tree to compare 115 | * @return the assertion for fluent comparison 116 | */ 117 | default A isEqualToYaml(String yaml) { 118 | return isEqualTo(TreeComparisonCondition.isEqualTo(yaml, yamlStringProvider())); 119 | } 120 | 121 | /** 122 | * Create isEqualToYaml condition 123 | * @param yaml the yaml tree to compare 124 | * @return the assertion for fluent comparison 125 | */ 126 | default A isEqualToYaml(File yaml) { 127 | return isEqualTo(TreeComparisonCondition.isEqualTo(yaml, yamlFileProvider())); 128 | } 129 | 130 | /** 131 | * Create isEqualTo condition 132 | * @param yaml the json tree to compare 133 | * @return the assertion for fluent comparison 134 | */ 135 | default A isEqualToYaml(Path yaml) { 136 | return isEqualTo(TreeComparisonCondition.isEqualTo(yaml, yamlPathProvider())); 137 | } 138 | 139 | /** 140 | * Create isNotEqualToYaml condition 141 | * @param yaml the yaml tree to compare 142 | * @return the assertion for fluent comparison 143 | */ 144 | default A isNotEqualToYaml(String yaml) { 145 | return isNotEqualTo(TreeComparisonCondition.isEqualTo(yaml, yamlStringProvider())); 146 | } 147 | 148 | /** 149 | * Create isNotEqualToYaml condition 150 | * @param yaml the yaml tree to compare 151 | * @return the assertion for fluent comparison 152 | */ 153 | default A isNotEqualToYaml(File yaml) { 154 | return isNotEqualTo(TreeComparisonCondition.isEqualTo(yaml, yamlFileProvider())); 155 | } 156 | 157 | /** 158 | * Create isNotEqualTo condition 159 | * @param yaml the json tree to compare 160 | * @return the assertion for fluent comparison 161 | */ 162 | default A isNotEqualToYaml(Path yaml) { 163 | return isNotEqualTo(TreeComparisonCondition.isEqualTo(yaml, yamlPathProvider())); 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /src/main/java/uk/org/webcompere/modelassert/json/dsl/nodespecific/tree/PathDsl.java: -------------------------------------------------------------------------------- 1 | package uk.org.webcompere.modelassert.json.dsl.nodespecific.tree; 2 | 3 | import uk.org.webcompere.modelassert.json.Condition; 4 | import uk.org.webcompere.modelassert.json.condition.Ignore; 5 | import uk.org.webcompere.modelassert.json.condition.tree.PathMatch; 6 | import uk.org.webcompere.modelassert.json.condition.tree.PathRule; 7 | import uk.org.webcompere.modelassert.json.condition.tree.TreeRule; 8 | import uk.org.webcompere.modelassert.json.dsl.JsonNodeAssertDsl; 9 | 10 | import static uk.org.webcompere.modelassert.json.condition.tree.TreeRule.*; 11 | 12 | /** 13 | * Path DSL context within the {@link WhereDsl} 14 | * @param the overall type of assertion 15 | */ 16 | public class PathDsl implements JsonNodeAssertDsl> { 17 | private WhereDsl whereDsl; 18 | private PathMatch pathMatch; 19 | 20 | PathDsl(WhereDsl whereDsl, Object pathStart, Object... pathRemainder) { 21 | this(whereDsl, new PathMatch(pathStart, pathRemainder)); 22 | } 23 | 24 | private PathDsl(WhereDsl whereDsl, PathMatch pathMatch) { 25 | this.whereDsl = whereDsl; 26 | this.pathMatch = pathMatch; 27 | } 28 | 29 | /** 30 | * Create a path DSL from a JSON Pointer expression. Note: this allows for matching to the root 31 | * object with a path of "/" 32 | * @param where the parent where 33 | * @param jsonPointer the pointer expression 34 | * @param the type of assertion 35 | * @return a new {@link PathDsl} for adding configuration to 36 | */ 37 | public static PathDsl fromJsonPointer(WhereDsl where, String jsonPointer) { 38 | return new PathDsl<>(where, PathMatch.ofJsonPointer(jsonPointer)); 39 | } 40 | 41 | // overridden here to bridge back to the where dsl 42 | @Override 43 | public WhereDsl satisfies(Condition condition) { 44 | return whereDsl.pathRule(new PathRule(pathMatch, condition)); 45 | } 46 | 47 | /** 48 | * Add a {@link TreeRule#IGNORE_KEY_ORDER} to the clause 49 | * @return the {@link WhereDsl} for fluent calling with the path added 50 | */ 51 | public WhereDsl keysInAnyOrder() { 52 | return whereDsl.pathRule(new PathRule(pathMatch, IGNORE_KEY_ORDER)); 53 | } 54 | 55 | /** 56 | * Add a {@link TreeRule#REQUIRE_KEY_ORDER} to the path - this can't be done in {@link WhereDsl} 57 | * as it is already the default so wouldn't mean anything 58 | * @return the {@link WhereDsl} for fluent calling with the path added 59 | */ 60 | public WhereDsl keysInOrder() { 61 | return whereDsl.pathRule(new PathRule(pathMatch, REQUIRE_KEY_ORDER)); 62 | } 63 | 64 | /** 65 | * Allow missing keys in objects 66 | * @return the {@link WhereDsl} for fluent calling with the path added 67 | */ 68 | public WhereDsl objectContains() { 69 | return whereDsl.pathRule(new PathRule(pathMatch, OBJECT_CONTAINS)); 70 | } 71 | 72 | /** 73 | * Relax the ordering requirement for an array at this position in the tree 74 | * @return this for fluent calling 75 | */ 76 | public WhereDsl arrayInAnyOrder() { 77 | return whereDsl.pathRule(new PathRule(pathMatch, TreeRule.IGNORE_ARRAY_ORDER)); 78 | } 79 | 80 | /** 81 | * Allow the array at this position in the tree to just contain the other elements 82 | * rather than match it completely 83 | * @return this for fluent calling 84 | */ 85 | public WhereDsl arrayContains() { 86 | return whereDsl.pathRule(new PathRule(pathMatch, TreeRule.ARRAY_CONTAINS)); 87 | } 88 | 89 | /** 90 | * Ignore everything at this path 91 | * @return the {@link WhereDsl} for fluent calling, with this path ignored 92 | */ 93 | public WhereDsl isIgnored() { 94 | return whereDsl.pathRule(new PathRule(pathMatch, new Ignore())); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/main/java/uk/org/webcompere/modelassert/json/dsl/nodespecific/tree/WhereDsl.java: -------------------------------------------------------------------------------- 1 | package uk.org.webcompere.modelassert.json.dsl.nodespecific.tree; 2 | 3 | import uk.org.webcompere.modelassert.json.PathWildCard; 4 | import uk.org.webcompere.modelassert.json.condition.tree.PathRule; 5 | import uk.org.webcompere.modelassert.json.condition.tree.TreeComparisonCondition; 6 | import uk.org.webcompere.modelassert.json.condition.tree.TreeRule; 7 | import uk.org.webcompere.modelassert.json.dsl.Satisfies; 8 | 9 | import java.util.LinkedList; 10 | import java.util.List; 11 | import java.util.function.UnaryOperator; 12 | import java.util.regex.Pattern; 13 | 14 | import static uk.org.webcompere.modelassert.json.condition.Not.not; 15 | 16 | /** 17 | * The DSL for the Where Context 18 | * @param the type of assertion this belongs to 19 | */ 20 | public class WhereDsl implements IsEqualToDsl { 21 | private Satisfies coreAssertion; 22 | private List rules = new LinkedList<>(); 23 | 24 | /** 25 | * Construct with the assertion to return to 26 | * @param coreAssertion the core assertion, as expressed by its {@link Satisfies} entry point 27 | */ 28 | public WhereDsl(Satisfies coreAssertion) { 29 | this.coreAssertion = coreAssertion; 30 | } 31 | 32 | /** 33 | * Relax the key ordering constraint for everywhere 34 | * @return this for fluent calling 35 | */ 36 | public WhereDsl keysInAnyOrder() { 37 | return pathRule(new PathRule(TreeRule.IGNORE_KEY_ORDER)); 38 | } 39 | 40 | /** 41 | * Allow missing keys in objects 42 | * @return this for fluent calling 43 | */ 44 | public WhereDsl objectContains() { 45 | return pathRule(new PathRule(TreeRule.OBJECT_CONTAINS)); 46 | } 47 | 48 | /** 49 | * Relax the ordering requirement for an array, everywhere 50 | * @return this for fluent calling 51 | */ 52 | public WhereDsl arrayInAnyOrder() { 53 | return pathRule(new PathRule(TreeRule.IGNORE_ARRAY_ORDER)); 54 | } 55 | 56 | /** 57 | * Allow arrays to just contain the other elements rather than match completely 58 | * @return this for fluent calling 59 | */ 60 | public WhereDsl arrayContains() { 61 | return pathRule(new PathRule(TreeRule.ARRAY_CONTAINS)); 62 | } 63 | 64 | /** 65 | * Add common configuration to the where dsl 66 | * @param configurer the configurer to use 67 | * @return the {@link WhereDsl} for further customisation 68 | */ 69 | public WhereDsl configuredBy(UnaryOperator> configurer) { 70 | return configurer.apply(this); 71 | } 72 | 73 | /** 74 | * Add a path rule to this, and return this 75 | * @param pathRule the rule 76 | * @return this for fluent calling 77 | */ 78 | WhereDsl pathRule(PathRule pathRule) { 79 | rules.add(pathRule); 80 | return this; 81 | } 82 | 83 | /** 84 | * Enter the path context - specialising a rule for the given path. The path is a list of node ids 85 | * or wildcards leading from the root element up to some part of the tree we're going to specialise. 86 | * This is useful for mixing named fields or array indices with {@link PathWildCard}s like 87 | * {@link PathWildCard#ANY} or {@link PathWildCard#ANY_SUBTREE}, but can be long winded for when there's 88 | * a fixed path, and doesn't allow for expressing a rule on root. There's also {@link #at(String)} which 89 | * will allow a root path - "/" 90 | * @param pathStart the first String, {@link PathWildCard} or {@link Pattern} 91 | * @param pathRemainder the remaining String, {@link PathWildCard} or {@link Pattern}s 92 | * @return the {@link PathDsl} to complete specialising what to do at that path instead of the defaults 93 | */ 94 | public PathDsl path(Object pathStart, Object... pathRemainder) { 95 | return new PathDsl<>(this, pathStart, pathRemainder); 96 | } 97 | 98 | /** 99 | * Provide a path using the JSON Pointer syntax - i.e. no wildcards or regular expressions used. 100 | * Note: this allows for matching to the root object with a path of "/" 101 | * @param jsonPointer the json pointer expression 102 | * @return the {@link PathDsl} to complete specialising what to do at that path instead of the defaults 103 | */ 104 | public PathDsl at(String jsonPointer) { 105 | return PathDsl.fromJsonPointer(this, jsonPointer); 106 | } 107 | 108 | @Override 109 | public A isEqualTo(TreeComparisonCondition condition) { 110 | return coreAssertion.satisfies(condition.withRules(rules)); 111 | } 112 | 113 | @Override 114 | public A isNotEqualTo(TreeComparisonCondition condition) { 115 | return coreAssertion.satisfies(not(condition.withRules(rules))); 116 | } 117 | 118 | } 119 | -------------------------------------------------------------------------------- /src/main/java/uk/org/webcompere/modelassert/json/hamcrest/HamcrestJsonAssertion.java: -------------------------------------------------------------------------------- 1 | package uk.org.webcompere.modelassert.json.hamcrest; 2 | 3 | import uk.org.webcompere.modelassert.json.JsonProvider; 4 | import uk.org.webcompere.modelassert.json.impl.CoreJsonAssertion; 5 | 6 | /** 7 | * Add strong typing to the {@link CoreJsonAssertion} to provide a hamcrest only assertion 8 | * @param the type of input json 9 | */ 10 | public class HamcrestJsonAssertion extends CoreJsonAssertion> { 11 | public HamcrestJsonAssertion(JsonProvider jsonProvider) { 12 | super(jsonProvider); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/uk/org/webcompere/modelassert/json/hamcrest/HamcrestJsonAssertionBuilder.java: -------------------------------------------------------------------------------- 1 | package uk.org.webcompere.modelassert.json.hamcrest; 2 | 3 | import uk.org.webcompere.modelassert.json.Condition; 4 | import uk.org.webcompere.modelassert.json.JsonProvider; 5 | import uk.org.webcompere.modelassert.json.dsl.JsonNodeAssertDsl; 6 | 7 | /** 8 | * This is an interim object - the result of starting to build a hamcrest JSON assertion. It is not, itself 9 | * a hamcrest matcher, but will produce one as soon as a meaningful assertion is added 10 | * @param the type of JSON that this asserts 11 | */ 12 | public class HamcrestJsonAssertionBuilder implements JsonNodeAssertDsl> { 13 | private HamcrestJsonAssertion assertion; 14 | 15 | public HamcrestJsonAssertionBuilder(JsonProvider provider) { 16 | this.assertion = new HamcrestJsonAssertion<>(provider); 17 | } 18 | 19 | @Override 20 | public HamcrestJsonAssertion satisfies(Condition condition) { 21 | return assertion.satisfies(condition); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/uk/org/webcompere/modelassert/json/impl/CoreJsonAssertion.java: -------------------------------------------------------------------------------- 1 | package uk.org.webcompere.modelassert.json.impl; 2 | 3 | import com.fasterxml.jackson.databind.JsonNode; 4 | import org.hamcrest.BaseMatcher; 5 | import org.hamcrest.Description; 6 | import org.mockito.ArgumentMatcher; 7 | import uk.org.webcompere.modelassert.json.Condition; 8 | import uk.org.webcompere.modelassert.json.JsonProvider; 9 | import uk.org.webcompere.modelassert.json.Result; 10 | import uk.org.webcompere.modelassert.json.condition.JsonIsNotNull; 11 | import uk.org.webcompere.modelassert.json.dsl.JsonNodeAssertDsl; 12 | import uk.org.webcompere.modelassert.json.dsl.Satisfies; 13 | 14 | import java.util.LinkedList; 15 | import java.util.List; 16 | 17 | import static java.util.stream.Collectors.joining; 18 | 19 | /** 20 | * Common implementation of both variants of the json assertion. Is itself a {@link BaseMatcher} as 21 | * the implementation has to work hardest to be compatible with hamcrest 22 | * @param the type of JSON source - e.g. String or File 23 | * @param the type of the assertion subclass, into which the fluent methods cast this 24 | */ 25 | public abstract class CoreJsonAssertion> extends BaseMatcher 26 | implements JsonNodeAssertDsl, Satisfies { 27 | 28 | private JsonProvider jsonProvider; 29 | private List conditions = new LinkedList<>(); 30 | 31 | protected CoreJsonAssertion(JsonProvider jsonProvider) { 32 | this.jsonProvider = jsonProvider; 33 | conditions.add(new JsonIsNotNull()); 34 | } 35 | 36 | @Override 37 | public A satisfies(Condition condition) { 38 | conditions.add(condition); 39 | return assertion(); 40 | } 41 | 42 | @SuppressWarnings("unchecked") 43 | A assertion() { 44 | return (A)this; 45 | } 46 | 47 | // hamcrest doesn't provide type safety but this object is constrained by generics 48 | @Override 49 | @SuppressWarnings({"unchecked", "java:S1905"}) 50 | public boolean matches(Object item) { 51 | JsonNode jsonNode = jsonProvider.jsonFrom((T)item); 52 | return conditions.stream() 53 | .map(condition -> condition.test(jsonNode)) 54 | .allMatch(Result::isPassed); 55 | } 56 | 57 | @Override 58 | public void describeTo(Description description) { 59 | description.appendText(conditions.stream() 60 | .map(Condition::describe) 61 | .collect(joining("\n"))); 62 | } 63 | 64 | @Override 65 | @SuppressWarnings({"unchecked", "java:S1905"}) 66 | public void describeMismatch(Object item, Description description) { 67 | JsonNode jsonNode = jsonProvider.jsonFrom((T)item); 68 | description.appendText(conditions.stream() 69 | .map(condition -> condition.test(jsonNode)) 70 | .filter(res -> !res.isPassed()) 71 | .map(res -> res.getCondition() + " was " + res.getWas()) 72 | .collect(joining("\n"))); 73 | } 74 | 75 | /** 76 | * Convert this to a mockito argument matcher in order to test JSON sent through to a method 77 | * or provide responses based on JSON 78 | * @return this as an argument matcher 79 | */ 80 | public ArgumentMatcher toArgumentMatcher() { 81 | return this::matches; 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/main/java/uk/org/webcompere/modelassert/json/impl/MemoizedSupplier.java: -------------------------------------------------------------------------------- 1 | package uk.org.webcompere.modelassert.json.impl; 2 | 3 | import java.util.function.Supplier; 4 | 5 | /** 6 | * A supplier which returns the same answer after the first time 7 | * @param the answer 8 | */ 9 | public class MemoizedSupplier implements Supplier { 10 | private boolean hasEvaluated; 11 | private T answer; 12 | private Supplier actualSupplier; 13 | 14 | public MemoizedSupplier(Supplier actualSupplier) { 15 | this.actualSupplier = actualSupplier; 16 | } 17 | 18 | public static MemoizedSupplier of(Supplier supplier) { 19 | return new MemoizedSupplier<>(supplier); 20 | } 21 | 22 | @Override 23 | public T get() { 24 | if (!hasEvaluated) { 25 | hasEvaluated = true; 26 | answer = actualSupplier.get(); 27 | } 28 | return answer; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/test/java/uk/org/webcompere/modelassert/json/JsonAssertionsTest.java: -------------------------------------------------------------------------------- 1 | package uk.org.webcompere.modelassert.json; 2 | 3 | import com.fasterxml.jackson.databind.JsonNode; 4 | import com.fasterxml.jackson.databind.ObjectMapper; 5 | import org.junit.jupiter.api.DisplayName; 6 | import org.junit.jupiter.api.Test; 7 | 8 | import java.nio.file.Path; 9 | import java.nio.file.Paths; 10 | 11 | import static org.assertj.core.api.Assertions.assertThatThrownBy; 12 | import static org.hamcrest.MatcherAssert.assertThat; 13 | import static uk.org.webcompere.modelassert.json.JsonAssertions.*; 14 | 15 | @DisplayName("The JSON assertions can be expressed either as fluent assertions or hamcrest matchers") 16 | class JsonAssertionsTest { 17 | private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); 18 | private static final Path SIMPLE_JSON = Paths.get("src", "test", "resources", "simple.json"); 19 | private static final Path SIMPLE_YAML = Paths.get("src", "test", "resources", "simple.yaml"); 20 | 21 | @Test 22 | void jsonAtInHamcrest() { 23 | assertThat("{\"name\":\"Bill\"}", json().at("/name").hasValue("Bill")); 24 | } 25 | 26 | @Test 27 | void multiJsonAtInHamcrest() { 28 | assertThat("{\"name\":\"Bill\", \"age\":42}", 29 | json().at("/name").hasValue("Bill") 30 | .at("/age").hasValue(42)); 31 | } 32 | 33 | @Test 34 | void jsonAtInHamcrestNegative() { 35 | assertThatThrownBy(() -> assertThat("{\"name\":\"Bill\"}", json().at("/name").hasValue("Not Bill"))) 36 | .isInstanceOf(Error.class) 37 | .hasMessage("\nExpected: Json is not null\nPath at /name is equal to Not Bill\n" + 38 | " but: /name is equal to Not Bill was \"Bill\""); 39 | } 40 | 41 | @Test 42 | void jsonAtMultiInHamcrestNegative() { 43 | assertThatThrownBy(() -> assertThat("{\"name\":\"Bill\", \"age\":42}", 44 | json().at("/name").hasValue("Bill") 45 | .at("/age").hasValue(12))) 46 | .isInstanceOf(Error.class) 47 | .hasMessage("\nExpected: Json is not null\nPath at /name is equal to Bill\nPath at /age is equal to 12\n" + 48 | " but: /age is equal to 12 was 42"); 49 | } 50 | 51 | @Test 52 | void jsonAtInHamcrestBadJson() { 53 | assertThatThrownBy(() -> assertThat("{\"name\"", json().at("/name").hasValue("Not Bill"))) 54 | .isInstanceOf(Error.class); 55 | } 56 | 57 | @Test 58 | void jsonAtWithAssertJson() { 59 | assertJson("{\"name\":\"Bill\"}") 60 | .at("/name") 61 | .hasValue("Bill"); 62 | } 63 | 64 | @Test 65 | void jsonAtWithAssertJsonNegative() { 66 | assertThatThrownBy(() -> assertJson("{\"name\":\"Bill\"}").at("/name").hasValue("Not Bill")) 67 | .isInstanceOf(Error.class) 68 | .hasMessage("Expected: Path at /name is equal to Not Bill\n but: /name is equal to Not Bill was \"Bill\""); 69 | } 70 | 71 | @Test 72 | void multiJsonWithAssertJson() { 73 | assertJson("{\"name\":\"Bill\", \"age\":42}") 74 | .at("/name").hasValue("Bill") 75 | .at("/age").hasValue(42); 76 | } 77 | 78 | @Test 79 | void jsonAtMultiWithAssertJsonNegative() { 80 | assertThatThrownBy(() -> assertJson("{\"name\":\"Bill\", \"age\":42}") 81 | .at("/name").hasValue("Bill") 82 | .at("/age").hasValue(12)) 83 | .isInstanceOf(Error.class) 84 | .hasMessage("Expected: Path at /age is equal to 12\n but: /age is equal to 12 was 42"); 85 | } 86 | 87 | @Test 88 | void canAssertWithJsonNode() throws Exception { 89 | JsonNode node = OBJECT_MAPPER.readTree("{\"name\":\"John\"}"); 90 | assertJson(node) 91 | .at("/name") 92 | .hasValue("John"); 93 | } 94 | 95 | @Test 96 | void canUseHamcrestAssertWithJsonNode() throws Exception { 97 | JsonNode node = OBJECT_MAPPER.readTree("{\"name\":\"John\"}"); 98 | assertThat(node, jsonNode() 99 | .at("/name") 100 | .hasValue("John")); 101 | } 102 | 103 | @Test 104 | void canAssertWithPath() { 105 | assertJson(SIMPLE_JSON) 106 | .at("/child/name") 107 | .hasValue("Ms Child"); 108 | } 109 | 110 | @Test 111 | void canUseHamcrestAssertWithPath() { 112 | assertThat(SIMPLE_JSON, jsonFilePath() 113 | .at("/child/name") 114 | .hasValue("Ms Child")); 115 | } 116 | 117 | @Test 118 | void canUseHamcrestAssertWithFile() { 119 | assertThat(SIMPLE_JSON.toFile(), jsonFile() 120 | .at("/child/name") 121 | .hasValue("Ms Child")); 122 | } 123 | 124 | @Test 125 | void compareYamlWithJson() { 126 | assertYaml(SIMPLE_YAML) 127 | .isEqualTo(SIMPLE_JSON); 128 | } 129 | 130 | @Test 131 | void compareYamlFileWithJson() { 132 | assertYaml(SIMPLE_YAML.toFile()) 133 | .isEqualTo(SIMPLE_JSON); 134 | } 135 | 136 | @Test 137 | void hamcrestCompareYamlWithJson() { 138 | assertThat(SIMPLE_YAML, yamlFilePath() 139 | .isEqualTo(SIMPLE_JSON)); 140 | } 141 | 142 | @Test 143 | void hamcrestCompareYamlFileWithJson() { 144 | assertThat(SIMPLE_YAML.toFile(), yamlFile() 145 | .isEqualTo(SIMPLE_JSON)); 146 | } 147 | 148 | @Test 149 | void compareJsonWithYaml() { 150 | assertJson(SIMPLE_JSON) 151 | .isEqualToYaml(SIMPLE_YAML); 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /src/test/java/uk/org/webcompere/modelassert/json/OverrideObjectMapper.java: -------------------------------------------------------------------------------- 1 | package uk.org.webcompere.modelassert.json; 2 | 3 | import com.fasterxml.jackson.databind.ObjectMapper; 4 | import com.fasterxml.jackson.databind.SerializationFeature; 5 | import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; 6 | import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; 7 | import org.junit.jupiter.api.AfterAll; 8 | import org.junit.jupiter.api.BeforeAll; 9 | 10 | import static com.fasterxml.jackson.dataformat.yaml.YAMLParser.Feature.PARSE_BOOLEAN_LIKE_WORDS_AS_STRINGS; 11 | import static uk.org.webcompere.modelassert.json.JsonProviders.*; 12 | 13 | // common overrides of object mapper 14 | abstract class OverrideObjectMapper { 15 | @BeforeAll 16 | static void beforeAll() { 17 | overrideObjectMapper(defaultObjectMapper() 18 | .registerModule(new JavaTimeModule()) 19 | .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)); 20 | overrideYamlObjectMapper(new ObjectMapper( 21 | new YAMLFactory() 22 | .configure(PARSE_BOOLEAN_LIKE_WORDS_AS_STRINGS, true))); 23 | } 24 | 25 | @AfterAll 26 | static void afterAll() { 27 | clearObjectMapperOverride(); 28 | clearYamlObjectMapperOverride(); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/test/java/uk/org/webcompere/modelassert/json/OverrideObjectMapperTest.java: -------------------------------------------------------------------------------- 1 | package uk.org.webcompere.modelassert.json; 2 | 3 | import com.fasterxml.jackson.databind.ObjectMapper; 4 | import com.fasterxml.jackson.databind.SerializationFeature; 5 | import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; 6 | import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; 7 | import org.junit.jupiter.api.AfterAll; 8 | import org.junit.jupiter.api.BeforeAll; 9 | import org.junit.jupiter.api.Test; 10 | import org.junit.jupiter.api.extension.ExtendWith; 11 | 12 | import java.time.LocalDateTime; 13 | 14 | import static uk.org.webcompere.modelassert.json.JsonAssertions.assertJson; 15 | import static uk.org.webcompere.modelassert.json.JsonAssertions.assertYaml; 16 | 17 | class OverrideObjectMapperTest extends OverrideObjectMapper { 18 | public static class DateHolder { 19 | public LocalDateTime date; 20 | } 21 | 22 | @Test 23 | void objectWithDateInItCanBeAsserted() { 24 | DateHolder object = new DateHolder(); 25 | object.date = LocalDateTime.of(2022, 5, 1, 23, 0); 26 | 27 | assertJson(object).isEqualTo("{\"date\":\"2022-05-01T23:00:00\"}"); 28 | } 29 | 30 | @Test 31 | void yamlWithDateInItCanBeAsserted() { 32 | assertYaml("item: yes").isEqualToYaml("item: 'yes'"); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/test/java/uk/org/webcompere/modelassert/json/PatternsTest.java: -------------------------------------------------------------------------------- 1 | package uk.org.webcompere.modelassert.json; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import static org.assertj.core.api.Assertions.assertThat; 6 | import static uk.org.webcompere.modelassert.json.Patterns.*; 7 | 8 | class PatternsTest { 9 | 10 | @Test 11 | void theGuidPatternMatchesGuid() { 12 | assertThat("611901a1-7b59-4ab9-b0d9-7b7bb02afd0b") 13 | .matches(GUID_PATTERN); 14 | } 15 | 16 | @Test 17 | void isoDateOnly() { 18 | assertThat("2021-06-09").matches(ISO_8601_DATE); 19 | } 20 | 21 | @Test 22 | void isoDateTimeOnly() { 23 | assertThat("2021-06-09T20:00:09Z").matches(ISO_8601_DATE_TIME); 24 | } 25 | 26 | @Test 27 | void isoZonedDateTimeOnly() { 28 | assertThat("2021-06-09T20:00:09+00:00").matches(ISO_8601_ZONED_DATE_TIME); 29 | } 30 | 31 | @Test 32 | void isoDateAny() { 33 | assertThat("2021-06-09").matches(ISO_8601_DATE_ANY); 34 | assertThat("2021-06-09T20:00:09Z").matches(ISO_8601_DATE_ANY); 35 | assertThat("2021-06-09T20:00:09+00:00").matches(ISO_8601_DATE_ANY); 36 | } 37 | 38 | @Test 39 | void anyIntegerPositive() { 40 | assertThat("1234").matches(ANY_INTEGER); 41 | } 42 | 43 | @Test 44 | void anyIntegerNegative() { 45 | assertThat("-1234").matches(ANY_INTEGER); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/test/java/uk/org/webcompere/modelassert/json/TestAssertions.java: -------------------------------------------------------------------------------- 1 | package uk.org.webcompere.modelassert.json; 2 | 3 | import org.opentest4j.AssertionFailedError; 4 | import uk.org.webcompere.modelassert.json.impl.CoreJsonAssertion; 5 | 6 | import java.util.function.Consumer; 7 | 8 | import static org.assertj.core.api.Assertions.assertThatThrownBy; 9 | import static org.hamcrest.MatcherAssert.assertThat; 10 | import static org.hamcrest.Matchers.not; 11 | import static uk.org.webcompere.modelassert.json.JsonAssertions.assertJson; 12 | import static uk.org.webcompere.modelassert.json.JsonAssertions.json; 13 | 14 | /** 15 | * Helpful assertions to help test the assertJson library 16 | */ 17 | public class TestAssertions { 18 | 19 | @SuppressWarnings({"unchecked", "rawtypes"}) 20 | public static void assertAllWays(String actualThatPasses, 21 | String actualThatFails, 22 | Consumer> addRules) { 23 | addRules.accept(assertJson(actualThatPasses)); 24 | 25 | assertThatThrownBy(() -> addRules.accept(assertJson(actualThatFails))) 26 | .describedAs("Negative test of fails") 27 | .isInstanceOf(AssertionFailedError.class); 28 | 29 | CoreJsonAssertion assertion = json().is("ok", jsonNode -> true); 30 | addRules.accept(assertion); 31 | assertThat(actualThatPasses, assertion); 32 | assertThat(actualThatFails, not(assertion)); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/test/java/uk/org/webcompere/modelassert/json/condition/AndConditionTest.java: -------------------------------------------------------------------------------- 1 | package uk.org.webcompere.modelassert.json.condition; 2 | 3 | import com.fasterxml.jackson.databind.JsonNode; 4 | import org.junit.jupiter.api.BeforeEach; 5 | import org.junit.jupiter.api.Test; 6 | import org.junit.jupiter.api.extension.ExtendWith; 7 | import org.mockito.Mock; 8 | import org.mockito.junit.jupiter.MockitoExtension; 9 | import uk.org.webcompere.modelassert.json.Condition; 10 | import uk.org.webcompere.modelassert.json.Result; 11 | 12 | import static org.assertj.core.api.Assertions.assertThat; 13 | import static org.mockito.ArgumentMatchers.any; 14 | import static org.mockito.BDDMockito.given; 15 | import static org.mockito.BDDMockito.then; 16 | import static org.mockito.Mockito.never; 17 | 18 | @ExtendWith(MockitoExtension.class) 19 | class AndConditionTest { 20 | 21 | @Mock 22 | private Condition condition1; 23 | 24 | @Mock 25 | private Condition condition2; 26 | 27 | @Mock 28 | private JsonNode mockNode; 29 | 30 | private AndCondition andCondition; 31 | 32 | @BeforeEach 33 | void beforeEach() { 34 | andCondition = new AndCondition(condition1, condition2); 35 | } 36 | 37 | @Test 38 | void whenFirstConditionFailsThenAndFailsAndShortCircuits() { 39 | given(condition1.test(any())) 40 | .willReturn(new Result("a", "b", false)); 41 | 42 | assertThat(andCondition.test(mockNode).isPassed()).isFalse(); 43 | then(condition2) 44 | .should(never()) 45 | .test(any()); 46 | } 47 | 48 | @Test 49 | void whenFirstConditionPassesAndSecondFailsThenAndFails() { 50 | given(condition1.test(any())) 51 | .willReturn(new Result("a", "b", true)); 52 | 53 | given(condition2.test(any())) 54 | .willReturn(new Result("a", "b", false)); 55 | 56 | assertThat(andCondition.test(mockNode).isPassed()).isFalse(); 57 | } 58 | 59 | @Test 60 | void whenBothPassThenPasses() { 61 | given(condition1.test(any())) 62 | .willReturn(new Result("a", "b", true)); 63 | 64 | given(condition2.test(any())) 65 | .willReturn(new Result("a", "b", true)); 66 | 67 | assertThat(andCondition.test(mockNode).isPassed()).isTrue(); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/test/java/uk/org/webcompere/modelassert/json/condition/ConditionListTest.java: -------------------------------------------------------------------------------- 1 | package uk.org.webcompere.modelassert.json.condition; 2 | 3 | import org.junit.jupiter.api.Test; 4 | import uk.org.webcompere.modelassert.json.Condition; 5 | 6 | import static org.assertj.core.api.Assertions.assertThat; 7 | import static org.assertj.core.api.Assertions.assertThatThrownBy; 8 | import static uk.org.webcompere.modelassert.json.condition.ConditionList.conditions; 9 | 10 | class ConditionListTest { 11 | 12 | @Test 13 | void cannotCreateAnEmptyConditionList() { 14 | assertThatThrownBy(() -> conditions().toCondition()) 15 | .isInstanceOf(IllegalArgumentException.class); 16 | } 17 | 18 | @Test 19 | void conditionListOfOneConditionIsJustThatCondition() { 20 | Condition someCondition = new HasSize(1); 21 | 22 | Condition amalgamatedCondition = conditions().satisfies(someCondition).toCondition(); 23 | assertThat(amalgamatedCondition).isSameAs(someCondition); 24 | } 25 | 26 | @Test 27 | void conditionListOfTwoConditionsIsNeither() { 28 | Condition someCondition1 = new HasSize(1); 29 | Condition someCondition2 = new HasSize(2); 30 | 31 | Condition amalgamatedCondition = conditions() 32 | .satisfies(someCondition1) 33 | .satisfies(someCondition2) 34 | .toCondition(); 35 | 36 | assertThat(amalgamatedCondition) 37 | .isNotSameAs(someCondition1) 38 | .isNotSameAs(someCondition2); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/test/java/uk/org/webcompere/modelassert/json/condition/HasSizeTest.java: -------------------------------------------------------------------------------- 1 | package uk.org.webcompere.modelassert.json.condition; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import static uk.org.webcompere.modelassert.json.TestAssertions.assertAllWays; 6 | 7 | class HasSizeTest { 8 | 9 | @Test 10 | void arrayHasSizeOne() { 11 | assertAllWays("[{}]", "[]", 12 | assertion -> assertion.hasSize(1)); 13 | } 14 | 15 | @Test 16 | void stringHasSizeOne() { 17 | assertAllWays("\"A\"", "\"AAAAA\"", 18 | assertion -> assertion.hasSize(1)); 19 | } 20 | 21 | @Test 22 | void objectHasSizeOne() { 23 | assertAllWays("{\"name\":\"A\"}", "{}", 24 | assertion -> assertion.hasSize(1)); 25 | } 26 | 27 | @Test 28 | void sizeFailsWhenNotValidType() { 29 | assertAllWays("{}", "false", 30 | assertion -> assertion.hasSize(0)); 31 | } 32 | 33 | @Test 34 | void arraySizeInBoundary() { 35 | assertAllWays("[1, 2, 3, 4]", "[]", 36 | assertion -> assertion.size().isBetween(2, 7)); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/test/java/uk/org/webcompere/modelassert/json/condition/IsEmptyTest.java: -------------------------------------------------------------------------------- 1 | package uk.org.webcompere.modelassert.json.condition; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import static uk.org.webcompere.modelassert.json.TestAssertions.assertAllWays; 6 | 7 | class IsEmptyTest { 8 | 9 | @Test 10 | void isNotEmptyOnNumberIsTrue() { 11 | assertAllWays("{\"count\":0}", "{\"count\":[]}", 12 | assertion -> assertion.at("/count").isNotEmpty()); 13 | } 14 | 15 | @Test 16 | void isNotEmptyOnMissingIsFalse() { 17 | assertAllWays("{\"count\":0}", "{}", 18 | assertion -> assertion.at("/count").isNotEmpty()); 19 | } 20 | 21 | @Test 22 | void isNotEmptyOnNullIsFalse() { 23 | assertAllWays("{\"count\":0}", "{\"count\":null}", 24 | assertion -> assertion.at("/count").isNotEmpty()); 25 | } 26 | 27 | 28 | @Test 29 | void isEmptyOnArrayIsTrue() { 30 | assertAllWays("{\"count\":[]}", "{\"count\":[\"value\"]}", 31 | assertion -> assertion.at("/count").isEmpty()); 32 | } 33 | 34 | @Test 35 | void isEmptyOnObjectIsTrue() { 36 | assertAllWays("{\"thing\":{}}", "{\"thing\":{\"foo\":true}}", 37 | assertion -> assertion.at("/thing").isEmpty()); 38 | } 39 | 40 | @Test 41 | void isEmptyOnStringIsTrue() { 42 | assertAllWays("{\"text\":\"\"}", "{\"text\":\"value\"}", 43 | assertion -> assertion.at("/text").isEmpty()); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/test/java/uk/org/webcompere/modelassert/json/condition/OrConditionTest.java: -------------------------------------------------------------------------------- 1 | package uk.org.webcompere.modelassert.json.condition; 2 | 3 | import com.fasterxml.jackson.databind.JsonNode; 4 | import org.junit.jupiter.api.BeforeEach; 5 | import org.junit.jupiter.api.Test; 6 | import org.junit.jupiter.api.extension.ExtendWith; 7 | import org.mockito.Mock; 8 | import org.mockito.junit.jupiter.MockitoExtension; 9 | import uk.org.webcompere.modelassert.json.Condition; 10 | import uk.org.webcompere.modelassert.json.Result; 11 | 12 | import static org.assertj.core.api.Assertions.assertThat; 13 | import static org.mockito.ArgumentMatchers.any; 14 | import static org.mockito.BDDMockito.given; 15 | import static org.mockito.BDDMockito.then; 16 | import static org.mockito.Mockito.never; 17 | 18 | @ExtendWith(MockitoExtension.class) 19 | class OrConditionTest { 20 | 21 | @Mock 22 | private Condition condition1; 23 | 24 | @Mock 25 | private Condition condition2; 26 | 27 | @Mock 28 | private JsonNode mockNode; 29 | 30 | private OrCondition orCondition; 31 | 32 | @BeforeEach 33 | void beforeEach() { 34 | orCondition = new OrCondition(condition1, condition2); 35 | } 36 | 37 | @Test 38 | void whenFirstConditionPassesThenOrPassesAndShortCircuits() { 39 | given(condition1.test(any())) 40 | .willReturn(new Result("a", "b", true)); 41 | 42 | assertThat(orCondition.test(mockNode).isPassed()).isTrue(); 43 | then(condition2) 44 | .should(never()) 45 | .test(any()); 46 | } 47 | 48 | @Test 49 | void whenFirstConditionFailsAndSecondFailsThenAOrFails() { 50 | given(condition1.test(any())) 51 | .willReturn(new Result("a", "b", false)); 52 | 53 | given(condition2.test(any())) 54 | .willReturn(new Result("a", "b", false)); 55 | 56 | assertThat(orCondition.test(mockNode).isPassed()).isFalse(); 57 | } 58 | 59 | @Test 60 | void whenFirstFailsAndSecondPassesThenPasses() { 61 | given(condition1.test(any())) 62 | .willReturn(new Result("a", "b", false)); 63 | 64 | given(condition2.test(any())) 65 | .willReturn(new Result("a", "b", true)); 66 | 67 | assertThat(orCondition.test(mockNode).isPassed()).isTrue(); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/test/java/uk/org/webcompere/modelassert/json/condition/tree/PathMatchTest.java: -------------------------------------------------------------------------------- 1 | package uk.org.webcompere.modelassert.json.condition.tree; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import static org.assertj.core.api.Assertions.assertThat; 6 | import static org.assertj.core.api.Assertions.assertThatThrownBy; 7 | import static uk.org.webcompere.modelassert.json.PathWildCard.ANY; 8 | import static uk.org.webcompere.modelassert.json.PathWildCard.ANY_SUBTREE; 9 | 10 | class PathMatchTest { 11 | 12 | @Test 13 | void pathMatchOfJsonPointerMatchesLocation() { 14 | assertThat(PathMatch.ofJsonPointer("/a/b/c") 15 | .matches(new Location().child("a").child("b").child("c"))).isTrue(); 16 | } 17 | 18 | @Test 19 | void pathMatchOfJsonPointerDoesNotMatchDeeperLocation() { 20 | assertThat(PathMatch.ofJsonPointer("/a/b/c") 21 | .matches(new Location().child("a").child("b").child("c").child("d"))) 22 | .isFalse(); 23 | } 24 | 25 | @Test 26 | void subtreePathMatchCanFindNestedField() { 27 | assertThat(new PathMatch(ANY_SUBTREE, "d") 28 | .matches(new Location().child("a").child("b").child("c").child("d"))) 29 | .isTrue(); 30 | } 31 | 32 | @Test 33 | void subtreePathMatchCanFindNestedFieldPair() { 34 | assertThat(new PathMatch(ANY_SUBTREE, "c", "d") 35 | .matches(new Location().child("a").child("b").child("c").child("d"))) 36 | .isTrue(); 37 | } 38 | 39 | @Test 40 | void fieldWildCardCanApplyToAnywhereInPath() { 41 | assertThat(new PathMatch(ANY, "b", "c") 42 | .matches(new Location().child("a").child("b").child("c"))) 43 | .isTrue(); 44 | 45 | assertThat(new PathMatch("a", ANY, "c") 46 | .matches(new Location().child("a").child("b").child("c"))) 47 | .isTrue(); 48 | 49 | assertThat(new PathMatch("a", "b", ANY) 50 | .matches(new Location().child("a").child("b").child("c"))) 51 | .isTrue(); 52 | } 53 | 54 | @Test 55 | void subtreePathMatchWontFindBeyondTargetpair() { 56 | assertThat(new PathMatch(ANY_SUBTREE, "c", "d") 57 | .matches(new Location().child("a").child("b").child("c").child("d").child("e"))) 58 | .isFalse(); 59 | } 60 | 61 | @Test 62 | void tailingSubTreeMatchesExactAndAllChildLocations() { 63 | PathMatch match = new PathMatch("a", "b", "c", ANY_SUBTREE); 64 | assertThat(match.matches(new Location().child("a").child("b").child("c"))) 65 | .isTrue(); 66 | 67 | assertThat(match.matches(new Location().child("a").child("b").child("c").child("d"))) 68 | .isTrue(); 69 | } 70 | 71 | @Test 72 | void prefixingAndTailingSubTreeMatchesExactAndAllChildLocations() { 73 | PathMatch match = new PathMatch(ANY_SUBTREE, "b", "c", ANY_SUBTREE); 74 | assertThat(match.matches(new Location().child("a").child("b").child("c"))) 75 | .isTrue(); 76 | 77 | assertThat(match.matches(new Location().child("a").child("b").child("c").child("d"))) 78 | .isTrue(); 79 | } 80 | 81 | @Test 82 | void pathMustStartSlash() { 83 | assertThatThrownBy(() -> PathMatch.ofJsonPointer("foo")) 84 | .isInstanceOf(IllegalArgumentException.class); 85 | } 86 | 87 | @Test 88 | void canApplyToRootOnlyWithRootPath() { 89 | PathMatch rootMatch = PathMatch.ofJsonPointer("/"); 90 | assertThat(rootMatch.matches(new Location())) 91 | .isTrue(); 92 | assertThat(rootMatch.matches(new Location().child("a"))).isFalse(); 93 | } 94 | 95 | @Test 96 | void cannotProvideWhitespaceInPath() { 97 | assertThatThrownBy(() -> PathMatch.ofJsonPointer("/a /")) 98 | .isInstanceOf(IllegalArgumentException.class); 99 | } 100 | 101 | @Test 102 | void cannotProvideEmptyInPath() { 103 | assertThatThrownBy(() -> PathMatch.ofJsonPointer("/a//b/c")) 104 | .isInstanceOf(IllegalArgumentException.class); 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/test/java/uk/org/webcompere/modelassert/json/condition/tree/PathMatcherTest.java: -------------------------------------------------------------------------------- 1 | package uk.org.webcompere.modelassert.json.condition.tree; 2 | 3 | import org.junit.jupiter.api.Test; 4 | import uk.org.webcompere.modelassert.json.PathWildCard; 5 | 6 | import java.util.Collections; 7 | import java.util.regex.Pattern; 8 | 9 | import static java.util.Collections.singletonList; 10 | import static org.assertj.core.api.Assertions.assertThat; 11 | import static org.assertj.core.api.Assertions.assertThatThrownBy; 12 | import static uk.org.webcompere.modelassert.json.PathWildCard.ANY_SUBTREE; 13 | 14 | class PathMatcherTest { 15 | 16 | @Test 17 | void pathMatcherMustNotHaveAnyObjectInPath() { 18 | assertThatThrownBy(() -> PathMatcher.of(new Object())) 19 | .isInstanceOf(IllegalArgumentException.class); 20 | } 21 | 22 | @Test 23 | void pathMatcherWithAnyFieldShouldMatchSingleLocation() { 24 | assertThat(PathMatcher.of(PathWildCard.ANY) 25 | .matches(new Location().child("something"), Collections.emptyList())) 26 | .isTrue(); 27 | } 28 | 29 | @Test 30 | void pathMatcherWithAnySubtreeShouldMatchSingleLocation() { 31 | assertThat(PathMatcher.of(ANY_SUBTREE) 32 | .matches(new Location().child("something"), Collections.emptyList())) 33 | .isTrue(); 34 | } 35 | 36 | @Test 37 | void pathMatcherWithAnySubtreeShouldMatchNestedLocation() { 38 | assertThat(PathMatcher.of(ANY_SUBTREE) 39 | .matches(new Location().child("something").child("else"), Collections.emptyList())) 40 | .isTrue(); 41 | } 42 | 43 | @Test 44 | void pathMatcherWithAnySubtreeShouldMatchRoot() { 45 | assertThat(PathMatcher.of(ANY_SUBTREE) 46 | .matches(new Location(), Collections.emptyList())) 47 | .isTrue(); 48 | } 49 | 50 | @Test 51 | void pathMatcherWithAnySubtreeThenFieldShouldMatchNestedLocation() { 52 | assertThat(PathMatcher.of(ANY_SUBTREE) 53 | .matches(new Location().child("something").child("else"), singletonList(PathMatcher.of("else")))) 54 | .isTrue(); 55 | } 56 | 57 | @Test 58 | void pathMatcherWithAnySubtreeThenFieldShouldNotMatchIncorrectNestedLocation() { 59 | assertThat(PathMatcher.of(ANY_SUBTREE) 60 | .matches(new Location().child("something").child("other"), singletonList(PathMatcher.of("else")))) 61 | .isFalse(); 62 | } 63 | 64 | @Test 65 | void pathMatcherWithSpecificFieldShouldMatchSingleLocation() { 66 | assertThat(PathMatcher.of("something") 67 | .matches(new Location().child("something"), Collections.emptyList())) 68 | .isTrue(); 69 | } 70 | 71 | @Test 72 | void pathMatcherWithRegexForFieldShouldMatchSingleLocation() { 73 | assertThat(PathMatcher.of(Pattern.compile("som.th..g")) 74 | .matches(new Location().child("something"), Collections.emptyList())) 75 | .isTrue(); 76 | } 77 | 78 | @Test 79 | void subTreeBeforeTail() { 80 | assertThat(PathMatcher.of(ANY_SUBTREE) 81 | .matches(new Location().child("c"), singletonList(PathMatcher.of("c")))) 82 | .isTrue(); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/test/java/uk/org/webcompere/modelassert/json/dsl/nodespecific/ArrayNodeDslTest.java: -------------------------------------------------------------------------------- 1 | package uk.org.webcompere.modelassert.json.dsl.nodespecific; 2 | 3 | import org.junit.jupiter.api.Test; 4 | import org.opentest4j.AssertionFailedError; 5 | 6 | import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; 7 | import static uk.org.webcompere.modelassert.json.JsonAssertions.assertJson; 8 | import static uk.org.webcompere.modelassert.json.TestAssertions.assertAllWays; 9 | import static uk.org.webcompere.modelassert.json.condition.ConditionList.conditions; 10 | 11 | class ArrayNodeDslTest { 12 | 13 | @Test 14 | void isArrayContainingStrings() { 15 | assertAllWays("[\"a\",\"b\",\"c\"]", "[]", 16 | assertion -> assertion.isArrayContaining("b", "c")); 17 | } 18 | 19 | @Test 20 | void isArrayContainingNumbers() { 21 | assertAllWays("[1, 2, 3, 4]", "[]", 22 | assertion -> assertion.isArrayContaining(2, 4)); 23 | } 24 | 25 | @Test 26 | void isArrayContainingNull() { 27 | assertAllWays("[null]", "[]", 28 | assertion -> assertion.isArrayContaining(null)); 29 | } 30 | 31 | @Test 32 | void factorInDuplicatesWhereSecondParameterIsNull() { 33 | assertAllWays("[null, null]", "[null]", 34 | assertion -> assertion.isArrayContaining(null, null)); 35 | } 36 | 37 | @Test 38 | void factorInDuplicates() { 39 | assertAllWays("[null, null, null]", "[null]", 40 | assertion -> assertion.isArrayContaining(null, null, null)); 41 | } 42 | 43 | @Test 44 | void isArrayContainingNumbersExactly() { 45 | assertAllWays("[1, 2, 3, 4]", "[1, 2, 3, 4, 5]", 46 | assertion -> assertion.isArrayContainingExactly(1, 2, 3, 4)); 47 | } 48 | 49 | @Test 50 | void isArrayContainingNumbersExactly_whereFailsBecauseOfOrder() { 51 | assertAllWays("[1, 2, 3, 4]", "[4, 3, 2, 1]", 52 | assertion -> assertion.isArrayContainingExactly(1, 2, 3, 4)); 53 | } 54 | 55 | @Test 56 | void isArrayContainingNumbersExactly_inAnyOrder() { 57 | assertAllWays("[1, 2, 3, 4]", "[4, 3, 2, 1, 0]", 58 | assertion -> assertion.isArrayContainingExactlyInAnyOrder(4, 3, 2, 1)); 59 | } 60 | 61 | @Test 62 | void isArrayContainingMixOfValuesExactly_inAnyOrder() { 63 | assertAllWays("[1, null, \"three\", true]", "[4, 3, 2, 1, 0]", 64 | assertion -> assertion.isArrayContainingExactlyInAnyOrder(1, null, "three", true)); 65 | assertAllWays("[1, null, \"three\", true]", "[4, 3, 2, 1, 0]", 66 | assertion -> assertion.isArrayContainingExactlyInAnyOrder(null,1, true, "three")); 67 | } 68 | 69 | @Test 70 | void isArrayContainingNumbersExactlyAsConditions() { 71 | assertAllWays("[1, 2, 3, 4]", "[1, 2, 3, 4, 5]", 72 | assertion -> assertion.isArrayContainingExactly(conditions() 73 | .hasValue(1) 74 | .hasValue(2) 75 | .hasValue(3) 76 | .hasValue(4))); 77 | } 78 | 79 | @Test 80 | void isArrayContainingNumbersExactlyAsConditions_whereFailsBecauseOfOrder() { 81 | assertAllWays("[1, 2, 3, 4]", "[4, 3, 2, 1]", 82 | assertion -> assertion.isArrayContainingExactly(conditions() 83 | .hasValue(1) 84 | .hasValue(2) 85 | .hasValue(3) 86 | .hasValue(4))); 87 | } 88 | 89 | @Test 90 | void isArrayContainingNumbersExactlyAsConditions_inAnyOrder() { 91 | assertAllWays("[1, 2, 3, 4]", "[4, 3, 2, 1, 0]", 92 | assertion -> assertion.isArrayContainingExactlyInAnyOrder(conditions() 93 | .hasValue(4) 94 | .hasValue(3) 95 | .hasValue(2) 96 | .hasValue(1))); 97 | } 98 | 99 | @Test 100 | void whenUsingArrayNodeThenNodeMustBeArray() { 101 | assertThatThrownBy(() -> assertJson("{foo:{}}") 102 | .at("/foo").array().isEmpty()) 103 | .isInstanceOf(AssertionFailedError.class); 104 | } 105 | 106 | @Test 107 | void whenUsingArrayNodeWithArrayThenIsEmptyWorks() { 108 | assertJson("{foo:[]}") 109 | .at("/foo").array().isEmpty(); 110 | } 111 | 112 | @Test 113 | void whenUsingArrayNodeWithArrayThenIsNotEmptyWorks() { 114 | assertJson("{foo:[\"bar\"]}") 115 | .at("/foo").array().isNotEmpty(); 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /src/test/java/uk/org/webcompere/modelassert/json/dsl/nodespecific/BooleanNodeDslTest.java: -------------------------------------------------------------------------------- 1 | package uk.org.webcompere.modelassert.json.dsl.nodespecific; 2 | 3 | import org.junit.jupiter.api.Test; 4 | import org.opentest4j.AssertionFailedError; 5 | 6 | import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; 7 | import static uk.org.webcompere.modelassert.json.JsonAssertions.assertJson; 8 | import static uk.org.webcompere.modelassert.json.TestAssertions.assertAllWays; 9 | 10 | class BooleanNodeDslTest { 11 | 12 | @Test 13 | void booleanIsTrue() { 14 | assertAllWays("{\"good\":true}", "{\"good\":false}", 15 | assertion -> assertion.at("/good").isTrue()); 16 | } 17 | 18 | @Test 19 | void booleanIsFalse() { 20 | assertAllWays("{\"good\":false}", "{\"good\":true}", 21 | assertion -> assertion.at("/good").isFalse()); 22 | } 23 | 24 | @Test 25 | void booleanIsBoolean() { 26 | assertAllWays("{\"good\":false}", "{\"good\":0}", 27 | assertion -> assertion.at("/good").isBoolean()); 28 | } 29 | 30 | @Test 31 | void booleanIsNotBoolean() { 32 | assertAllWays("{\"good\":1}", "{\"good\":true}", 33 | assertion -> assertion.at("/good").isNotBoolean()); 34 | } 35 | 36 | @Test 37 | void booleanIsTrueInBooleanContext() { 38 | assertAllWays("{\"good\":true}", "{\"good\":false}", 39 | assertion -> assertion.at("/good").booleanNode().isTrue()); 40 | } 41 | 42 | @Test 43 | void whenUsingBooleanNodeThenNodeMustBeBoolean() { 44 | assertThatThrownBy(() -> assertJson("{foo:{}}") 45 | .at("/foo").booleanNode()) 46 | .isInstanceOf(AssertionFailedError.class); 47 | } 48 | 49 | @Test 50 | void whenUsingBooleanNodeThenIsTrueWorks() { 51 | assertJson("{foo:true}") 52 | .at("/foo").booleanNode().isTrue(); 53 | } 54 | 55 | @Test 56 | void whenUsingBooleanNodeThenIsFalseWorks() { 57 | assertJson("{foo:false}") 58 | .at("/foo").booleanNode().isFalse(); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/test/java/uk/org/webcompere/modelassert/json/dsl/nodespecific/JsonNodeDslTest.java: -------------------------------------------------------------------------------- 1 | package uk.org.webcompere.modelassert.json.dsl.nodespecific; 2 | 3 | import org.junit.jupiter.api.Test; 4 | import uk.org.webcompere.modelassert.json.dsl.JsonNodeAssertDsl; 5 | 6 | import static uk.org.webcompere.modelassert.json.TestAssertions.assertAllWays; 7 | 8 | class JsonNodeDslTest { 9 | 10 | @Test 11 | void isText() { 12 | assertAllWays("\"Damian\"", "null", 13 | JsonNodeAssertDsl::isText); 14 | } 15 | 16 | @Test 17 | void isNotText() { 18 | assertAllWays("null", "\"Damian\"", 19 | JsonNodeAssertDsl::isNotText); 20 | } 21 | 22 | @Test 23 | void canDetectArray() { 24 | assertAllWays("[]", "{}", 25 | JsonNodeAssertDsl::isArray); 26 | } 27 | 28 | @Test 29 | void canDetectNonArray() { 30 | assertAllWays("{}", "[]", 31 | JsonNodeAssertDsl::isNotArray); 32 | } 33 | 34 | @Test 35 | void objectIsObject() { 36 | assertAllWays("{}", "null", 37 | JsonNodeAssertDsl::isObject); 38 | } 39 | 40 | @Test 41 | void nonObjectIsNotObject() { 42 | assertAllWays("[]", "{}", 43 | JsonNodeAssertDsl::isNotObject); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/test/java/uk/org/webcompere/modelassert/json/dsl/nodespecific/NumericNodeDslTest.java: -------------------------------------------------------------------------------- 1 | package uk.org.webcompere.modelassert.json.dsl.nodespecific; 2 | 3 | import org.junit.jupiter.api.Test; 4 | import org.opentest4j.AssertionFailedError; 5 | 6 | import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; 7 | import static uk.org.webcompere.modelassert.json.JsonAssertions.assertJson; 8 | import static uk.org.webcompere.modelassert.json.TestAssertions.assertAllWays; 9 | 10 | class NumericNodeDslTest { 11 | 12 | @Test 13 | void integerEqual() { 14 | assertAllWays("42", "0", 15 | assertion -> assertion.isNumberEqualTo(42)); 16 | } 17 | 18 | @Test 19 | void integerNotEqual() { 20 | assertAllWays("42", "43", 21 | assertion -> assertion.isNumberNotEqualTo(43)); 22 | } 23 | 24 | @Test 25 | void longEqual() { 26 | assertAllWays("21474836470", "0", 27 | assertion -> assertion.isNumberEqualTo(21474836470L)); 28 | } 29 | 30 | @Test 31 | void integerGreaterThan() { 32 | assertAllWays("42", "41", 33 | assertion -> assertion.isGreaterThanInt(41)); 34 | } 35 | 36 | @Test 37 | void integerGreaterThanOrEqualTo() { 38 | assertAllWays("42", "41", 39 | assertion -> assertion.isGreaterThanOrEqualToInt(42)); 40 | } 41 | 42 | @Test 43 | void integerLessThanOrEqualTo() { 44 | assertAllWays("42", "43", 45 | assertion -> assertion.isLessThanOrEqualToInt(42)); 46 | } 47 | 48 | @Test 49 | void longLessThanOrEqualTo() { 50 | assertAllWays("42", "43", 51 | assertion -> assertion.isLessThanOrEqualToLong(42L)); 52 | } 53 | 54 | @Test 55 | void longGreaterThan() { 56 | assertAllWays("42", "41", 57 | assertion -> assertion.isGreaterThanLong(41L)); 58 | } 59 | 60 | @Test 61 | void doubleEqualTo() { 62 | assertAllWays("1.23", "3.21", 63 | assertion -> assertion.isNumberEqualTo(1.23d)); 64 | } 65 | 66 | @Test 67 | void doubleEqualToNoD() { 68 | assertAllWays("1.23", "1.23001", 69 | assertion -> assertion.isNumberEqualTo(1.23)); 70 | } 71 | 72 | @Test 73 | void doubleGreaterThan() { 74 | assertAllWays("1.23001", "1.23", 75 | assertion -> assertion.isGreaterThanDouble(1.23)); 76 | } 77 | 78 | @Test 79 | void doubleGreaterThanOrEqualTo() { 80 | assertAllWays("1.23", "1.229999", 81 | assertion -> assertion.isGreaterThanOrEqualToDouble(1.23)); 82 | } 83 | 84 | @Test 85 | void doubleLessThanOrEqualTo() { 86 | assertAllWays("1.23", "1.2300001", 87 | assertion -> assertion.isLessThanOrEqualToDouble(1.23)); 88 | } 89 | 90 | @Test 91 | void numberLessThan() { 92 | assertAllWays("41", "42", 93 | assertion -> assertion.isLessThan(42)); 94 | } 95 | 96 | @Test 97 | void intLessThan() { 98 | assertAllWays("41", "42", 99 | assertion -> assertion.isLessThanInt(42)); 100 | } 101 | 102 | @Test 103 | void longLessThan() { 104 | assertAllWays("41", "42", 105 | assertion -> assertion.isLessThanLong(42)); 106 | } 107 | 108 | @Test 109 | void doubleLessThan() { 110 | assertAllWays("41.99999", "42", 111 | assertion -> assertion.isLessThanDouble(42)); 112 | } 113 | 114 | @Test 115 | void doubleIsZero() { 116 | assertAllWays("0.0", "0.1", 117 | NumberComparisonDsl::isZero); 118 | } 119 | 120 | @Test 121 | void zeroIsZero() { 122 | assertAllWays("0", "123", 123 | NumberComparisonDsl::isZero); 124 | } 125 | 126 | @Test 127 | void isGreaterThanOrEqualToWhenEqual() { 128 | assertAllWays("12", "10", 129 | assertion -> assertion.isGreaterThanOrEqualTo(11)); 130 | } 131 | 132 | @Test 133 | void isGreaterThanOrEqualToWhenGreater() { 134 | assertAllWays("13", "10", 135 | assertion -> assertion.isGreaterThanOrEqualTo(11)); 136 | } 137 | 138 | @Test 139 | void isLessThanOrEqualToWhenEqual() { 140 | assertAllWays("12", "13", 141 | assertion -> assertion.isLessThanOrEqualTo(12)); 142 | } 143 | 144 | @Test 145 | void isLessThanOrEqualToWhenLess() { 146 | assertAllWays("10", "12", 147 | assertion -> assertion.isLessThanOrEqualTo(11)); 148 | } 149 | 150 | @Test 151 | void isBetweenWhenBetween() { 152 | assertAllWays("5", "0", 153 | assertion -> assertion.isBetween(2, 7)); 154 | } 155 | 156 | @Test 157 | void isBetweenWhenLowerLimit() { 158 | assertAllWays("2", "0", 159 | assertion -> assertion.isBetween(2, 7)); 160 | } 161 | 162 | @Test 163 | void isBetweenWhenUpperLimit() { 164 | assertAllWays("7", "0", 165 | assertion -> assertion.isBetween(2, 7)); 166 | } 167 | 168 | @Test 169 | void isBetweenWhenUpperLimitAndIncorrectIsAfterUpper() { 170 | assertAllWays("7", "8", 171 | assertion -> assertion.isBetween(2, 7)); 172 | } 173 | 174 | @Test 175 | void isNotNumber() { 176 | assertAllWays("\"\"", "1", 177 | assertion -> assertion.isNotNumber()); 178 | } 179 | 180 | @Test 181 | void isNumber() { 182 | assertAllWays("1", "\"\"", 183 | assertion -> assertion.isNumber()); 184 | } 185 | 186 | @Test 187 | void whenUsingNumberNodeThenNodeMustBeNumber() { 188 | assertThatThrownBy(() -> assertJson("{foo:[]}") 189 | .at("/foo").number()) 190 | .isInstanceOf(AssertionFailedError.class); 191 | } 192 | 193 | @Test 194 | void whenUsingNumberNodeWithThenComparisonsWork() { 195 | assertJson("{foo:12}") 196 | .at("/foo").number().isGreaterThan(10); 197 | } 198 | } 199 | -------------------------------------------------------------------------------- /src/test/java/uk/org/webcompere/modelassert/json/dsl/nodespecific/ObjectNodeDslTest.java: -------------------------------------------------------------------------------- 1 | package uk.org.webcompere.modelassert.json.dsl.nodespecific; 2 | 3 | import org.junit.jupiter.api.Test; 4 | import org.opentest4j.AssertionFailedError; 5 | 6 | import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; 7 | import static uk.org.webcompere.modelassert.json.JsonAssertions.assertJson; 8 | import static uk.org.webcompere.modelassert.json.TestAssertions.assertAllWays; 9 | 10 | class ObjectNodeDslTest { 11 | 12 | @Test 13 | void objectIsObjectViaObjectContext() { 14 | assertAllWays("{}", "null", 15 | assertion -> assertion.object()); 16 | } 17 | 18 | @Test 19 | void objectIsEmpty() { 20 | assertAllWays("{}", "{\"array\":[]}", 21 | Sizeable::isEmpty); 22 | } 23 | 24 | @Test 25 | void objectContainsKey() { 26 | assertAllWays("{\"name\":\"Mr Name\"}", "{\"array\":[]}", 27 | assertion -> assertion.containsKey("name")); 28 | } 29 | 30 | @Test 31 | void objectDoesNotContainKey() { 32 | assertAllWays("{\"array\":[]}", "{\"name\":\"Mr Name\"}", 33 | assertion -> assertion.doesNotContainKey("name")); 34 | } 35 | 36 | @Test 37 | void objectDoesNotContainMultipleKeys() { 38 | assertAllWays("{\"array\":[]}", "{\"name\":\"Mr Name\"}", 39 | assertion -> assertion.doesNotContainKey("name") 40 | .doesNotContainKey("foo")); 41 | } 42 | 43 | @Test 44 | void objectContainsKeys() { 45 | assertAllWays("{\"name\":\"Mr Name\", \"gender\":\"male\"}", "{\"array\":[]}", 46 | assertion -> assertion.containsKeys("name", "gender")); 47 | } 48 | 49 | @Test 50 | void objectDoesNotContainKeys() { 51 | assertAllWays("{\"array\":[]}", "{\"name\":\"Mr Name\", \"gender\":\"male\"}", 52 | assertion -> assertion.doesNotContainKeys("name", "gender")); 53 | } 54 | 55 | @Test 56 | void objectContainsKeysExactly_whereExtraField() { 57 | assertAllWays("{\"name\":\"Mr Name\", \"gender\":\"male\"}", 58 | "{\"name\":\"Mr Name\", \"gender\":\"male\", \"field\": true}", 59 | assertion -> assertion.containsKeysExactly("name", "gender")); 60 | } 61 | 62 | @Test 63 | void objectContainsKeysExactly_whereMissingField() { 64 | assertAllWays("{\"name\":\"Mr Name\", \"gender\":\"male\"}", 65 | "{\"name\":\"Mr Name\"}", 66 | assertion -> assertion.containsKeysExactly("name", "gender")); 67 | } 68 | 69 | @Test 70 | void objectContainsKeysExactly_whereOrderIsIncorrect() { 71 | assertAllWays("{\"name\":\"Mr Name\", \"gender\":\"male\"}", 72 | "{\"gender\":\"male\", \"name\":\"Mr Name\"}", 73 | assertion -> assertion.containsKeysExactly("name", "gender")); 74 | } 75 | 76 | @Test 77 | void objectContainsKeysExactly_whereReOrderIsAllowed() { 78 | assertAllWays("{\"gender\":\"male\", \"name\":\"Mr Name\"}", 79 | "{\"gender\":\"male\", \"name\":\"Mr Name\", \"other\": true}", 80 | assertion -> assertion.containsKeysExactlyInAnyOrder("name", "gender")); 81 | } 82 | 83 | @Test 84 | void whenUsingObjectNodeThenNodeMustBeObject() { 85 | assertThatThrownBy(() -> assertJson("{foo:[]}") 86 | .at("/foo").object().isEmpty()) 87 | .isInstanceOf(AssertionFailedError.class); 88 | } 89 | 90 | @Test 91 | void whenUsingObjectNodeWithObjectThenIsEmptyWorks() { 92 | assertJson("{foo:{}}") 93 | .at("/foo").object().isEmpty(); 94 | } 95 | 96 | @Test 97 | void whenUsingObjectNodeWithObjectThenIsNotEmptyWorks() { 98 | assertJson("{foo:{bar:\"bar\"}}") 99 | .at("/foo").object().isNotEmpty(); 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/test/java/uk/org/webcompere/modelassert/json/dsl/nodespecific/TextNodeDslTest.java: -------------------------------------------------------------------------------- 1 | package uk.org.webcompere.modelassert.json.dsl.nodespecific; 2 | 3 | import org.junit.jupiter.api.Test; 4 | import org.opentest4j.AssertionFailedError; 5 | import uk.org.webcompere.modelassert.json.dsl.JsonNodeAssertDsl; 6 | 7 | import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; 8 | import static uk.org.webcompere.modelassert.json.JsonAssertions.assertJson; 9 | import static uk.org.webcompere.modelassert.json.Patterns.GUID_PATTERN; 10 | import static uk.org.webcompere.modelassert.json.TestAssertions.assertAllWays; 11 | 12 | class TextNodeDslTest { 13 | 14 | @Test 15 | void isTextWithValue() { 16 | assertAllWays("\"Damian\"", "\"Jason\"", 17 | assertion -> assertion.isText("Damian")); 18 | } 19 | 20 | @Test 21 | void isEmptyText() { 22 | assertAllWays("\"\"", "\"Jason\"", 23 | JsonNodeAssertDsl::isEmptyText); 24 | } 25 | 26 | @Test 27 | void isNotEmptyText() { 28 | assertAllWays("\"Jason\"", "\"\"", 29 | JsonNodeAssertDsl::isNotEmptyText); 30 | } 31 | 32 | @Test 33 | void isNotEmptyTextDoesNotApplyToNonText() { 34 | assertAllWays("\"Jason\"", "false", 35 | JsonNodeAssertDsl::isNotEmptyText); 36 | assertAllWays("\"Jason\"", "null", 37 | JsonNodeAssertDsl::isNotEmptyText); 38 | assertAllWays("\"Jason\"", "[]", 39 | JsonNodeAssertDsl::isNotEmptyText); 40 | assertAllWays("\"Jason\"", "{}", 41 | JsonNodeAssertDsl::isNotEmptyText); 42 | } 43 | 44 | @Test 45 | void jsonAtMatches() { 46 | assertAllWays("{\"guid\":\"625110f5-502f-4748-8f1d-bad2237aa0aa\"}", 47 | "{\"guid\":\"not-a-guid\"}", 48 | assertion -> assertion.at("/guid").matches(GUID_PATTERN)); 49 | } 50 | 51 | @Test 52 | void jsonAtMatchesByString() { 53 | assertAllWays("{\"guid\":\"625110f5-502f-4748-8f1d-bad2237aa0aa\"}", 54 | "{\"guid\":\"not-a-guid\"}", 55 | assertion -> assertion.at("/guid").matches(GUID_PATTERN.pattern())); 56 | } 57 | 58 | @Test 59 | void textMatches() { 60 | assertAllWays("\"625110f5-502f-4748-8f1d-bad2237aa0aa\"", 61 | "\"not-a-guid\"", 62 | assertion -> assertion.matches(GUID_PATTERN)); 63 | } 64 | 65 | @Test 66 | void textContains() { 67 | assertAllWays("\"625110f5-502f-4748-8f1d-bad2237aa0aa\"", 68 | "\"not-a-guid\"", 69 | assertion -> assertion.textContains("bad2237aa0aa")); 70 | } 71 | 72 | @Test 73 | void textDoesNotContain() { 74 | assertAllWays("\"625110f5-502f-4748-8f1d-bad2237aa0aa\"", 75 | "\"not-a-guid\"", 76 | assertion -> assertion.textDoesNotContain("guid")); 77 | } 78 | 79 | @Test 80 | void textMatchesPredicate() { 81 | assertAllWays("\"625110f5-502f-4748-8f1d-bad2237aa0aa\"", 82 | "\"not a guid\"", 83 | assertion -> assertion.textMatches("Has dashes", text -> text.contains("-"))); 84 | } 85 | 86 | @Test 87 | void whenUsingTextNodeNodeMustBeText() { 88 | assertThatThrownBy(() -> assertJson("{foo:[]}") 89 | .at("/foo").text().isEmpty()) 90 | .isInstanceOf(AssertionFailedError.class); 91 | } 92 | 93 | @Test 94 | void whenUsingTextNodeWithTextIsEmptyWorks() { 95 | assertJson("{foo:\"\"}") 96 | .at("/foo").text().isEmpty(); 97 | } 98 | 99 | @Test 100 | void whenUsingTextNodeWithTextIsNotEmptyWorks() { 101 | assertJson("{foo:\"bar\"}") 102 | .at("/foo").text().isNotEmpty(); 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/test/java/uk/org/webcompere/modelassert/json/dsl/nodespecific/TreeComparisonDslTest.java: -------------------------------------------------------------------------------- 1 | package uk.org.webcompere.modelassert.json.dsl.nodespecific; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import java.util.regex.Pattern; 6 | 7 | import static uk.org.webcompere.modelassert.json.TestAssertions.assertAllWays; 8 | import static uk.org.webcompere.modelassert.json.PathWildCard.ANY; 9 | import static uk.org.webcompere.modelassert.json.PathWildCard.ANY_SUBTREE; 10 | 11 | class TreeComparisonDslTest { 12 | 13 | @Test 14 | void canCompareStringNodes() { 15 | assertAllWays("\"string\"", "\"not the string\"", 16 | assertion -> assertion.isEqualTo("\"string\"")); 17 | } 18 | 19 | @Test 20 | void stringNodeWontMatchIfWrongTye() { 21 | assertAllWays("\"true\"", "true", 22 | assertion -> assertion.isEqualTo("\"true\"")); 23 | 24 | assertAllWays("\"true\"", "{}", 25 | assertion -> assertion.isEqualTo("\"true\"")); 26 | } 27 | 28 | @Test 29 | void twoEmptyObjectsAreTheSame() { 30 | assertAllWays("{}", "{\"name\":null}", 31 | assertion -> assertion.isEqualTo("{}")); 32 | } 33 | 34 | @Test 35 | void emptyObjectDoesNotMatchWithNull() { 36 | assertAllWays("{}", "null", 37 | assertion -> assertion.isEqualTo("{}")); 38 | } 39 | 40 | @Test 41 | void keysMustBeInCorrectOrder() { 42 | assertAllWays("{\"name\":null, \"age\":42, \"isOk\":true}", 43 | "{\"name\":null, \"isOk\":true, \"age\":42}", 44 | assertion -> assertion.isEqualTo("{\"name\":null, \"age\":42, \"isOk\":true}")); 45 | } 46 | 47 | @Test 48 | void keysMustAllBePresent() { 49 | assertAllWays("{\"name\":null, \"age\":42, \"isOk\":true}", 50 | "{\"age\":42, \"isOk\":true}", 51 | assertion -> assertion.isEqualTo("{\"name\":null, \"age\":42, \"isOk\":true}")); 52 | } 53 | 54 | @Test 55 | void arrayOfZeroSizeMatches() { 56 | assertAllWays("[]", "[1]", 57 | assertion -> assertion.isEqualTo("[]")); 58 | } 59 | 60 | @Test 61 | void arrayOfZeroSizeWontMatchIncorrectType() { 62 | assertAllWays("[]", "null", 63 | assertion -> assertion.isEqualTo("[]")); 64 | } 65 | 66 | @Test 67 | void arraysOfOneElementMatch() { 68 | assertAllWays("[1]", "[]", 69 | assertion -> assertion.isEqualTo("[1]")); 70 | } 71 | 72 | @Test 73 | void arraysOfTwoElementsMatchInOrder() { 74 | assertAllWays("[1, 2]", "[2, 1]", 75 | assertion -> assertion.isEqualTo("[1, 2]")); 76 | } 77 | 78 | @Test 79 | void keysMustBeInCorrectOrderDownTheTree() { 80 | assertAllWays("{\"a\":1, \"b\":2, \"c\":{\"d\":3, \"e\":4}}", 81 | "{\"b\":2, \"a\":1, \"c\":{\"d\":3, \"e\":4}}", 82 | assertion -> assertion.isEqualTo("{\"a\":1, \"b\":2, \"c\":{\"d\":3, \"e\":4}}")); 83 | } 84 | 85 | @Test 86 | void keysCanBeInLooseOrderDownTheTree() { 87 | assertAllWays("{\"b\":2, \"a\":1, \"c\":{\"e\":4, \"d\":3}}", 88 | "{\"b\":1, \"a\":1, \"c\":{\"d\":3, \"e\":4}}", 89 | assertion -> assertion.where().keysInAnyOrder() 90 | .isEqualTo("{\"a\":1, \"b\":2, \"c\":{\"d\":3, \"e\":4}}")); 91 | } 92 | 93 | @Test 94 | void keysCanBeInLooseOrderInSubTree() { 95 | assertAllWays("{\"a\":1, \"b\":2, \"c\":{\"d\":3, \"e\":4}}", 96 | "{\"b\":2, \"a\":1, \"c\":{\"d\":3, \"e\":4}}", 97 | assertion -> assertion.where() 98 | .path("c").keysInAnyOrder() 99 | .isEqualTo("{\"a\":1, \"b\":2, \"c\":{\"e\":4, \"d\":3}}")); 100 | } 101 | 102 | @Test 103 | void keysCanBeInLooseOrderInSubTreeByAt() { 104 | assertAllWays("{\"a\":1, \"b\":2, \"c\":{\"d\":3, \"e\":4}}", 105 | "{\"b\":2, \"a\":1, \"c\":{\"d\":3, \"e\":4}}", 106 | assertion -> assertion.where() 107 | .at("/c").keysInAnyOrder() 108 | .isEqualTo("{\"a\":1, \"b\":2, \"c\":{\"e\":4, \"d\":3}}")); 109 | } 110 | 111 | @Test 112 | void keysCanBeInLooseOrderInSubTreeByRegex() { 113 | assertAllWays("{\"a\":1, \"b\":2, \"c\":{\"d\":3, \"e\":4}}", 114 | "{\"b\":2, \"a\":1, \"c\":{\"d\":3, \"e\":4}}", 115 | assertion -> assertion.where() 116 | .path(Pattern.compile("c")).keysInAnyOrder() 117 | .isEqualTo("{\"a\":1, \"b\":2, \"c\":{\"e\":4, \"d\":3}}")); 118 | } 119 | 120 | @Test 121 | void keysCanBeInLooseOrderInSubTreeByWildCard() { 122 | assertAllWays("{\"a\":1, \"b\":2, \"c\":{\"d\":3, \"e\":4}}", 123 | "{\"b\":2, \"a\":1, \"c\":{\"d\":3, \"e\":4}}", 124 | assertion -> assertion.where() 125 | .path(ANY).keysInAnyOrder() 126 | .isEqualTo("{\"a\":1, \"b\":2, \"c\":{\"e\":4, \"d\":3}}")); 127 | } 128 | 129 | @Test 130 | void keysCanBeInLooseOrderInSubTreeBySubtreeWildCardAndField() { 131 | assertAllWays("{\"a\":1, \"b\":2, \"c\":{\"d\":3, \"e\":4}}", 132 | "{\"b\":2, \"a\":1, \"c\":{\"d\":3, \"e\":4}}", 133 | assertion -> assertion.where() 134 | .path(ANY_SUBTREE, "c").keysInAnyOrder() 135 | .isEqualTo("{\"a\":1, \"b\":2, \"c\":{\"e\":4, \"d\":3}}")); 136 | } 137 | 138 | @Test 139 | void looseComparisonOfArrays() { 140 | assertAllWays("[1, 2, 3, 4]", "[1, 2, 3]", 141 | assertion -> assertion.where() 142 | .arrayInAnyOrder() 143 | .isEqualTo("[4, 3, 2, 1]")); 144 | } 145 | 146 | @Test 147 | void looseComparisonOfArraysWithContains() { 148 | assertAllWays("[1, 2, 3, 4]", "[1, 2]", 149 | assertion -> assertion.where() 150 | .arrayInAnyOrder() 151 | .arrayContains() 152 | .isEqualTo("[3, 2, 1]")); 153 | } 154 | 155 | @Test 156 | void looseComparisonOfArraysWithContainsDoesntRequireOrder() { 157 | assertAllWays("[1, 2, 3, 4]", "[1, 2]", 158 | assertion -> assertion.where() 159 | .arrayContains() 160 | .isEqualTo("[3, 2, 1]")); 161 | } 162 | 163 | @Test 164 | void looseComparisonOfArraysWithContainsDoesntRequireOrderInSubTree() { 165 | assertAllWays("{a:[1, 2, 3, 4]}", "{a:[1, 2]}", 166 | assertion -> assertion.where() 167 | .arrayContains() 168 | .isEqualTo("{a:[3, 2, 1]}")); 169 | } 170 | 171 | @Test 172 | void looseComparisonOfArraysWithContainsDoesntRequireOrderInSubTreeWithPathRule() { 173 | assertAllWays("{a:[1, 2, 3, 4]}", "{a:[1, 2]}", 174 | assertion -> assertion.where() 175 | .path("a").arrayContains() 176 | .isEqualTo("{a:[3, 2, 1]}")); 177 | } 178 | 179 | @Test 180 | void givenObjectWithMissingKeyThenCanStillHaveRulesForWhenKeyIsPresent() { 181 | assertAllWays("{a:1}", "{}", 182 | assertion -> assertion.where() 183 | .path("a").isInteger() 184 | .isEqualTo("{a:42}")); 185 | } 186 | } 187 | -------------------------------------------------------------------------------- /src/test/java/uk/org/webcompere/modelassert/json/examples/PathAsserterTest.java: -------------------------------------------------------------------------------- 1 | package uk.org.webcompere.modelassert.json.examples; 2 | 3 | import org.junit.jupiter.api.Test; 4 | import org.opentest4j.AssertionFailedError; 5 | 6 | import static org.assertj.core.api.Assertions.assertThatThrownBy; 7 | import static uk.org.webcompere.modelassert.json.JsonAssertions.assertJson; 8 | 9 | class PathAsserterTest { 10 | private static final String EXAMPLE_JSON = 11 | "{" + 12 | "\"make\":\"VW\"," + 13 | "\"model\":\"Golf Plus\"," + 14 | "\"owner\":\"\"," + 15 | "\"color\":null" + 16 | "}"; 17 | 18 | @Test 19 | void fluentAssertionExample() { 20 | assertJson(EXAMPLE_JSON) 21 | .at("/make").isText("VW") 22 | .at("/model").isText("Golf Plus") 23 | .at("/owner").isEmpty() 24 | .at("/color").isNull() 25 | .at("/make").isNotNull() 26 | .at("/owner").isNotNull(); 27 | } 28 | 29 | @Test 30 | void isEmptyWithTextSpecific() { 31 | assertJson(EXAMPLE_JSON) 32 | .at("/owner").text().isEmpty(); 33 | } 34 | 35 | @Test 36 | void isEmptyAsTextOnNullFails() { 37 | assertThatThrownBy(() -> assertJson(EXAMPLE_JSON) 38 | .at("/color").text().isEmpty()) 39 | .isInstanceOf(AssertionFailedError.class); 40 | } 41 | 42 | @Test 43 | void isNotEmptyWithTextSpecific() { 44 | assertJson(EXAMPLE_JSON) 45 | .at("/model").text().isNotEmpty(); 46 | } 47 | 48 | @Test 49 | void foundPropertyCanBeAsserted() { 50 | assertJson(EXAMPLE_JSON) 51 | .at("/make").isText("VW"); 52 | } 53 | 54 | @Test 55 | void foundPropertyWithIncorrectAssertionIsError() { 56 | assertJson(EXAMPLE_JSON) 57 | .at("/make").isNotText("Ford"); 58 | } 59 | 60 | @Test 61 | void isEmptyOnEmptyPasses() { 62 | assertJson(EXAMPLE_JSON) 63 | .at("/owner").isEmpty(); 64 | } 65 | 66 | @Test 67 | void isEmptyOnNonEmptyFails() { 68 | assertJson(EXAMPLE_JSON) 69 | .at("/make").isNotEmpty(); 70 | } 71 | 72 | @Test 73 | void startsWithWhenCorrectPasses() { 74 | assertJson(EXAMPLE_JSON) 75 | .at("/model").textStartsWith("Golf"); 76 | } 77 | 78 | @Test 79 | void doesNotStartWith() { 80 | assertJson(EXAMPLE_JSON) 81 | .at("/model").textDoesNotStartWith("Beetle"); 82 | } 83 | 84 | @Test 85 | void containsWhenCorrectPasses() { 86 | assertJson(EXAMPLE_JSON) 87 | .at("/model").textContains("Plus"); 88 | } 89 | 90 | @Test 91 | void containsWhenIncorrectFails() { 92 | assertJson(EXAMPLE_JSON) 93 | .at("/model").textDoesNotContain("Guice"); 94 | } 95 | 96 | @Test 97 | void isNotNullWhenCorrectPasses() { 98 | assertJson(EXAMPLE_JSON) 99 | .at("/model").isNotNull(); 100 | } 101 | 102 | @Test 103 | void isNullWhenCorrectPasses() { 104 | assertJson(EXAMPLE_JSON) 105 | .at("/color").isNull(); 106 | } 107 | 108 | @Test 109 | void failedAssertionContainsHelpfulMessageWithAllInputsIn() { 110 | assertThatThrownBy(() -> assertJson(EXAMPLE_JSON) 111 | .at("/model").textContains("Guice")) 112 | .hasMessageContaining("model") 113 | .hasMessageContaining("Guice"); 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /src/test/java/uk/org/webcompere/modelassert/json/impl/CoreJsonAssertionTest.java: -------------------------------------------------------------------------------- 1 | package uk.org.webcompere.modelassert.json.impl; 2 | 3 | import org.junit.jupiter.api.DisplayName; 4 | import org.junit.jupiter.api.Test; 5 | import uk.org.webcompere.modelassert.json.dsl.JsonNodeAssertDsl; 6 | import uk.org.webcompere.modelassert.json.dsl.nodespecific.ArrayNodeDsl; 7 | import uk.org.webcompere.modelassert.json.dsl.nodespecific.ObjectNodeDsl; 8 | 9 | import static uk.org.webcompere.modelassert.json.JsonAssertions.jsonNode; 10 | import static uk.org.webcompere.modelassert.json.Patterns.GUID_PATTERN; 11 | import static uk.org.webcompere.modelassert.json.TestAssertions.assertAllWays; 12 | 13 | @DisplayName("Common assertions for the built in behaviour of all variants of the CoreJson assertion") 14 | class CoreJsonAssertionTest { 15 | 16 | @Test 17 | void jsonAt() { 18 | assertAllWays("{\"name\":\"Jason\"}", "{\"name\":\"Damian\"}", 19 | assertion -> assertion.at("/name").hasValue("Jason")); 20 | } 21 | 22 | @Test 23 | void jsonAtIsNull() { 24 | assertAllWays("{\"name\":null}", "{\"name\":\"Damian\"}", 25 | assertion -> assertion.at("/name").isNull()); 26 | } 27 | 28 | @Test 29 | void jsonAtIsMissing() { 30 | assertAllWays("{}", "{\"name\":\"Damian\"}", 31 | assertion -> assertion.at("/name").isMissing()); 32 | } 33 | 34 | @Test 35 | void jsonIsNull() { 36 | assertAllWays("null", "{\"name\":\"Damian\"}", 37 | JsonNodeAssertDsl::isNull); 38 | } 39 | 40 | @Test 41 | void recursiveJsonMatching() { 42 | assertAllWays("{\"guid\":\"625110f5-502f-4748-8f1d-bad2237aa0aa\"}", 43 | "{\"guid\":\"not-a-guid\"}", 44 | assertion -> assertion.at("") 45 | .matches(jsonNode() 46 | .at("/guid") 47 | .matches(GUID_PATTERN.pattern()))); 48 | } 49 | 50 | @Test 51 | void numericIsGreaterThan() { 52 | assertAllWays("{\"count\":10}", "{\"count\":9}", 53 | assertion -> assertion.at("/count").isGreaterThan(9)); 54 | } 55 | 56 | @Test 57 | void intIsGreaterThan() { 58 | assertAllWays("{\"count\":10}", "{\"count\":9}", 59 | assertion -> assertion.at("/count").isGreaterThanInt(9)); 60 | } 61 | 62 | @Test 63 | void longIsGreaterThan() { 64 | assertAllWays("{\"count\":10}", "{\"count\":9}", 65 | assertion -> assertion.at("/count").isGreaterThanLong(9)); 66 | } 67 | 68 | @Test 69 | void longIsGreaterThanOrEqualTo() { 70 | assertAllWays("{\"count\":9}", "{\"count\":8}", 71 | assertion -> assertion.at("/count").isGreaterThanOrEqualToLong(9)); 72 | } 73 | 74 | @Test 75 | void numericIsGreaterThanViaNumbersDsl() { 76 | assertAllWays("{\"count\":10}", "{\"count\":9}", 77 | assertion -> assertion.at("/count").number().isGreaterThan(9)); 78 | } 79 | 80 | @Test 81 | void jsonAtStringInStringContext() { 82 | assertAllWays("{\"name\":\"Jason\"}", "{\"name\":\"Damian\"}", 83 | assertion -> assertion.at("/name") 84 | .text().isText("Jason")); 85 | } 86 | 87 | @Test 88 | void jsonAtStringInStringContextDetectString() { 89 | assertAllWays("{\"name\":\"Jason\"}", "{\"name\":null}", 90 | assertion -> assertion.at("/name") 91 | .text()); 92 | } 93 | 94 | @Test 95 | void recursiveAtIsPossibleEvenThoughPointless() { 96 | assertAllWays("{\"root\":{\"name\":\"Jason\"}}", "{\"root\":{\"name\":\"Damo\"}}", 97 | assertion -> assertion.at("/root").at("/name") 98 | .text().isText("Jason")); 99 | } 100 | 101 | @Test 102 | void isObjectOnRoot() { 103 | assertAllWays("{}", "null", 104 | JsonNodeAssertDsl::isObject); 105 | } 106 | 107 | @Test 108 | void isNotObjectOnRoot() { 109 | assertAllWays("123", "{}", 110 | JsonNodeAssertDsl::isNotObject); 111 | } 112 | 113 | @Test 114 | void isArrayOnRoot() { 115 | assertAllWays("[]", "{}", JsonNodeAssertDsl::isArray); 116 | } 117 | 118 | @Test 119 | void isNotArrayOnRoot() { 120 | assertAllWays("{}", "[]", JsonNodeAssertDsl::isNotArray); 121 | } 122 | 123 | @Test 124 | void isNotArrayContextOnRoot() { 125 | assertAllWays("{}", "[]", 126 | assertion -> assertion.isNotArray()); 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /src/test/java/uk/org/webcompere/modelassert/json/impl/MockitoArgumentMatcherTest.java: -------------------------------------------------------------------------------- 1 | package uk.org.webcompere.modelassert.json.impl; 2 | 3 | import org.junit.jupiter.api.Test; 4 | import org.junit.jupiter.api.extension.ExtendWith; 5 | import org.mockito.Mock; 6 | import org.mockito.junit.jupiter.MockitoExtension; 7 | 8 | import static org.assertj.core.api.AssertionsForClassTypes.assertThat; 9 | import static org.mockito.ArgumentMatchers.argThat; 10 | import static org.mockito.BDDMockito.given; 11 | import static org.mockito.BDDMockito.then; 12 | import static uk.org.webcompere.modelassert.json.JsonAssertions.json; 13 | 14 | @ExtendWith(MockitoExtension.class) 15 | class MockitoArgumentMatcherTest { 16 | interface SomeInterface { 17 | String findValueFromJson(String json); 18 | } 19 | 20 | @Mock 21 | private SomeInterface someInterface; 22 | 23 | @Test 24 | void whenCallMockThenCanDetectItByJsonAssertion() { 25 | someInterface.findValueFromJson("{\"name\":\"foo\"}"); 26 | 27 | then(someInterface) 28 | .should() 29 | .findValueFromJson(argThat(json() 30 | .at("/name").hasValue("foo") 31 | .toArgumentMatcher())); 32 | } 33 | 34 | @Test 35 | void whenCallMockThenAnswerCanBeBasedOnJson() { 36 | given(someInterface.findValueFromJson(argThat(json() 37 | .at("/name").hasValue("foo") 38 | .toArgumentMatcher()))) 39 | .willReturn("foo"); 40 | given(someInterface.findValueFromJson(argThat(json() 41 | .at("/name").hasValue("bar") 42 | .toArgumentMatcher()))) 43 | .willReturn("bar"); 44 | 45 | assertThat(someInterface.findValueFromJson("{}")).isNull(); 46 | assertThat(someInterface.findValueFromJson("{\"name\":\"foo\"}")).isEqualTo("foo"); 47 | assertThat(someInterface.findValueFromJson("{\"name\":\"bar\"}")).isEqualTo("bar"); 48 | } 49 | } -------------------------------------------------------------------------------- /src/test/resources/examples/json-alphabetic.json: -------------------------------------------------------------------------------- 1 | { 2 | "a": "a", 3 | "b": "b", 4 | "c": "c", 5 | "d": "d" 6 | } 7 | -------------------------------------------------------------------------------- /src/test/resources/examples/json-array-of-objects-in-order.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": [ 3 | { 4 | "id": 1, 5 | "name": "Adam" 6 | }, 7 | { 8 | "id": 2, 9 | "name": "Ben" 10 | }, 11 | { 12 | "id": 3, 13 | "name": "Charlie" 14 | } 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /src/test/resources/examples/json-array-of-objects-out-of-order.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": [ 3 | { 4 | "id": 1, 5 | "name": "Adam" 6 | }, 7 | { 8 | "id": 3, 9 | "name": "Charlie" 10 | }, 11 | { 12 | "id": 2, 13 | "name": "Ben" 14 | } 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /src/test/resources/examples/json-array-of-objects-with-additional.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": [ 3 | { 4 | "id": 1, 5 | "name": "Adam" 6 | }, 7 | { 8 | "id": 2, 9 | "name": "Ben" 10 | }, 11 | { 12 | "id": 3, 13 | "name": "Charlie" 14 | }, 15 | { 16 | "id": 4, 17 | "name": "Dirty Den" 18 | } 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /src/test/resources/examples/json-scrambled.json: -------------------------------------------------------------------------------- 1 | { 2 | "b": "b", 3 | "d": "d", 4 | "a": "a", 5 | "c": "c" 6 | } 7 | -------------------------------------------------------------------------------- /src/test/resources/examples/json-string-array-alphabetic.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": [ 3 | "a","b","c","d","e" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /src/test/resources/examples/json-string-array-scrambled-and-additional.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": [ 3 | "b","e","c","a","d","z" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /src/test/resources/examples/json-string-array-scrambled-and-missing.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": [ 3 | "b","e","c","a" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /src/test/resources/examples/json-string-array-scrambled.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": [ 3 | "b","e","c","a","d" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /src/test/resources/examples/yaml-tree-2.yml: -------------------------------------------------------------------------------- 1 | - name: Colin 2 | age: 42 3 | children: 4 | - name: Mary 5 | age: 16 6 | - name: Bert 7 | age: 16 8 | - name: Richard 9 | age: 12 10 | children: [] 11 | - name: Colin 12 | age: 42 13 | children: 14 | - name: Rebecca 15 | age: 13 16 | - name: Bert 17 | age: 16 18 | - name: Carl 19 | age: 42 20 | children: [] 21 | - name: Colin 22 | age: 42 23 | children: 24 | - name: Bert 25 | age: 16 26 | - name: Rebecca 27 | age: 13 28 | -------------------------------------------------------------------------------- /src/test/resources/examples/yaml-tree.yml: -------------------------------------------------------------------------------- 1 | - name: Colin 2 | age: 42 3 | children: 4 | - name: Bert 5 | age: 16 6 | - name: Rebecca 7 | age: 13 8 | - name: Colin 9 | age: 42 10 | children: 11 | - name: Bert 12 | age: 16 13 | - name: Mary 14 | age: 16 15 | - name: Colin 16 | age: 42 17 | children: 18 | - name: Bert 19 | age: 16 20 | - name: Rebecca 21 | age: 13 22 | - name: Richard 23 | age: 12 24 | children: [] 25 | - name: Carl 26 | age: 42 27 | children: [] 28 | -------------------------------------------------------------------------------- /src/test/resources/simple-copy.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Mr Name", 3 | "age": 42, 4 | "isOld": true, 5 | "child": { 6 | "name": "Ms Child", 7 | "age": 7, 8 | "isOld": false 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/test/resources/simple.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Mr Name", 3 | "age": 42, 4 | "isOld": true, 5 | "child": { 6 | "name": "Ms Child", 7 | "age": 7, 8 | "isOld": false 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/test/resources/simple.yaml: -------------------------------------------------------------------------------- 1 | name: Mr Name 2 | age: 42 3 | isOld: true 4 | child: 5 | name: Ms Child 6 | age: 7 7 | isOld: false 8 | --------------------------------------------------------------------------------