RuleExampleSuite.java
/*******************************************************************************
* Copyright (c) 2026 Carsten Hammer.
* This program and the accompanying materials are made available under the terms
* of the Eclipse Public License 2.0: https://www.eclipse.org/legal/epl-2.0/
* SPDX-License-Identifier: EPL-2.0
*******************************************************************************/
package org.sandbox.jdt.triggerpattern.api;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonObject;
import com.google.gson.Strictness;
/**
* Versioned, readable evidence stored beside a rule as {@code .examples.json}.
* Sources are complete compilation units; no formatting or line-ending changes
* are made. Evidence is replayed, never interpreted as permission to rewrite.
*/
public record RuleExampleSuite(int schemaVersion, List<Example> examples) {
public static final int CURRENT_VERSION = 1;
private static final Gson JSON = new GsonBuilder().setPrettyPrinting()
.disableHtmlEscaping().setStrictness(Strictness.STRICT).create();
public RuleExampleSuite {
if (schemaVersion != CURRENT_VERSION) {
throw new IllegalArgumentException("Unsupported example schema version: " + schemaVersion);
}
examples = List.copyOf(Objects.requireNonNull(examples, "examples"));
Set<String> names = new HashSet<>();
for (Example example : examples) {
if (!names.add(example.name())) {
throw new IllegalArgumentException("Example names must be unique: " + example.name());
}
}
}
public record Example(String name, String unitName, String before, String after, boolean expectChange) {
public Example {
if (name == null || name.isBlank() || unitName == null || !unitName.endsWith(".java")) {
throw new IllegalArgumentException("Each example needs a name and a .java compilation-unit name");
}
if (before == null || before.isBlank() || after == null || after.isBlank()) {
throw new IllegalArgumentException("Examples require complete nonempty before and after source");
}
if (expectChange == before.equals(after)) {
throw new IllegalArgumentException("Positive examples must change; counterexamples must stay identical");
}
}
}
public String toJson() {
return JSON.toJson(this) + "\n";
}
/** Rejects missing or misspelled fields instead of silently changing expectations. */
public static RuleExampleSuite fromJson(String source) {
try {
JsonObject root = JSON.fromJson(source, JsonObject.class);
requireFields(root, Set.of("schemaVersion", "examples"));
if (!root.get("schemaVersion").isJsonPrimitive()
|| !root.getAsJsonPrimitive("schemaVersion").isNumber()
|| root.get("schemaVersion").getAsBigDecimal().intValueExact() != CURRENT_VERSION) {
throw new IllegalArgumentException("schemaVersion must be the supported JSON integer " + CURRENT_VERSION);
}
for (var element : root.getAsJsonArray("examples")) {
JsonObject example = element.getAsJsonObject();
requireFields(example, Set.of("name", "unitName", "before", "after", "expectChange"));
for (String field : List.of("name", "unitName", "before", "after")) {
if (!example.get(field).isJsonPrimitive() || !example.getAsJsonPrimitive(field).isString()) {
throw new IllegalArgumentException(field + " must be a JSON string");
}
}
if (!example.get("expectChange").isJsonPrimitive()
|| !example.getAsJsonPrimitive("expectChange").isBoolean()) {
throw new IllegalArgumentException("expectChange must be a JSON boolean");
}
}
return JSON.fromJson(root, RuleExampleSuite.class);
} catch (RuntimeException exception) {
throw new IllegalArgumentException("Invalid rule examples: " + exception.getMessage(), exception);
}
}
private static void requireFields(JsonObject object, Set<String> fields) {
if (object == null || !object.keySet().equals(fields)) {
throw new IllegalArgumentException("Required fields: " + fields);
}
}
}