diff --git a/CHANGES.md b/CHANGES.md
index 8c25d13a52..ae2e550f7f 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -11,6 +11,10 @@ We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format (
## [Unreleased]
+### Added
+* Added formatter for [JVM-based JSON formatting](https://github.com/diffplug/spotless/issues/850)
+* Added Gradle configuration JVM-based JSON formatting
+
## [2.14.0] - 2021-06-10
### Added
* Added support for `eclipse-cdt` at `4.19.0`. Note that version requires Java 11 or higher.
diff --git a/lib/src/main/java/com/diffplug/spotless/json/JsonSimpleStep.java b/lib/src/main/java/com/diffplug/spotless/json/JsonSimpleStep.java
new file mode 100644
index 0000000000..118f81d0d7
--- /dev/null
+++ b/lib/src/main/java/com/diffplug/spotless/json/JsonSimpleStep.java
@@ -0,0 +1,106 @@
+/*
+ * Copyright 2021 DiffPlug
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.diffplug.spotless.json;
+
+import java.io.IOException;
+import java.io.Serializable;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.Objects;
+
+import com.diffplug.spotless.FormatterFunc;
+import com.diffplug.spotless.FormatterStep;
+import com.diffplug.spotless.JarState;
+import com.diffplug.spotless.Provisioner;
+
+/**
+ * Simple JSON formatter which reformats the file according to the org.json library's default pretty-printing, but has no ability to customise more than the indentation size.
+ */
+public final class JsonSimpleStep {
+ private static final String MAVEN_COORDINATE = "org.json:json:";
+ private static final String DEFAULT_VERSION = "20210307";
+
+ public static FormatterStep create(int indent, Provisioner provisioner) {
+ Objects.requireNonNull(provisioner, "provisioner cannot be null");
+ return FormatterStep.createLazy("json", () -> new State(indent, provisioner), State::toFormatter);
+ }
+
+ private static final class State implements Serializable {
+ private static final long serialVersionUID = 1L;
+
+ private final int indentSpaces;
+ private final JarState jarState;
+
+ private State(int indent, Provisioner provisioner) throws IOException {
+ this.indentSpaces = indent;
+ this.jarState = JarState.from(MAVEN_COORDINATE + DEFAULT_VERSION, provisioner);
+ }
+
+ FormatterFunc toFormatter() {
+ Method objectToString;
+ Method arrayToString;
+ Constructor> objectConstructor;
+ Constructor> arrayConstructor;
+ try {
+ ClassLoader classLoader = jarState.getClassLoader();
+ Class> jsonObject = classLoader.loadClass("org.json.JSONObject");
+ Class>[] constructorArguments = new Class[]{String.class};
+ objectConstructor = jsonObject.getConstructor(constructorArguments);
+ objectToString = jsonObject.getMethod("toString", int.class);
+
+ Class> jsonArray = classLoader.loadClass("org.json.JSONArray");
+ arrayConstructor = jsonArray.getConstructor(constructorArguments);
+ arrayToString = jsonArray.getMethod("toString", int.class);
+ } catch (ClassNotFoundException | NoSuchMethodException e) {
+ throw new IllegalStateException("There was a problem preparing org.json dependencies", e);
+ }
+
+ return s -> {
+ String prettyPrinted = null;
+ if (s.isEmpty()) {
+ prettyPrinted = s;
+ }
+ if (s.startsWith("{")) {
+ try {
+ Object parsed = objectConstructor.newInstance(s);
+ prettyPrinted = objectToString.invoke(parsed, indentSpaces) + "\n";
+ } catch (InvocationTargetException ignored) {
+ // ignore if we cannot convert to JSON string
+ }
+ }
+ if (s.startsWith("[")) {
+ try {
+ Object parsed = arrayConstructor.newInstance(s);
+ prettyPrinted = arrayToString.invoke(parsed, indentSpaces) + "\n";
+ } catch (InvocationTargetException ignored) {
+ // ignore if we cannot convert to JSON string
+ }
+ }
+
+ if (prettyPrinted == null) {
+ throw new AssertionError("Invalid JSON file provided");
+ }
+
+ return prettyPrinted;
+ };
+ }
+ }
+
+ private JsonSimpleStep() {
+ // cannot be directly instantiated
+ }
+}
diff --git a/lib/src/main/java/com/diffplug/spotless/json/package-info.java b/lib/src/main/java/com/diffplug/spotless/json/package-info.java
new file mode 100644
index 0000000000..0d62356d77
--- /dev/null
+++ b/lib/src/main/java/com/diffplug/spotless/json/package-info.java
@@ -0,0 +1,7 @@
+@ParametersAreNonnullByDefault
+@ReturnValuesAreNonnullByDefault
+package com.diffplug.spotless.extra.json.java;
+
+import javax.annotation.ParametersAreNonnullByDefault;
+
+import com.diffplug.spotless.annotations.ReturnValuesAreNonnullByDefault;
diff --git a/plugin-gradle/CHANGES.md b/plugin-gradle/CHANGES.md
index e271edce27..b0dbc43827 100644
--- a/plugin-gradle/CHANGES.md
+++ b/plugin-gradle/CHANGES.md
@@ -4,6 +4,8 @@ We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format (
## [Unreleased]
+### Added
+* Added Gradle configuration [JVM-based JSON formatting](https://github.com/diffplug/spotless/issues/850)
### Fixed
* Fixed IndexOutOfBoundsException in parallel execution of `eclipse-groovy` formatter ([#877](https://github.com/diffplug/spotless/issues/877))
diff --git a/plugin-gradle/README.md b/plugin-gradle/README.md
index ad2bbf7681..de4a2e434c 100644
--- a/plugin-gradle/README.md
+++ b/plugin-gradle/README.md
@@ -68,6 +68,7 @@ Spotless supports all of Gradle's built-in performance features (incremental bui
- [Antlr4](#antlr4) ([antlr4formatter](#antlr4formatter))
- [SQL](#sql) ([dbeaver](#dbeaver), [prettier](#prettier))
- [Typescript](#typescript) ([tsfmt](#tsfmt), [prettier](#prettier))
+ - [JSON](#json)
- Multiple languages
- [Prettier](#prettier) ([plugins](#prettier-plugins), [npm detection](#npm-detection), [`.npmrc` detection](#npmrc-detection))
- javascript, jsx, angular, vue, flow, typescript, css, less, scss, html, json, graphql, markdown, ymaml
@@ -527,6 +528,36 @@ spotless {
For details, see the [npm detection](#npm-detection) and [`.npmrc` detection](#npmrc-detection) sections of prettier, which apply also to tsfmt.
+## JSON
+
+- `com.diffplug.gradle.spotless.JsonExtension` [javadoc](https://javadoc.io/static/com.diffplug.spotless/spotless-plugin-gradle/5.13.0/com/diffplug/gradle/spotless/JsonExtension.html), [code](https://github.com/diffplug/spotless/blob/main/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/JsonExtension.java)
+
+```gradle
+spotless {
+ json {
+ target 'src/**/*.json' // you have to set the target manually
+ simple() // has its own section below
+ prettier().config(['parser': 'json']) // see Prettier section below
+ eclipseWtp('json') // see Eclipse web tools platform section
+ }
+}
+```
+
+### simple
+
+Uses a JSON pretty-printer that optionally allows configuring the number of spaces that are used to pretty print objects:
+
+```gradle
+spotless {
+ json {
+ target 'src/**/*.json'
+ simple()
+ // optional: specify the number of spaces to use
+ simple().indentWithSpaces(6)
+ }
+}
+```
+
## Prettier
diff --git a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/JsonExtension.java b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/JsonExtension.java
new file mode 100644
index 0000000000..e15d395f3b
--- /dev/null
+++ b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/JsonExtension.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright 2016-2021 DiffPlug
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.diffplug.gradle.spotless;
+
+import javax.inject.Inject;
+
+import com.diffplug.spotless.FormatterStep;
+import com.diffplug.spotless.json.JsonSimpleStep;
+
+public class JsonExtension extends FormatExtension {
+ private static final int DEFAULT_INDENTATION = 4;
+ static final String NAME = "json";
+
+ @Inject
+ public JsonExtension(SpotlessExtension spotless) {
+ super(spotless);
+ }
+
+ @Override
+ protected void setupTask(SpotlessTask task) {
+ if (target == null) {
+ throw noDefaultTargetException();
+ }
+ super.setupTask(task);
+ }
+
+ public SimpleConfig simple() {
+ return new SimpleConfig(DEFAULT_INDENTATION);
+ }
+
+ public class SimpleConfig {
+ private int indent;
+
+ public SimpleConfig(int indent) {
+ this.indent = indent;
+ addStep(createStep());
+ }
+
+ public void indentWithSpaces(int indent) {
+ this.indent = indent;
+ replaceStep(createStep());
+ }
+
+ private FormatterStep createStep() {
+ return JsonSimpleStep.create(indent, provisioner());
+ }
+ }
+
+}
diff --git a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/SpotlessExtension.java b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/SpotlessExtension.java
index 1f55efc1ee..3e56dda443 100644
--- a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/SpotlessExtension.java
+++ b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/SpotlessExtension.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2016-2020 DiffPlug
+ * Copyright 2016-2021 DiffPlug
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -169,6 +169,12 @@ public void python(Action closure) {
format(PythonExtension.NAME, PythonExtension.class, closure);
}
+ /** Configures the special JSON-specific extension. */
+ public void json(Action closure) {
+ requireNonNull(closure);
+ format(JsonExtension.NAME, JsonExtension.class, closure);
+ }
+
/** Configures a custom extension. */
public void format(String name, Action closure) {
requireNonNull(name, "name");
diff --git a/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/JsonExtensionTest.java b/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/JsonExtensionTest.java
new file mode 100644
index 0000000000..cd8c56ad4c
--- /dev/null
+++ b/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/JsonExtensionTest.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright 2021 DiffPlug
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.diffplug.gradle.spotless;
+
+import java.io.IOException;
+
+import org.junit.Test;
+
+public class JsonExtensionTest extends GradleIntegrationHarness {
+ @Test
+ public void defaultFormatting() throws IOException {
+ setFile("build.gradle").toLines(
+ "buildscript { repositories { mavenCentral() } }",
+ "plugins {",
+ " id 'java'",
+ " id 'com.diffplug.spotless'",
+ "}",
+ "spotless {",
+ " json {",
+ " target 'examples/**/*.json'",
+ " simple()",
+ "}",
+ "}");
+ setFile("src/main/resources/example.json").toResource("json/nestedObjectBefore.json");
+ setFile("examples/main/resources/example.json").toResource("json/nestedObjectBefore.json");
+ gradleRunner().withArguments("spotlessApply").build();
+ assertFile("src/main/resources/example.json").sameAsResource("json/nestedObjectBefore.json");
+ assertFile("examples/main/resources/example.json").sameAsResource("json/nestedObjectAfter.json");
+ }
+
+ @Test
+ public void formattingWithCustomNumberOfSpaces() throws IOException {
+ setFile("build.gradle").toLines(
+ "buildscript { repositories { mavenCentral() } }",
+ "plugins {",
+ " id 'java'",
+ " id 'com.diffplug.spotless'",
+ "}",
+ "spotless {",
+ " json {",
+ " target 'src/**/*.json'",
+ " simple().indentWithSpaces(6)",
+ "}",
+ "}");
+ setFile("src/main/resources/example.json").toResource("json/singletonArrayBefore.json");
+ gradleRunner().withArguments("spotlessApply").build();
+ assertFile("src/main/resources/example.json").sameAsResource("json/singletonArrayAfter6Spaces.json");
+ }
+}
diff --git a/testlib/src/main/resources/json/cucumberJsonSampleAfter.json b/testlib/src/main/resources/json/cucumberJsonSampleAfter.json
new file mode 100644
index 0000000000..dc0f6f90f7
--- /dev/null
+++ b/testlib/src/main/resources/json/cucumberJsonSampleAfter.json
@@ -0,0 +1,550 @@
+[
+ {
+ "line": 2,
+ "elements": [
+ {
+ "line": 7,
+ "name": "Activate Credit Card",
+ "description": "Perfect background",
+ "keyword": "Background",
+ "type": "background",
+ "steps": [
+ {
+ "result": {
+ "duration": 99107447000,
+ "status": "passed"
+ },
+ "embeddings": [
+ {
+ "data": "",
+ "mime_type": "image/url"
+ },
+ {
+ "data": "",
+ "media": {"type": "text/plain"}
+ }
+ ],
+ "line": 8,
+ "name": "I have a new credit card",
+ "match": {"location": "ATMScenario.I_have_a_new_credit_card()"},
+ "keyword": "Given "
+ },
+ {
+ "result": {
+ "duration": 9520000,
+ "status": "passed"
+ },
+ "line": 9,
+ "name": "My credit card is described as follow:",
+ "match": {"location": "ATMScenario.My_credit_card_is_described_as_follow"},
+ "keyword": "And ",
+ "doc_string": {
+ "content_type": "",
+ "line": 10,
+ "value": "{\n\"issuer\": {\n\"name\": \"Real Bank Inc.\",\n\"isn:\": \"RB55800093842N\"\n},\n\"card_number\": \"4896 0215 8478 6325\",\n\"holder\": \"A guy\"\n}"
+ }
+ },
+ {
+ "result": {
+ "duration": 7040000,
+ "status": "passed"
+ },
+ "line": 18,
+ "name": "I confirm my pin number",
+ "match": {"location": "ATMScenario.I_confirm_my_pin_number()"},
+ "keyword": "When ",
+ "rows": [
+ {
+ "cells": [
+ "Müller",
+ "Deutschland"
+ ],
+ "line": 2
+ },
+ {
+ "cells": [
+ "Nováková",
+ "Česko"
+ ],
+ "line": 3
+ },
+ {
+ "cells": [
+ "Kovačević",
+ "Hrvatska"
+ ],
+ "line": 4
+ },
+ {
+ "cells": [
+ "Παπαδόπουλος",
+ "Παπαδόπουλος"
+ ],
+ "line": 5
+ },
+ {
+ "cells": [
+ "罗/羅",
+ "中國"
+ ],
+ "line": 6
+ }
+ ]
+ },
+ {
+ "result": {
+ "duration": 111111,
+ "status": "passed"
+ },
+ "line": 19,
+ "name": "the card should be activated",
+ "match": {"location": "ATMScenario.the_card_should_be_activated()"},
+ "keyword": "Then "
+ }
+ ]
+ },
+ {
+ "line": 33,
+ "name": "Account has ",
+ "description": "Account holder withdraws cash",
+ "id": "account-holder-withdraws-cash;account-has-'sufficient-funds';;2",
+ "after": [{
+ "result": {
+ "duration": 60744700,
+ "error_message": "Completed",
+ "status": "passed"
+ },
+ "match": {"location": "MachineFactory.timeout()"}
+ }],
+ "keyword": "Scenario Outline",
+ "type": "scenario",
+ "steps": [
+ {
+ "result": {
+ "duration": 17007000,
+ "status": "passed"
+ },
+ "line": 23,
+ "name": "the account balance is 100",
+ "match": {
+ "arguments": [{
+ "val": "100",
+ "offset": 23
+ }],
+ "location": "ATMScenario.createAccount(int)"
+ },
+ "keyword": "Given "
+ },
+ {
+ "result": {
+ "duration": 33444444,
+ "status": "passed"
+ },
+ "line": 24,
+ "name": "the card is valid",
+ "match": {
+ "arguments": [{
+ "val": "",
+ "offset": 0
+ }],
+ "location": "ATMScenario.createCreditCard()"
+ },
+ "keyword": "And "
+ },
+ {
+ "result": {
+ "duration": 44333333,
+ "status": "passed"
+ },
+ "line": 25,
+ "name": "100 is contained in the machine",
+ "match": {
+ "arguments": [{
+ "val": "100",
+ "offset": 0
+ }],
+ "location": "ATMScenario.createATM(int)"
+ },
+ "matchedColumns": [1],
+ "keyword": "And "
+ },
+ {
+ "result": {
+ "duration": 11000001,
+ "status": "passed"
+ },
+ "line": 26,
+ "name": "the Account Holder requests 10, entering PIN 1234",
+ "match": {
+ "arguments": [
+ {
+ "val": "10",
+ "offset": 28
+ },
+ {
+ "val": "1234",
+ "offset": 45
+ }
+ ],
+ "location": "ATMScenario.requestMoney(int)"
+ },
+ "matchedColumns": [2],
+ "keyword": "When "
+ },
+ {
+ "result": {
+ "duration": 3220000,
+ "status": "passed"
+ },
+ "line": 27,
+ "name": "the ATM should dispense 10 monetary units",
+ "match": {
+ "arguments": [
+ {
+ "val": "10",
+ "offset": 24
+ },
+ {
+ "val": "",
+ "offset": 0
+ }
+ ],
+ "location": "ATMScenario.checkMoney(int)"
+ },
+ "matchedColumns": [3],
+ "keyword": "Then "
+ },
+ {
+ "result": {
+ "duration": 30000000,
+ "status": "passed"
+ },
+ "line": 28,
+ "name": "the account balance should be 90",
+ "match": {"location": "ATMScenario.checkBalance(int)"},
+ "arguments": [{"rows": [
+ {"cells": [
+ "max",
+ "min"
+ ]},
+ {"cells": [
+ "20",
+ "3"
+ ]}
+ ]}],
+ "matchedColumns": [2],
+ "keyword": "And "
+ }
+ ],
+ "tags": [
+ {
+ "line": 21,
+ "name": "@fast"
+ },
+ {
+ "line": 1,
+ "name": "@featureTag"
+ },
+ {
+ "line": 21,
+ "name": "@checkout"
+ }
+ ]
+ }
+ ],
+ "name": "1st feature",
+ "description": "This is description of the feature",
+ "id": "account-holder-withdraws-cash",
+ "keyword": "Feature",
+ "uri": "net/masterthought/example(s)/ATM:東京.feature",
+ "tags": [{
+ "line": 1,
+ "name": "@featureTag"
+ }]
+ },
+ {
+ "line": 1,
+ "elements": [
+ {
+ "before": [
+ {
+ "output": ["System version: beta3"],
+ "result": {
+ "duration": 10744700,
+ "status": "passed"
+ },
+ "match": {"location": "MachineFactory.findCashMachine()"}
+ },
+ {
+ "result": {
+ "duration": 1000001,
+ "error_message": " \n",
+ "status": "failed"
+ },
+ "match": {"location": "MachineFactory.wait()"}
+ }
+ ],
+ "line": 19,
+ "name": "Account may not have sufficient funds",
+ "description": "Account holder withdraws more cash",
+ "id": "account-holder-withdraws-more-cash;account-has-sufficient-funds;;2",
+ "after": [{
+ "result": {
+ "duration": 64700000,
+ "error_message": "Undefined step",
+ "status": "undefined"
+ },
+ "embeddings": [{
+ "data": "",
+ "mime_type": "image/png"
+ }],
+ "match": {"location": "any.error()"}
+ }],
+ "keyword": "Scenario Outline",
+ "type": "scenario",
+ "steps": [
+ {
+ "result": {"status": "undefined"},
+ "before": [{
+ "embeddings": [{
+ "data": "",
+ "mime_type": "text/plain"
+ }],
+ "result": {
+ "duration": 410802047,
+ "status": "failed"
+ }
+ }],
+ "line": 7,
+ "name": "the account balance is 100",
+ "match": {"arguments": [
+ {
+ "val": "100",
+ "offset": 23
+ },
+ {}
+ ]},
+ "matchedColumns": [0],
+ "keyword": "Given "
+ },
+ {
+ "result": {
+ "duration": 13000,
+ "status": "passed"
+ },
+ "line": 8,
+ "name": "the card is valid",
+ "match": {
+ "arguments": [{
+ "val": "",
+ "offset": 17
+ }],
+ "location": "ATMScenario.createCreditCard()"
+ },
+ "after": [{
+ "result": {
+ "duration": 410802048,
+ "status": "passed"
+ },
+ "match": {"location": "StepHook.afterStep()"}
+ }],
+ "keyword": "And "
+ },
+ {
+ "result": {
+ "duration": 36000,
+ "status": "passed"
+ },
+ "line": 9,
+ "name": "the machine contains 100",
+ "match": {
+ "arguments": [{
+ "val": "100",
+ "offset": 21
+ }],
+ "location": "ATMScenario.createATM(int)"
+ },
+ "matchedColumns": [1],
+ "keyword": "And "
+ },
+ {
+ "result": {
+ "duration": 32000,
+ "status": "passed"
+ },
+ "line": 10,
+ "name": "the Account Holder requests 20",
+ "match": {
+ "arguments": [{
+ "val": "20",
+ "offset": 28
+ }],
+ "location": "ATMScenario.requestMoney(int)"
+ },
+ "matchedColumns": [2],
+ "keyword": "When "
+ },
+ {
+ "result": {
+ "duration": 36000,
+ "status": "passed"
+ },
+ "line": 11,
+ "name": "the ATM should dispense 20",
+ "match": {
+ "arguments": [{
+ "val": "20",
+ "offset": 24
+ }],
+ "location": "ATMScenario.checkMoney(int)"
+ },
+ "matchedColumns": [3],
+ "keyword": "Then "
+ },
+ {
+ "result": {
+ "duration": 1933000,
+ "error_message": "java.lang.AssertionError: \nExpected: is <80>\n got: <90>\n\n\tat org.junit.Assert.assertThat(Assert.java:780)\n\tat org.junit.Assert.assertThat(Assert.java:738)\n\tat net.masterthought.example.ATMScenario.checkBalance(ATMScenario.java:69)\n\tat ✽.And the account balance should be 90(net/masterthought/example/ATMK.feature:12)\n",
+ "status": "skipped"
+ },
+ "embeddings": [
+ {
+ "data": "",
+ "mime_type": "image/png",
+ "name": "Some PNG image"
+ },
+ {
+ "data": "",
+ "mime_type": "image/jpeg"
+ },
+ {
+ "data": "",
+ "mime_type": "text/plain"
+ },
+ {
+ "data": "",
+ "mime_type": "text/html",
+ "name": "Some HTML embedding"
+ },
+ {
+ "data": "",
+ "mime_type": "text/xml"
+ },
+ {
+ "data": "",
+ "mime_type": "image/svg+xml"
+ },
+ {
+ "data": "",
+ "mime_type": "js"
+ },
+ {
+ "data": "",
+ "mime_type": "text/plain"
+ },
+ {
+ "data": "",
+ "mime_type": "text/csv"
+ },
+ {
+ "data": "",
+ "mime_type": "video/mp4"
+ }
+ ],
+ "line": 12,
+ "name": "the account balance should be 90",
+ "match": {
+ "arguments": [{
+ "val": "90",
+ "offset": 30
+ }],
+ "location": "ATMScenario.checkBalance(int)"
+ },
+ "matchedColumns": [4],
+ "keyword": "And "
+ },
+ {
+ "result": {"status": "pending"},
+ "embeddings": [{
+ "data": "",
+ "mime_type": "application/json"
+ }],
+ "line": 13,
+ "name": "the card should be returned",
+ "match": {"location": "ATMScenario.cardShouldBeReturned()"},
+ "keyword": "And "
+ },
+ {
+ "result": {"status": "skipped"},
+ "output": [
+ ["Could not connect to the server @Rocky@"],
+ ["Could not connect to the server @Mike@"]
+ ],
+ "line": 14,
+ "name": "its not implemented",
+ "match": {"location": "ATMScenario.its_not_implemented()"},
+ "keyword": "And "
+ },
+ {
+ "result": {"status": "failed"},
+ "output": [
+ "Checkpoints",
+ 232
+ ],
+ "line": 15,
+ "name": "the card is valid",
+ "match": {"location": "ATMScenario.createCreditCard()"},
+ "keyword": "And "
+ },
+ {
+ "result": {
+ "duration": 90000000,
+ "status": "ambiguous"
+ },
+ "line": 29,
+ "name": "the card should be returned",
+ "match": {"location": "ATMScenario.cardShouldBeReturned()"},
+ "keyword": "And "
+ }
+ ],
+ "tags": [{
+ "line": 101,
+ "name": "@checkout"
+ }]
+ },
+ {
+ "line": 31,
+ "name": "Clean-up",
+ "id": "account-holder-withdraws-more-cash;clean-up",
+ "keyword": "Scenario",
+ "type": "scenario",
+ "steps": [{
+ "result": {
+ "duration": 560000,
+ "status": "passed"
+ },
+ "line": 32,
+ "name": "Stream closing",
+ "keyword": "Given "
+ }]
+ },
+ {
+ "line": 35,
+ "name": "This step has no result...",
+ "id": "undefined-result",
+ "keyword": "Scenario",
+ "type": "scenario",
+ "steps": [{
+ "line": 36,
+ "name": " - even it should",
+ "keyword": "Given "
+ }]
+ }
+ ],
+ "name": "Second feature",
+ "description": "As an Account Holder\nI want to withdraw cash from an ATM,
so that I can get money when the bank is closed",
+ "id": "account-holder-withdraws-more-cash",
+ "keyword": "Feature",
+ "uri": "net/masterthought/example/ATMK.feature"
+ }
+]
diff --git a/testlib/src/main/resources/json/cucumberJsonSampleBefore.json b/testlib/src/main/resources/json/cucumberJsonSampleBefore.json
new file mode 100644
index 0000000000..8250630cf7
--- /dev/null
+++ b/testlib/src/main/resources/json/cucumberJsonSampleBefore.json
@@ -0,0 +1,660 @@
+[
+ {
+ "id": "account-holder-withdraws-cash",
+ "tags": [
+ {
+ "name": "@featureTag",
+ "line": 1
+ }
+ ],
+ "description": "This is description of the feature",
+ "name": "1st feature",
+ "keyword": "Feature",
+ "line": 2,
+ "elements": [
+ {
+ "description": "Perfect background",
+ "name": "Activate Credit Card",
+ "keyword": "Background",
+ "line": 7,
+ "steps": [
+ {
+ "result": {
+ "duration": 99107447000,
+ "status": "passed"
+ },
+ "name": "I have a new credit card",
+ "keyword": "Given ",
+ "line": 8,
+ "match": {
+ "location": "ATMScenario.I_have_a_new_credit_card()"
+ },
+ "embeddings": [
+ {
+ "mime_type": "image/url",
+ "data": ""
+ },
+ {
+ "data": "",
+ "media": {
+ "type": "text/plain"
+ }
+ }
+ ]
+ },
+ {
+ "result": {
+ "duration": 9520000,
+ "status": "passed"
+ },
+ "name": "My credit card is described as follow:",
+ "keyword": "And ",
+ "line": 9,
+ "match": {
+ "location": "ATMScenario.My_credit_card_is_described_as_follow"
+ },
+ "doc_string": {
+ "content_type": "",
+ "line": 10,
+ "value": "{\n\"issuer\": {\n\"name\": \"Real Bank Inc.\",\n\"isn:\": \"RB55800093842N\"\n},\n\"card_number\": \"4896 0215 8478 6325\",\n\"holder\": \"A guy\"\n}"
+ }
+ },
+ {
+ "result": {
+ "duration": 7040000,
+ "status": "passed"
+ },
+ "name": "I confirm my pin number",
+ "keyword": "When ",
+ "line": 18,
+ "match": {
+ "location": "ATMScenario.I_confirm_my_pin_number()"
+ },
+ "rows": [
+ {
+ "cells": [
+ "Müller",
+ "Deutschland"
+ ],
+ "line": 2
+ },
+ {
+ "cells": [
+ "Nováková",
+ "Česko"
+ ],
+ "line": 3
+ },
+ {
+ "cells": [
+ "Kovačević",
+ "Hrvatska"
+ ],
+ "line": 4
+ },
+ {
+ "cells": [
+ "Παπαδόπουλος",
+ "Παπαδόπουλος"
+ ],
+ "line": 5
+ },
+ {
+ "cells": [
+ "罗/羅",
+ "中國"
+ ],
+ "line": 6
+ }
+ ]
+ },
+ {
+ "result": {
+ "duration": 111111,
+ "status": "passed"
+ },
+ "name": "the card should be activated",
+ "keyword": "Then ",
+ "line": 19,
+ "match": {
+ "location": "ATMScenario.the_card_should_be_activated()"
+ }
+ }
+ ],
+ "type": "background"
+ },
+ {
+ "id": "account-holder-withdraws-cash;account-has-\u0027sufficient-funds\u0027;;2",
+ "tags": [
+ {
+ "name": "@fast",
+ "line": 21
+ },
+ {
+ "name": "@featureTag",
+ "line": 1
+ },
+ {
+ "name": "@checkout",
+ "line": 21
+ }
+ ],
+ "description": "Account holder withdraws cash",
+ "name": "Account has ",
+ "keyword": "Scenario Outline",
+ "line": 33,
+ "steps": [
+ {
+ "result": {
+ "duration": 17007000,
+ "status": "passed"
+ },
+ "name": "the account balance is 100",
+ "keyword": "Given ",
+ "line": 23,
+ "match": {
+ "arguments": [
+ {
+ "val": "100",
+ "offset": 23
+ }
+ ],
+ "location": "ATMScenario.createAccount(int)"
+ }
+ },
+ {
+ "result": {
+ "duration": 33444444,
+ "status": "passed"
+ },
+ "name": "the card is valid",
+ "keyword": "And ",
+ "line": 24,
+ "match": {
+ "arguments": [
+ {
+ "val": "",
+ "offset": 0
+ }
+ ],
+ "location": "ATMScenario.createCreditCard()"
+ }
+ },
+ {
+ "result": {
+ "duration": 44333333,
+ "status": "passed"
+ },
+ "name": "100 is contained in the machine",
+ "keyword": "And ",
+ "line": 25,
+ "match": {
+ "arguments": [
+ {
+ "val": "100",
+ "offset": 0
+ }
+ ],
+ "location": "ATMScenario.createATM(int)"
+ },
+ "matchedColumns": [
+ 1
+ ]
+ },
+ {
+ "result": {
+ "duration": 11000001,
+ "status": "passed"
+ },
+ "name": "the Account Holder requests 10, entering PIN 1234",
+ "keyword": "When ",
+ "line": 26,
+ "match": {
+ "arguments": [
+ {
+ "val": "10",
+ "offset": 28
+ },
+ {
+ "val": "1234",
+ "offset": 45
+ }
+ ],
+ "location": "ATMScenario.requestMoney(int)"
+ },
+ "matchedColumns": [
+ 2
+ ]
+ },
+ {
+ "result": {
+ "duration": 3220000,
+ "status": "passed"
+ },
+ "name": "the ATM should dispense 10 monetary units",
+ "keyword": "Then ",
+ "line": 27,
+ "match": {
+ "arguments": [
+ {
+ "val": "10",
+ "offset": 24
+ },
+ {
+ "val": "",
+ "offset": 0
+ }
+ ],
+ "location": "ATMScenario.checkMoney(int)"
+ },
+ "matchedColumns": [
+ 3
+ ]
+ },
+ {
+ "result": {
+ "duration": 30000000,
+ "status": "passed"
+ },
+ "name": "the account balance should be 90",
+ "keyword": "And ",
+ "line": 28,
+ "arguments": [
+ {
+ "rows": [
+ {
+ "cells": [
+ "max",
+ "min"
+ ]
+ },
+ {
+ "cells": [
+ "20",
+ "3"
+ ]
+ }
+ ]
+ }
+ ],
+ "match": {
+ "location": "ATMScenario.checkBalance(int)"
+ },
+ "matchedColumns": [
+ 2
+ ]
+ }
+ ],
+ "type": "scenario",
+ "after": [
+ {
+ "result": {
+ "duration": 60744700,
+ "status": "passed",
+ "error_message": "Completed"
+ },
+ "match": {
+ "location": "MachineFactory.timeout()"
+ }
+ }
+ ]
+ }
+ ],
+ "uri": "net/masterthought/example(s)/ATM:東京.feature"
+ },
+ {
+ "id": "account-holder-withdraws-more-cash",
+ "description": "As an Account Holder\nI want to withdraw cash from an ATM,
so that I can get money when the bank is closed",
+ "name": "Second feature",
+ "keyword": "Feature",
+ "line": 1,
+ "elements": [
+ {
+ "id": "account-holder-withdraws-more-cash;account-has-sufficient-funds;;2",
+ "tags": [
+ {
+ "name": "@checkout",
+ "line": 101
+ }
+ ],
+ "before": [
+ {
+ "output": [
+ "System version: beta3"
+ ],
+ "result": {
+ "duration": 10744700,
+ "status": "passed"
+ },
+ "match": {
+ "location": "MachineFactory.findCashMachine()"
+ }
+ },
+ {
+ "result": {
+ "duration": 1000001,
+ "status": "failed",
+ "error_message": " \n"
+ },
+ "match": {
+ "location": "MachineFactory.wait()"
+ }
+ }
+ ],
+ "description": "Account holder withdraws more cash",
+ "name": "Account may not have sufficient funds",
+ "keyword": "Scenario Outline",
+ "line": 19,
+ "steps": [
+ {
+ "result": {
+ "status": "undefined"
+ },
+ "name": "the account balance is 100",
+ "keyword": "Given ",
+ "line": 7,
+ "match": {
+ "arguments": [
+ {
+ "val": "100",
+ "offset": 23
+ },
+ {}
+ ]
+ },
+ "matchedColumns": [
+ 0
+ ],
+ "before": [
+ {
+ "embeddings": [
+ {
+ "mime_type": "text/plain",
+ "data": ""
+ }
+ ],
+ "result": {
+ "duration": 410802047,
+ "status": "failed"
+ }
+ }
+ ]
+ },
+ {
+ "result": {
+ "duration": 13000,
+ "status": "passed"
+ },
+ "name": "the card is valid",
+ "keyword": "And ",
+ "line": 8,
+ "match": {
+ "arguments": [
+ {
+ "val": "",
+ "offset": 17
+ }
+ ],
+ "location": "ATMScenario.createCreditCard()"
+ },
+ "after": [
+ {
+ "result": {
+ "duration": 410802048,
+ "status": "passed"
+ },
+ "match": {
+ "location": "StepHook.afterStep()"
+ }
+ }
+ ]
+ },
+ {
+ "result": {
+ "duration": 36000,
+ "status": "passed"
+ },
+ "name": "the machine contains 100",
+ "keyword": "And ",
+ "line": 9,
+ "match": {
+ "arguments": [
+ {
+ "val": "100",
+ "offset": 21
+ }
+ ],
+ "location": "ATMScenario.createATM(int)"
+ },
+ "matchedColumns": [
+ 1
+ ]
+ },
+ {
+ "result": {
+ "duration": 32000,
+ "status": "passed"
+ },
+ "name": "the Account Holder requests 20",
+ "keyword": "When ",
+ "line": 10,
+ "match": {
+ "arguments": [
+ {
+ "val": "20",
+ "offset": 28
+ }
+ ],
+ "location": "ATMScenario.requestMoney(int)"
+ },
+ "matchedColumns": [
+ 2
+ ]
+ },
+ {
+ "result": {
+ "duration": 36000,
+ "status": "passed"
+ },
+ "name": "the ATM should dispense 20",
+ "keyword": "Then ",
+ "line": 11,
+ "match": {
+ "arguments": [
+ {
+ "val": "20",
+ "offset": 24
+ }
+ ],
+ "location": "ATMScenario.checkMoney(int)"
+ },
+ "matchedColumns": [
+ 3
+ ]
+ },
+ {
+ "result": {
+ "duration": 1933000,
+ "status": "skipped",
+ "error_message": "java.lang.AssertionError: \nExpected: is \u003c80\u003e\n got: \u003c90\u003e\n\n\tat org.junit.Assert.assertThat(Assert.java:780)\n\tat org.junit.Assert.assertThat(Assert.java:738)\n\tat net.masterthought.example.ATMScenario.checkBalance(ATMScenario.java:69)\n\tat ✽.And the account balance should be 90(net/masterthought/example/ATMK.feature:12)\n"
+ },
+ "name": "the account balance should be 90",
+ "keyword": "And ",
+ "line": 12,
+ "match": {
+ "arguments": [
+ {
+ "val": "90",
+ "offset": 30
+ }
+ ],
+ "location": "ATMScenario.checkBalance(int)"
+ },
+ "matchedColumns": [
+ 4
+ ],
+ "embeddings": [
+ {
+ "mime_type": "image/png",
+ "data": "",
+ "name": "Some PNG image"
+ },
+ {
+ "mime_type": "image/jpeg",
+ "data": ""
+ },
+ {
+ "mime_type": "text/plain",
+ "data": ""
+ },
+ {
+ "mime_type": "text/html",
+ "data": "",
+ "name": "Some HTML embedding"
+ },
+ {
+ "mime_type": "text/xml",
+ "data": ""
+ },
+ {
+ "mime_type": "image/svg+xml",
+ "data": ""
+ },
+ {
+ "mime_type": "js",
+ "data": ""
+ },
+ {
+ "mime_type": "text/plain",
+ "data": ""
+ },
+ {
+ "mime_type": "text/csv",
+ "data": ""
+ },
+ {
+ "mime_type": "video/mp4",
+ "data": ""
+ }
+ ]
+ },
+ {
+ "result": {
+ "status": "pending"
+ },
+ "name": "the card should be returned",
+ "keyword": "And ",
+ "line": 13,
+ "match": {
+ "location": "ATMScenario.cardShouldBeReturned()"
+ },
+ "embeddings": [
+ {
+ "mime_type": "application/json",
+ "data": ""
+ }
+ ]
+ },
+ {
+ "result": {
+ "status": "skipped"
+ },
+ "name": "its not implemented",
+ "keyword": "And ",
+ "line": 14,
+ "match": {
+ "location": "ATMScenario.its_not_implemented()"
+ },
+ "output": [
+ [
+ "Could not connect to the server @Rocky@"
+ ],
+ [
+ "Could not connect to the server @Mike@"
+ ]
+ ]
+ },
+ {
+ "result": {
+ "status": "failed"
+ },
+ "name": "the card is valid",
+ "keyword": "And ",
+ "line": 15,
+ "match": {
+ "location": "ATMScenario.createCreditCard()"
+ },
+ "output": [
+ "Checkpoints",
+ 232
+ ]
+ },
+ {
+ "result": {
+ "duration": 90000000,
+ "status": "ambiguous"
+ },
+ "name": "the card should be returned",
+ "keyword": "And ",
+ "line": 29,
+ "match": {
+ "location": "ATMScenario.cardShouldBeReturned()"
+ }
+ }
+ ],
+ "type": "scenario",
+ "after": [
+ {
+ "result": {
+ "duration": 64700000,
+ "status": "undefined",
+ "error_message": "Undefined step"
+ },
+ "match": {
+ "location": "any.error()"
+ },
+ "embeddings": [
+ {
+ "mime_type": "image/png",
+ "data": ""
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "id": "account-holder-withdraws-more-cash;clean-up",
+ "name": "Clean-up",
+ "keyword": "Scenario",
+ "line": 31,
+ "steps": [
+ {
+ "result": {
+ "duration": 560000,
+ "status": "passed"
+ },
+ "name": "Stream closing",
+ "keyword": "Given ",
+ "line": 32
+ }
+ ],
+ "type": "scenario"
+ },
+ {
+ "id": "undefined-result",
+ "name": "This step has no result...",
+ "keyword": "Scenario",
+ "line": 35,
+ "steps": [
+ {
+ "name": " - even it should",
+ "keyword": "Given ",
+ "line": 36
+ }
+ ],
+ "type": "scenario"
+ }
+ ],
+ "uri": "net/masterthought/example/ATMK.feature"
+ }
+]
diff --git a/testlib/src/main/resources/json/emptyAfter.json b/testlib/src/main/resources/json/emptyAfter.json
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/testlib/src/main/resources/json/emptyBefore.json b/testlib/src/main/resources/json/emptyBefore.json
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/testlib/src/main/resources/json/invalidJsonAfter.json b/testlib/src/main/resources/json/invalidJsonAfter.json
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/testlib/src/main/resources/json/invalidJsonBefore.json b/testlib/src/main/resources/json/invalidJsonBefore.json
new file mode 100644
index 0000000000..0a36b4c415
--- /dev/null
+++ b/testlib/src/main/resources/json/invalidJsonBefore.json
@@ -0,0 +1,2 @@
+{
+"a": f
diff --git a/testlib/src/main/resources/json/nestedObjectAfter.json b/testlib/src/main/resources/json/nestedObjectAfter.json
new file mode 100644
index 0000000000..070dfc481e
--- /dev/null
+++ b/testlib/src/main/resources/json/nestedObjectAfter.json
@@ -0,0 +1,11 @@
+{
+ "abc": "def",
+ "obj": {
+ "arr": [
+ 1,
+ 2,
+ 3
+ ],
+ "val": 5
+ }
+}
diff --git a/testlib/src/main/resources/json/nestedObjectBefore.json b/testlib/src/main/resources/json/nestedObjectBefore.json
new file mode 100644
index 0000000000..37a0e0b90c
--- /dev/null
+++ b/testlib/src/main/resources/json/nestedObjectBefore.json
@@ -0,0 +1,7 @@
+{ "abc": "def",
+ "obj": {
+ "arr": [
+ 1,2,3
+ ],
+ "val": 5
+ } }
diff --git a/testlib/src/main/resources/json/notJsonAfter.json b/testlib/src/main/resources/json/notJsonAfter.json
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/testlib/src/main/resources/json/notJsonBefore.json b/testlib/src/main/resources/json/notJsonBefore.json
new file mode 100644
index 0000000000..358200a574
--- /dev/null
+++ b/testlib/src/main/resources/json/notJsonBefore.json
@@ -0,0 +1 @@
+#not valid JSON
diff --git a/testlib/src/main/resources/json/objectWithNullAfter.json b/testlib/src/main/resources/json/objectWithNullAfter.json
new file mode 100644
index 0000000000..5e47a73852
--- /dev/null
+++ b/testlib/src/main/resources/json/objectWithNullAfter.json
@@ -0,0 +1,4 @@
+{
+ "another": 1,
+ "value": null
+}
diff --git a/testlib/src/main/resources/json/objectWithNullBefore.json b/testlib/src/main/resources/json/objectWithNullBefore.json
new file mode 100644
index 0000000000..f68567c97c
--- /dev/null
+++ b/testlib/src/main/resources/json/objectWithNullBefore.json
@@ -0,0 +1,4 @@
+{
+"value": null,
+"another": 1
+}
diff --git a/testlib/src/main/resources/json/singletonArrayAfter.json b/testlib/src/main/resources/json/singletonArrayAfter.json
new file mode 100644
index 0000000000..fa3d04c724
--- /dev/null
+++ b/testlib/src/main/resources/json/singletonArrayAfter.json
@@ -0,0 +1,6 @@
+[
+ 1,
+ 2,
+ 3,
+ 4
+]
diff --git a/testlib/src/main/resources/json/singletonArrayAfter0Spaces.json b/testlib/src/main/resources/json/singletonArrayAfter0Spaces.json
new file mode 100644
index 0000000000..8adb9bb604
--- /dev/null
+++ b/testlib/src/main/resources/json/singletonArrayAfter0Spaces.json
@@ -0,0 +1 @@
+[1,2,3,4]
diff --git a/testlib/src/main/resources/json/singletonArrayAfter6Spaces.json b/testlib/src/main/resources/json/singletonArrayAfter6Spaces.json
new file mode 100644
index 0000000000..2550a537b5
--- /dev/null
+++ b/testlib/src/main/resources/json/singletonArrayAfter6Spaces.json
@@ -0,0 +1,6 @@
+[
+ 1,
+ 2,
+ 3,
+ 4
+]
diff --git a/testlib/src/main/resources/json/singletonArrayAfterTabs.json b/testlib/src/main/resources/json/singletonArrayAfterTabs.json
new file mode 100644
index 0000000000..6a8580f45d
--- /dev/null
+++ b/testlib/src/main/resources/json/singletonArrayAfterTabs.json
@@ -0,0 +1,6 @@
+[
+ 1,
+ 2,
+ 3,
+ 4
+]
diff --git a/testlib/src/main/resources/json/singletonArrayBefore.json b/testlib/src/main/resources/json/singletonArrayBefore.json
new file mode 100644
index 0000000000..8290d39198
--- /dev/null
+++ b/testlib/src/main/resources/json/singletonArrayBefore.json
@@ -0,0 +1 @@
+[ 1, 2, 3, 4 ]
diff --git a/testlib/src/main/resources/json/singletonObjectAfter.json b/testlib/src/main/resources/json/singletonObjectAfter.json
new file mode 100644
index 0000000000..bbcc15e8cc
--- /dev/null
+++ b/testlib/src/main/resources/json/singletonObjectAfter.json
@@ -0,0 +1 @@
+{"a": "b"}
diff --git a/testlib/src/main/resources/json/singletonObjectBefore.json b/testlib/src/main/resources/json/singletonObjectBefore.json
new file mode 100644
index 0000000000..611670e052
--- /dev/null
+++ b/testlib/src/main/resources/json/singletonObjectBefore.json
@@ -0,0 +1 @@
+{"a": "b"}
diff --git a/testlib/src/main/resources/json/singletonObjectWithArrayAfter.json b/testlib/src/main/resources/json/singletonObjectWithArrayAfter.json
new file mode 100644
index 0000000000..39c104af94
--- /dev/null
+++ b/testlib/src/main/resources/json/singletonObjectWithArrayAfter.json
@@ -0,0 +1 @@
+{"a": [1]}
diff --git a/testlib/src/main/resources/json/singletonObjectWithArrayBefore.json b/testlib/src/main/resources/json/singletonObjectWithArrayBefore.json
new file mode 100644
index 0000000000..c427907dc0
--- /dev/null
+++ b/testlib/src/main/resources/json/singletonObjectWithArrayBefore.json
@@ -0,0 +1 @@
+{"a": [1]}
diff --git a/testlib/src/test/java/com/diffplug/spotless/json/JsonSimpleStepTest.java b/testlib/src/test/java/com/diffplug/spotless/json/JsonSimpleStepTest.java
new file mode 100644
index 0000000000..fccd563444
--- /dev/null
+++ b/testlib/src/test/java/com/diffplug/spotless/json/JsonSimpleStepTest.java
@@ -0,0 +1,131 @@
+/*
+ * Copyright 2021 DiffPlug
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.diffplug.spotless.json;
+
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+import org.junit.Test;
+
+import com.diffplug.spotless.FormatterStep;
+import com.diffplug.spotless.SerializableEqualityTester;
+import com.diffplug.spotless.StepHarness;
+import com.diffplug.spotless.TestProvisioner;
+
+public class JsonSimpleStepTest {
+
+ private static final int INDENT = 4;
+
+ private final FormatterStep step = JsonSimpleStep.create(INDENT, TestProvisioner.mavenCentral());
+ private final StepHarness stepHarness = StepHarness.forStep(step);
+
+ @Test
+ public void cannotProvidedNullProvisioner() {
+ assertThatThrownBy(() -> JsonSimpleStep.create(INDENT, null)).isInstanceOf(NullPointerException.class).hasMessage("provisioner cannot be null");
+ }
+
+ @Test
+ public void handlesSingletonObject() throws Exception {
+ doWithResource(stepHarness, "singletonObject");
+ }
+
+ @Test
+ public void handlesSingletonObjectWithArray() throws Exception {
+ doWithResource(stepHarness, "singletonObjectWithArray");
+ }
+
+ @Test
+ public void handlesNestedObject() throws Exception {
+ doWithResource(stepHarness, "nestedObject");
+ }
+
+ @Test
+ public void handlesSingletonArray() throws Exception {
+ doWithResource(stepHarness, "singletonArray");
+ }
+
+ @Test
+ public void handlesEmptyFile() throws Exception {
+ doWithResource(stepHarness, "empty");
+ }
+
+ @Test
+ public void handlesComplexNestedObject() throws Exception {
+ doWithResource(stepHarness, "cucumberJsonSample");
+ }
+
+ @Test
+ public void handlesObjectWithNull() throws Exception {
+ doWithResource(stepHarness, "objectWithNull");
+ }
+
+ @Test
+ public void handlesInvalidJson() {
+ assertThatThrownBy(() -> doWithResource(stepHarness, "invalidJson")).isInstanceOf(AssertionError.class).hasMessage("Invalid JSON file provided");
+ }
+
+ @Test
+ public void handlesNotJson() {
+ assertThatThrownBy(() -> doWithResource(stepHarness, "notJson")).isInstanceOf(AssertionError.class).hasMessage("Invalid JSON file provided");
+ }
+
+ @Test
+ public void canSetCustomIndentationLevel() throws Exception {
+ FormatterStep step = JsonSimpleStep.create(6, TestProvisioner.mavenCentral());
+ StepHarness stepHarness = StepHarness.forStep(step);
+
+ String before = "json/singletonArrayBefore.json";
+ String after = "json/singletonArrayAfter6Spaces.json";
+ stepHarness.testResource(before, after);
+ }
+
+ @Test
+ public void canSetIndentationLevelTo0() throws Exception {
+ FormatterStep step = JsonSimpleStep.create(0, TestProvisioner.mavenCentral());
+ StepHarness stepHarness = StepHarness.forStep(step);
+
+ String before = "json/singletonArrayBefore.json";
+ String after = "json/singletonArrayAfter0Spaces.json";
+ stepHarness.testResource(before, after);
+ }
+
+ @Test
+ public void equality() {
+ new SerializableEqualityTester() {
+ int spaces = 0;
+
+ @Override
+ protected void setupTest(API api) {
+ // no changes, are the same
+ api.areDifferentThan();
+
+ // with different spacing
+ spaces = 1;
+ api.areDifferentThan();
+ }
+
+ @Override
+ protected FormatterStep create() {
+ return JsonSimpleStep.create(spaces, TestProvisioner.mavenCentral());
+ }
+ }.testEquals();
+ }
+
+ private static void doWithResource(StepHarness stepHarness, String name) throws Exception {
+ String before = String.format("json/%sBefore.json", name);
+ String after = String.format("json/%sAfter.json", name);
+ stepHarness.testResource(before, after);
+ }
+}