diff --git a/CHANGELOG.md b/CHANGELOG.md
index 40177b4..f0e5dc3 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -14,6 +14,7 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
- Add check on GitHub commit SHAs [#223](https://github.com/nbbrd/heylogs/issues/223)
- Improve list command output [#231](https://github.com/nbbrd/heylogs/issues/231)
- Add error severity to failures [#17](https://github.com/nbbrd/heylogs/issues/17)
+- Add json formatting [#118](https://github.com/nbbrd/heylogs/issues/118)
### Changed
diff --git a/heylogs-api/pom.xml b/heylogs-api/pom.xml
index 7198cb1..49a15b3 100644
--- a/heylogs-api/pom.xml
+++ b/heylogs-api/pom.xml
@@ -84,6 +84,11 @@
com.github.nbbrd.java-io-util
java-io-http
+
+ com.google.code.gson
+ gson
+ 2.10.1
+
diff --git a/heylogs-api/src/main/java/internal/heylogs/JsonFormat.java b/heylogs-api/src/main/java/internal/heylogs/JsonFormat.java
new file mode 100644
index 0000000..9bd3712
--- /dev/null
+++ b/heylogs-api/src/main/java/internal/heylogs/JsonFormat.java
@@ -0,0 +1,130 @@
+package internal.heylogs;
+
+import com.google.gson.*;
+import lombok.NonNull;
+import nbbrd.design.MightBeGenerated;
+import nbbrd.heylogs.Problem;
+import nbbrd.heylogs.Resource;
+import nbbrd.heylogs.Status;
+import nbbrd.heylogs.TimeRange;
+import nbbrd.heylogs.spi.Format;
+import nbbrd.heylogs.spi.FormatType;
+import nbbrd.heylogs.spi.RuleIssue;
+import nbbrd.heylogs.spi.RuleSeverity;
+import nbbrd.service.ServiceProvider;
+
+import java.io.IOException;
+import java.lang.reflect.Type;
+import java.time.LocalDate;
+import java.util.EnumSet;
+import java.util.List;
+import java.util.Set;
+
+@ServiceProvider
+public final class JsonFormat implements Format {
+
+ public static final String ID = "json";
+
+ @Override
+ public @NonNull String getFormatId() {
+ return ID;
+ }
+
+ @Override
+ public @NonNull String getFormatName() {
+ return "Outputs JSON-serialized results.";
+ }
+
+ @Override
+ public @NonNull Set getSupportedFormatTypes() {
+ return EnumSet.allOf(FormatType.class);
+ }
+
+ @Override
+ public void formatProblems(@NonNull Appendable appendable, @NonNull String source, @NonNull List problems) throws IOException {
+ try {
+ GSON.toJson(new FileProblems(source, problems), appendable);
+ } catch (JsonIOException ex) {
+ throw new IOException(ex);
+ }
+ }
+
+ @Override
+ public void formatStatus(@NonNull Appendable appendable, @NonNull String source, @NonNull Status status) throws IOException {
+ try {
+ GSON.toJson(new FileStatus(source, status), appendable);
+ } catch (JsonIOException ex) {
+ throw new IOException(ex);
+ }
+ }
+
+ @Override
+ public void formatResources(@NonNull Appendable appendable, @NonNull List resources) throws IOException {
+ try {
+ GSON.toJson(resources, appendable);
+ } catch (JsonIOException ex) {
+ throw new IOException(ex);
+ }
+ }
+
+ @lombok.Value
+ private static class FileProblems {
+ String filePath;
+ List messages;
+ }
+
+ @lombok.Value
+ private static class FileStatus {
+ String filePath;
+ Status status;
+ }
+
+ private static final Gson GSON = new GsonBuilder()
+ .registerTypeAdapter(Problem.class, (JsonSerializer) JsonFormat::serializeProblem)
+ .registerTypeAdapter(Problem.class, (JsonDeserializer) JsonFormat::deserializeProblem)
+ .registerTypeAdapter(TimeRange.class, (JsonSerializer) JsonFormat::serializeTimeRange)
+ .registerTypeAdapter(TimeRange.class, (JsonDeserializer) JsonFormat::deserializeTimeRange)
+ .setPrettyPrinting()
+ .create();
+
+ @MightBeGenerated
+ private static JsonElement serializeProblem(Problem src, Type typeOfSrc, JsonSerializationContext context) {
+ JsonObject result = new JsonObject();
+ result.addProperty("ruleId", src.getId());
+ result.addProperty("severity", src.getSeverity().toCode());
+ result.addProperty("message", src.getIssue().getMessage());
+ result.addProperty("line", src.getIssue().getLine());
+ result.addProperty("column", src.getIssue().getColumn());
+ return result;
+ }
+
+ @MightBeGenerated
+ private static Problem deserializeProblem(JsonElement json, Type typeOfT, JsonDeserializationContext context) {
+ JsonObject x = json.getAsJsonObject();
+ return Problem
+ .builder()
+ .id(x.get("ruleId").getAsString())
+ .severity(RuleSeverity.parseCode(x.get("severity").getAsInt()))
+ .issue(RuleIssue
+ .builder()
+ .message(x.get("message").getAsString())
+ .line(x.get("line").getAsInt())
+ .column(x.get("column").getAsInt())
+ .build())
+ .build();
+ }
+
+ @MightBeGenerated
+ private static JsonElement serializeTimeRange(TimeRange src, Type typeOfSrc, JsonSerializationContext context) {
+ JsonObject result = new JsonObject();
+ result.addProperty("from", src.getFrom().toString());
+ result.addProperty("to", src.getTo().toString());
+ return result;
+ }
+
+ @MightBeGenerated
+ private static TimeRange deserializeTimeRange(JsonElement json, Type typeOfT, JsonDeserializationContext context) {
+ JsonObject x = json.getAsJsonObject();
+ return TimeRange.of(LocalDate.parse(x.get("from").getAsString()), LocalDate.parse(x.get("to").getAsString()));
+ }
+}
diff --git a/heylogs-api/src/main/java/nbbrd/heylogs/spi/RuleSeverity.java b/heylogs-api/src/main/java/nbbrd/heylogs/spi/RuleSeverity.java
index 287f2a8..4feec65 100644
--- a/heylogs-api/src/main/java/nbbrd/heylogs/spi/RuleSeverity.java
+++ b/heylogs-api/src/main/java/nbbrd/heylogs/spi/RuleSeverity.java
@@ -1,5 +1,14 @@
package nbbrd.heylogs.spi;
+import lombok.NonNull;
+import nbbrd.design.RepresentableAsInt;
+import nbbrd.design.RepresentableAsString;
+import nbbrd.design.StaticFactoryMethod;
+
+import java.util.stream.Stream;
+
+//@RepresentableAsString
+@RepresentableAsInt(formatMethodName = "toCode", parseMethodName = "parseCode")
@lombok.AllArgsConstructor
public enum RuleSeverity {
@@ -18,6 +27,16 @@ public enum RuleSeverity {
*/
ERROR(2);
- @lombok.Getter
private final int code;
+
+ public int toCode() {
+ return code;
+ }
+
+ @StaticFactoryMethod
+ public static @NonNull RuleSeverity parseCode(int code) {
+ return Stream.of(values()).filter(value -> value.code == code)
+ .findFirst()
+ .orElseThrow(IllegalArgumentException::new);
+ }
}
diff --git a/heylogs-api/src/test/java/nbbrd/heylogs/HeylogsTest.java b/heylogs-api/src/test/java/nbbrd/heylogs/HeylogsTest.java
index 2b63d2f..c80cc05 100644
--- a/heylogs-api/src/test/java/nbbrd/heylogs/HeylogsTest.java
+++ b/heylogs-api/src/test/java/nbbrd/heylogs/HeylogsTest.java
@@ -1,6 +1,7 @@
package nbbrd.heylogs;
import internal.heylogs.SemverRule;
+import internal.heylogs.StylishFormat;
import nbbrd.heylogs.spi.Rule;
import nbbrd.heylogs.spi.RuleIssue;
import org.junit.jupiter.api.Test;
@@ -56,7 +57,7 @@ public void testFormatProblems() throws IOException {
.isThrownBy(() -> Heylogs.ofServiceLoader().formatProblems("other", new StringBuilder(), "", emptyList()));
StringBuilder output = new StringBuilder();
- Heylogs.ofServiceLoader().formatProblems(FIRST_FORMAT_AVAILABLE, output, "file1", asList(Problem.builder().id("rule1").severity(ERROR).issue(RuleIssue.builder().message("some message").line(10).column(20).build()).build()));
+ Heylogs.ofServiceLoader().formatProblems(StylishFormat.ID, output, "file1", asList(Problem.builder().id("rule1").severity(ERROR).issue(RuleIssue.builder().message("some message").line(10).column(20).build()).build()));
assertThat(output.toString())
.isEqualToIgnoringNewLines(
"file1\n" +
@@ -112,7 +113,7 @@ public void testFormatStatus() throws IOException {
.isThrownBy(() -> Heylogs.ofServiceLoader().formatStatus("other", new StringBuilder(), "", Status.builder().build()));
StringBuilder output = new StringBuilder();
- Heylogs.ofServiceLoader().formatStatus(FIRST_FORMAT_AVAILABLE, output, "file1", new Status(
+ Heylogs.ofServiceLoader().formatStatus(StylishFormat.ID, output, "file1", new Status(
1,
TimeRange.of(LocalDate.of(2019, 2, 15), LocalDate.of(2019, 2, 15)),
true, " ()",