From 2d3fe35c019a68eea699f85f61472d092ff8cff5 Mon Sep 17 00:00:00 2001 From: david Date: Wed, 15 Jul 2026 19:11:01 +0200 Subject: [PATCH 1/6] Merge metric type implementations into SimpleMetric --- .../java/dev/faststats/data/ArrayMetric.java | 27 ------ .../java/dev/faststats/data/MapMetric.java | 28 ------ .../main/java/dev/faststats/data/Metric.java | 40 ++++----- .../java/dev/faststats/data/SimpleMetric.java | 90 ++++++++++++++++++- .../dev/faststats/data/SingleValueMetric.java | 23 ----- 5 files changed, 107 insertions(+), 101 deletions(-) delete mode 100644 core/src/main/java/dev/faststats/data/ArrayMetric.java delete mode 100644 core/src/main/java/dev/faststats/data/MapMetric.java delete mode 100644 core/src/main/java/dev/faststats/data/SingleValueMetric.java diff --git a/core/src/main/java/dev/faststats/data/ArrayMetric.java b/core/src/main/java/dev/faststats/data/ArrayMetric.java deleted file mode 100644 index 00b55b23..00000000 --- a/core/src/main/java/dev/faststats/data/ArrayMetric.java +++ /dev/null @@ -1,27 +0,0 @@ -package dev.faststats.data; - -import com.google.gson.JsonArray; -import com.google.gson.JsonElement; -import org.jspecify.annotations.Nullable; - -import java.util.Optional; -import java.util.concurrent.Callable; - -final class ArrayMetric extends SimpleMetric { - public ArrayMetric(@SourceId final String id, final Callable callable) throws IllegalArgumentException { - super(id, callable); - } - - @Override - public Optional getData() throws Exception { - return compute().map(data -> { - final var elements = new JsonArray(data.length); - for (final var d : data) { - if (d instanceof final Boolean b) elements.add(b); - else if (d instanceof final Number n) elements.add(n); - else elements.add(d.toString()); - } - return elements; - }); - } -} diff --git a/core/src/main/java/dev/faststats/data/MapMetric.java b/core/src/main/java/dev/faststats/data/MapMetric.java deleted file mode 100644 index d95943d6..00000000 --- a/core/src/main/java/dev/faststats/data/MapMetric.java +++ /dev/null @@ -1,28 +0,0 @@ -package dev.faststats.data; - -import com.google.gson.JsonElement; -import com.google.gson.JsonObject; -import org.jspecify.annotations.Nullable; - -import java.util.Map; -import java.util.Optional; -import java.util.concurrent.Callable; - -final class MapMetric extends SimpleMetric> { - public MapMetric(@SourceId final String id, final Callable> callable) throws IllegalArgumentException { - super(id, callable); - } - - @Override - public Optional getData() throws Exception { - return compute().map(data -> { - final var object = new JsonObject(); - data.forEach((key, value) -> { - if (value instanceof final Boolean bool) object.addProperty(key, bool); - else if (value instanceof final Number number) object.addProperty(key, number); - else object.addProperty(key, value.toString()); - }); - return object; - }); - } -} diff --git a/core/src/main/java/dev/faststats/data/Metric.java b/core/src/main/java/dev/faststats/data/Metric.java index 3f59ed40..914e7278 100644 --- a/core/src/main/java/dev/faststats/data/Metric.java +++ b/core/src/main/java/dev/faststats/data/Metric.java @@ -41,9 +41,7 @@ public interface Metric { * * @return an optional containing the metric data as {@link JsonElement} * @throws Exception if unable to get the metric data - * @implSpec The implementation must call {@link #compute()} to get the metric data - * and follow the same thread-safety and pureness requirements. - * @see #compute() + * @implSpec The implementation must be thread-safe and pure (i.e. not modify any shared state). * @since 0.24.0 */ @Contract(pure = true) @@ -57,12 +55,12 @@ public interface Metric { * @return the string array metric * @throws IllegalArgumentException if the source id is invalid * @apiNote The callable must be thread-safe and pure (i.e. not modify any shared state). - * @see #compute() + * @see #getData() * @since 0.24.0 */ @Contract(value = "_, _ -> new", pure = true) static Metric stringArray(@SourceId final String id, final Callable callable) throws IllegalArgumentException { - return new ArrayMetric<>(id, callable); + return new SimpleMetric.Array<>(id, callable); } /** @@ -73,12 +71,12 @@ static Metric stringArray(@SourceId final String id, final Callable booleanArray(@SourceId final String id, final Callable callable) throws IllegalArgumentException { - return new ArrayMetric<>(id, callable); + return new SimpleMetric.Array<>(id, callable); } /** @@ -89,12 +87,12 @@ static Metric booleanArray(@SourceId final String id, final Callable< * @return the number array metric * @throws IllegalArgumentException if the source id is invalid * @apiNote The callable must be thread-safe and pure (i.e. not modify any shared state). - * @see #compute() + * @see #getData() * @since 0.24.0 */ @Contract(value = "_, _ -> new", pure = true) static Metric numberArray(@SourceId final String id, final Callable callable) throws IllegalArgumentException { - return new ArrayMetric<>(id, callable); + return new SimpleMetric.Array<>(id, callable); } /** @@ -105,12 +103,12 @@ static Metric numberArray(@SourceId final String id, final Callable> stringMap(@SourceId final String id, final Callable> callable) throws IllegalArgumentException { - return new MapMetric<>(id, callable); + return new SimpleMetric.Map<>(id, callable); } /** @@ -121,12 +119,12 @@ static Metric numberArray(@SourceId final String id, final Callable> booleanMap(@SourceId final String id, final Callable> callable) throws IllegalArgumentException { - return new MapMetric<>(id, callable); + return new SimpleMetric.Map<>(id, callable); } /** @@ -137,12 +135,12 @@ static Metric numberArray(@SourceId final String id, final Callable> numberMap(@SourceId final String id, final Callable> callable) throws IllegalArgumentException { - return new MapMetric<>(id, callable); + return new SimpleMetric.Map<>(id, callable); } /** @@ -153,12 +151,12 @@ static Metric numberArray(@SourceId final String id, final Callable bool(@SourceId final String id, final Callable<@Nullable Boolean> callable) throws IllegalArgumentException { - return new SingleValueMetric<>(id, callable); + return new SimpleMetric.Primitive<>(id, callable); } /** @@ -169,12 +167,12 @@ static Metric bool(@SourceId final String id, final Callable<@Nullable * @return the string metric * @throws IllegalArgumentException if the source id is invalid * @apiNote The callable must be thread-safe and pure (i.e. not modify any shared state). - * @see #compute() + * @see #getData() * @since 0.24.0 */ @Contract(value = "_, _ -> new", pure = true) static Metric string(@SourceId final String id, final Callable<@Nullable String> callable) throws IllegalArgumentException { - return new SingleValueMetric<>(id, callable); + return new SimpleMetric.Primitive<>(id, callable); } /** @@ -185,11 +183,11 @@ static Metric string(@SourceId final String id, final Callable<@Nullable * @return the number metric * @throws IllegalArgumentException if the source id is invalid * @apiNote The callable must be thread-safe and pure (i.e. not modify any shared state). - * @see #compute() + * @see #getData() * @since 0.24.0 */ @Contract(value = "_, _ -> new", pure = true) static Metric number(@SourceId final String id, final Callable<@Nullable Number> callable) throws IllegalArgumentException { - return new SingleValueMetric<>(id, callable); + return new SimpleMetric.Primitive<>(id, callable); } } diff --git a/core/src/main/java/dev/faststats/data/SimpleMetric.java b/core/src/main/java/dev/faststats/data/SimpleMetric.java index 28082ade..d817bf82 100644 --- a/core/src/main/java/dev/faststats/data/SimpleMetric.java +++ b/core/src/main/java/dev/faststats/data/SimpleMetric.java @@ -1,5 +1,9 @@ package dev.faststats.data; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonPrimitive; import org.jspecify.annotations.Nullable; import java.util.Objects; @@ -8,9 +12,9 @@ abstract class SimpleMetric implements Metric { private final @SourceId String id; - private final Callable callable; + protected final Callable callable; - public SimpleMetric(@SourceId final String id, final Callable callable) throws IllegalArgumentException { + private SimpleMetric(@SourceId final String id, final Callable callable) throws IllegalArgumentException { if (!id.matches(SourceId.PATTERN)) { throw new IllegalArgumentException("Invalid source id '" + id + "', must match '" + SourceId.PATTERN + "'"); } @@ -23,6 +27,7 @@ public SimpleMetric(@SourceId final String id, final Callable compute() throws Exception { return Optional.ofNullable(callable.call()); } @@ -45,4 +50,85 @@ public String toString() { "id='" + id + '\'' + '}'; } + + static final class Json extends SimpleMetric { + public Json( + @SourceId final String id, + final Callable callable + ) throws IllegalArgumentException { + super(id, callable); + } + + @Override + public Optional getData() throws Exception { + return Optional.ofNullable(callable.call()); + } + } + + static final class Array extends SimpleMetric { + public Array( + @SourceId final String id, + final Callable callable + ) throws IllegalArgumentException { + super(id, callable); + } + + @Override + public Optional getData() throws Exception { + final var data = callable.call(); + if (data == null) return Optional.empty(); + + final var elements = new JsonArray(data.length); + for (final var d : data) { + if (d instanceof final Boolean b) elements.add(b); + else if (d instanceof final Number n) elements.add(n); + else elements.add(d.toString()); + } + return Optional.of(elements); + } + } + + static final class Map extends SimpleMetric> { + public Map( + @SourceId final String id, + final Callable> callable + ) throws IllegalArgumentException { + super(id, callable); + } + + @Override + public Optional getData() throws Exception { + final var data = callable.call(); + if (data == null) return Optional.empty(); + + final var object = new JsonObject(); + data.forEach((key, value) -> { + if (value instanceof final Boolean bool) object.addProperty(key, bool); + else if (value instanceof final Number number) object.addProperty(key, number); + else object.addProperty(key, value.toString()); + }); + return Optional.of(object); + } + } + + static final class Primitive extends SimpleMetric { + public Primitive( + @SourceId final String id, + final Callable callable + ) throws IllegalArgumentException { + super(id, callable); + } + + @Override + public Optional getData() throws Exception { + final var data = callable.call(); + if (data == null) return Optional.empty(); + + if (data instanceof final Boolean bool) + return Optional.of(new JsonPrimitive(bool)); + if (data instanceof final Number number) + return Optional.of(new JsonPrimitive(number)); + return Optional.of(new JsonPrimitive(data.toString())); + } + } } diff --git a/core/src/main/java/dev/faststats/data/SingleValueMetric.java b/core/src/main/java/dev/faststats/data/SingleValueMetric.java deleted file mode 100644 index 26034748..00000000 --- a/core/src/main/java/dev/faststats/data/SingleValueMetric.java +++ /dev/null @@ -1,23 +0,0 @@ -package dev.faststats.data; - -import com.google.gson.JsonElement; -import com.google.gson.JsonPrimitive; -import org.jspecify.annotations.Nullable; - -import java.util.Optional; -import java.util.concurrent.Callable; - -final class SingleValueMetric extends SimpleMetric { - public SingleValueMetric(@SourceId final String id, final Callable callable) throws IllegalArgumentException { - super(id, callable); - } - - @Override - public Optional getData() throws Exception { - return compute().map(data -> { - if (data instanceof final Boolean bool) return new JsonPrimitive(bool); - if (data instanceof final Number number) return new JsonPrimitive(number); - return new JsonPrimitive(data.toString()); - }); - } -} From 0216f0bfc9476ed55dc881d8ce97f78c80cd7e91 Mon Sep 17 00:00:00 2001 From: david Date: Wed, 15 Jul 2026 19:11:41 +0200 Subject: [PATCH 2/6] Mark `Metric#getData` as override only --- core/src/main/java/dev/faststats/data/Metric.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/core/src/main/java/dev/faststats/data/Metric.java b/core/src/main/java/dev/faststats/data/Metric.java index 914e7278..65b6577a 100644 --- a/core/src/main/java/dev/faststats/data/Metric.java +++ b/core/src/main/java/dev/faststats/data/Metric.java @@ -1,6 +1,7 @@ package dev.faststats.data; import com.google.gson.JsonElement; +import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.Contract; import org.jspecify.annotations.Nullable; @@ -45,6 +46,7 @@ public interface Metric { * @since 0.24.0 */ @Contract(pure = true) + @ApiStatus.OverrideOnly Optional getData() throws Exception; /** From 28b81d558602396e5a324eeb1d9aac6e5d2a560f Mon Sep 17 00:00:00 2001 From: david Date: Wed, 15 Jul 2026 19:12:36 +0200 Subject: [PATCH 3/6] Terminally deprecate `Metric#compute` in favor of `#getData` --- core/src/main/java/dev/faststats/data/Metric.java | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/core/src/main/java/dev/faststats/data/Metric.java b/core/src/main/java/dev/faststats/data/Metric.java index 65b6577a..ded0b237 100644 --- a/core/src/main/java/dev/faststats/data/Metric.java +++ b/core/src/main/java/dev/faststats/data/Metric.java @@ -1,6 +1,8 @@ package dev.faststats.data; import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonPrimitive; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.Contract; import org.jspecify.annotations.Nullable; @@ -32,10 +34,16 @@ public interface Metric { * @return an optional containing the metric data * @throws Exception if unable to compute the metric data * @implSpec The implementation must be thread-safe and pure (i.e. not modify any shared state). + * @see #getData() * @since 0.24.0 + * @deprecated This method only adds unnecessary mental overhead. Use {@link #getData()} instead. */ @Contract(pure = true) - Optional compute() throws Exception; + @ApiStatus.OverrideOnly + @Deprecated(since = "0.28.0", forRemoval = true) + default Optional compute() throws Exception { + return Optional.empty(); + } /** * Get the metric data as a JSON element. From 798106aa0f38ba7b9e56f0eb19e536d9b79da96f Mon Sep 17 00:00:00 2001 From: david Date: Wed, 15 Jul 2026 19:13:06 +0200 Subject: [PATCH 4/6] Add JSON metric types --- .../faststats/example/MetricTypesExample.java | 16 ++++++ .../main/java/dev/faststats/data/Metric.java | 49 +++++++++++++++++++ 2 files changed, 65 insertions(+) diff --git a/core/example/src/main/java/dev/faststats/example/MetricTypesExample.java b/core/example/src/main/java/dev/faststats/example/MetricTypesExample.java index 5d7ac3e1..7fdd4347 100644 --- a/core/example/src/main/java/dev/faststats/example/MetricTypesExample.java +++ b/core/example/src/main/java/dev/faststats/example/MetricTypesExample.java @@ -1,5 +1,8 @@ package dev.faststats.example; +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; +import com.google.gson.JsonPrimitive; import dev.faststats.data.Metric; public final class MetricTypesExample { @@ -11,4 +14,17 @@ public final class MetricTypesExample { // Array metrics public static final Metric INSTALLED_PLUGINS = Metric.stringArray("installed_plugins", () -> new String[]{"WorldEdit", "Essentials"}); public static final Metric WORLDS = Metric.stringArray("worlds", () -> new String[]{"city", "farmworld", "farmworld_nether", "famrworld_end"}); + + // Json metrics + public static final Metric JSON_OBJECT = Metric.object("nested_example", () -> { + var object = new JsonObject(); + object.addProperty("scope", "example"); + return object; + }); + public static final Metric JSON_ARRAY = Metric.array("examples", () -> { + var array = new JsonArray(); + array.add("example"); + return array; + }); + public static final Metric JSON_NUMBER = Metric.primitive("example", () -> new JsonPrimitive("example")); } diff --git a/core/src/main/java/dev/faststats/data/Metric.java b/core/src/main/java/dev/faststats/data/Metric.java index ded0b237..142e2cc5 100644 --- a/core/src/main/java/dev/faststats/data/Metric.java +++ b/core/src/main/java/dev/faststats/data/Metric.java @@ -1,5 +1,6 @@ package dev.faststats.data; +import com.google.gson.JsonArray; import com.google.gson.JsonElement; import com.google.gson.JsonObject; import com.google.gson.JsonPrimitive; @@ -57,6 +58,54 @@ default Optional compute() throws Exception { @ApiStatus.OverrideOnly Optional getData() throws Exception; + /** + * Create a JsonObject metric. + * + * @param id the source id + * @param callable the metric data callable + * @return the JsonObject metric + * @throws IllegalArgumentException if the source id is invalid + * @apiNote The callable must be thread-safe and pure (i.e. not modify any shared state). + * @see #getData() + * @since 0.28.0 + */ + @Contract(value = "_, _ -> new", pure = true) + static Metric object(@SourceId final String id, final Callable<@Nullable JsonObject> callable) throws IllegalArgumentException { + return new SimpleMetric.Json<>(id, callable); + } + + /** + * Create a JsonArray metric. + * + * @param id the source id + * @param callable the metric data callable + * @return the JsonArray metric + * @throws IllegalArgumentException if the source id is invalid + * @apiNote The callable must be thread-safe and pure (i.e. not modify any shared state). + * @see #getData() + * @since 0.28.0 + */ + @Contract(value = "_, _ -> new", pure = true) + static Metric array(@SourceId final String id, final Callable<@Nullable JsonArray> callable) throws IllegalArgumentException { + return new SimpleMetric.Json<>(id, callable); + } + + /** + * Create a JsonPromitive metric. + * + * @param id the source id + * @param callable the metric data callable + * @return the JsonPrimitive metric + * @throws IllegalArgumentException if the source id is invalid + * @apiNote The callable must be thread-safe and pure (i.e. not modify any shared state). + * @see #getData() + * @since 0.28.0 + */ + @Contract(value = "_, _ -> new", pure = true) + static Metric primitive(@SourceId final String id, final Callable<@Nullable JsonPrimitive> callable) throws IllegalArgumentException { + return new SimpleMetric.Json<>(id, callable); + } + /** * Create a string array metric. * From 79a39911a1631ea06ef8e62a3cab023d900425f1 Mon Sep 17 00:00:00 2001 From: david Date: Wed, 15 Jul 2026 19:13:34 +0200 Subject: [PATCH 5/6] Warn about illegal null entries in metrics --- core/src/main/java/dev/faststats/SimpleMetrics.java | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/core/src/main/java/dev/faststats/SimpleMetrics.java b/core/src/main/java/dev/faststats/SimpleMetrics.java index cdd885a6..cb8fbd63 100644 --- a/core/src/main/java/dev/faststats/SimpleMetrics.java +++ b/core/src/main/java/dev/faststats/SimpleMetrics.java @@ -75,9 +75,12 @@ private void appendInternalData(final JsonObject metrics) { private void appendCustomData(final JsonObject metrics) { this.metrics.forEach(metric -> { try { - if (metrics.has(metric.getId())) + if (metrics.has(metric.getId())) { logger.warn("Skipped duplicated metrics entry: %s", metric.getId()); - else metric.getData().ifPresent(element -> metrics.add(metric.getId(), element)); + } else metric.getData().ifPresent(element -> { + if (!element.isJsonNull()) metrics.add(metric.getId(), element); + else logger.warn("Ignored illegal null entry in metric: %s", metric.getId()); + }); } catch (final Throwable t) { logger.error("Failed to append custom metric data: %s", t, metric.getId()); context.errorTrackerService().ifPresent(service -> service.globalErrorTracker().trackError(t)); From 9e5d65c08325e449f7bc9f85f5607d3336ea286f Mon Sep 17 00:00:00 2001 From: david Date: Wed, 15 Jul 2026 19:14:02 +0200 Subject: [PATCH 6/6] Bump version to 0.28.0 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index e8d924ca..cfa1583d 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,2 +1,2 @@ -version=0.27.2 +version=0.28.0 org.gradle.jvmargs=-Xmx2g -XX:MaxMetaspaceSize=512m