Skip to content

Commit 0e62942

Browse files
committed
Enable field-level diagnostics in migration CLI and expose in reports
Add support for detailed field-level diagnostics during migrations. Extend CLI command options, enhance diagnostic report generation (text/JSON formats), and integrate diagnostics into the actuator endpoint. Signed-off-by: Erik Pförtner <splatcrafter@splatgames.de>
1 parent 8603d0f commit 0e62942

16 files changed

Lines changed: 1427 additions & 32 deletions

File tree

aether-datafixers-cli/src/main/java/de/splatgames/aether/datafixers/cli/command/MigrateCommand.java

Lines changed: 89 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,9 @@
2828
import de.splatgames.aether.datafixers.api.bootstrap.DataFixerBootstrap;
2929
import de.splatgames.aether.datafixers.api.dynamic.Dynamic;
3030
import de.splatgames.aether.datafixers.api.dynamic.TaggedDynamic;
31+
import de.splatgames.aether.datafixers.api.diagnostic.DiagnosticContext;
32+
import de.splatgames.aether.datafixers.api.diagnostic.DiagnosticOptions;
33+
import de.splatgames.aether.datafixers.api.diagnostic.MigrationReport;
3134
import de.splatgames.aether.datafixers.cli.bootstrap.BootstrapLoader;
3235
import de.splatgames.aether.datafixers.cli.format.FormatHandler;
3336
import de.splatgames.aether.datafixers.cli.format.FormatRegistry;
@@ -36,6 +39,7 @@
3639
import de.splatgames.aether.datafixers.core.AetherDataFixer;
3740
import de.splatgames.aether.datafixers.core.bootstrap.DataFixerRuntimeFactory;
3841
import org.jetbrains.annotations.NotNull;
42+
import org.jetbrains.annotations.Nullable;
3943
import picocli.CommandLine.Command;
4044
import picocli.CommandLine.Option;
4145
import picocli.CommandLine.Parameters;
@@ -396,6 +400,32 @@ public class MigrateCommand implements Callable<Integer> {
396400
)
397401
private boolean verbose;
398402

403+
/**
404+
* Whether to enable field-level diagnostics output.
405+
*
406+
* <p>When {@code true}, a detailed diagnostic report is generated for each
407+
* migrated file, including information about every fix execution, rule
408+
* application, and field-level operation (renames, removals, additions,
409+
* transforms, etc.).</p>
410+
*
411+
* <p>Diagnostics output is written alongside the migration report: to
412+
* {@link #reportFile} if specified, or to stderr otherwise. The output
413+
* format follows the selected {@link #reportFormat}.</p>
414+
*
415+
* <p>Default value: {@code false}</p>
416+
*
417+
* <p>CLI usage: {@code --diagnostics}</p>
418+
*
419+
* @see DiagnosticContext
420+
* @see de.splatgames.aether.datafixers.api.diagnostic.FieldOperation
421+
* @since 1.0.0
422+
*/
423+
@Option(
424+
names = {"--diagnostics"},
425+
description = "Enable field-level diagnostics output showing detailed fix and field operation information."
426+
)
427+
private boolean diagnostics;
428+
399429
/**
400430
* Whether to pretty-print the output JSON.
401431
*
@@ -474,6 +504,8 @@ public Integer call() {
474504
int errorCount = 0;
475505
final StringBuilder reportBuilder = new StringBuilder();
476506

507+
final StringBuilder diagnosticBuilder = new StringBuilder();
508+
477509
for (final File inputFile : this.inputFiles) {
478510
try {
479511
final MigrationResult result = processFile(
@@ -483,6 +515,9 @@ public Integer call() {
483515
if (this.generateReport) {
484516
reportBuilder.append(result.report).append("\n");
485517
}
518+
if (this.diagnostics && !result.diagnosticOutput.isEmpty()) {
519+
diagnosticBuilder.append(result.diagnosticOutput).append("\n");
520+
}
486521
} catch (final Exception e) {
487522
errorCount++;
488523
System.err.println("Error processing " + inputFile + ": " + e.getMessage());
@@ -505,6 +540,19 @@ public Integer call() {
505540
}
506541
}
507542

543+
// Write diagnostic output
544+
if (this.diagnostics && !diagnosticBuilder.isEmpty()) {
545+
final String diagnosticContent = diagnosticBuilder.toString();
546+
if (this.reportFile != null) {
547+
Files.writeString(this.reportFile.toPath(), diagnosticContent,
548+
StandardCharsets.UTF_8,
549+
java.nio.file.StandardOpenOption.CREATE,
550+
java.nio.file.StandardOpenOption.APPEND);
551+
} else {
552+
System.err.println(diagnosticContent);
553+
}
554+
}
555+
508556
// Summary
509557
if (this.inputFiles.size() > 1 || this.verbose) {
510558
System.err.println("Completed: " + successCount + " migrated, " + errorCount + " errors");
@@ -590,15 +638,24 @@ private <T> MigrationResult processFile(
590638
System.err.println("Skipping " + inputFile + " (already at v"
591639
+ sourceVersion.getVersion() + ")");
592640
}
593-
return new MigrationResult("", Duration.ZERO);
641+
return new MigrationResult("", Duration.ZERO, "");
594642
}
595643

596644
// Create dynamic and migrate
597645
final Dynamic<T> dynamic = new Dynamic<>(handler.ops(), data);
598646
final TaggedDynamic tagged = new TaggedDynamic(typeRef, dynamic);
599647

600-
// Perform migration
601-
final TaggedDynamic migrated = fixer.update(tagged, sourceVersion, targetVersion);
648+
// Perform migration (with diagnostics if enabled)
649+
final DiagnosticContext diagCtx = this.diagnostics
650+
? DiagnosticContext.create(DiagnosticOptions.builder()
651+
.captureSnapshots(false)
652+
.captureRuleDetails(true)
653+
.captureFieldDetails(true)
654+
.build())
655+
: null;
656+
final TaggedDynamic migrated = diagCtx != null
657+
? fixer.update(tagged, sourceVersion, targetVersion, diagCtx)
658+
: fixer.update(tagged, sourceVersion, targetVersion);
602659

603660
// Extract result
604661
@SuppressWarnings("unchecked")
@@ -631,7 +688,19 @@ private <T> MigrationResult processFile(
631688
);
632689
}
633690

634-
return new MigrationResult(report, duration);
691+
// Generate diagnostic report
692+
final MigrationReport diagnosticReport = diagCtx != null ? diagCtx.getReport() : null;
693+
String diagnosticOutput = "";
694+
if (diagnosticReport != null) {
695+
final ReportFormatter formatter = ReportFormatter.forFormat(this.reportFormat);
696+
diagnosticOutput = formatter.formatDiagnostic(
697+
inputFile.getName(),
698+
typeRef.getId(),
699+
diagnosticReport
700+
);
701+
}
702+
703+
return new MigrationResult(report, duration, diagnosticOutput);
635704
}
636705

637706
/**
@@ -719,6 +788,21 @@ private void writeOutput(@NotNull final File inputFile, @NotNull final String co
719788
* @see #processFile(File, AetherDataFixer, FormatHandler, TypeReference, DataVersion)
720789
* @see ReportFormatter
721790
*/
722-
private record MigrationResult(String report, Duration duration) {
791+
/**
792+
* Holds the result of a single file migration operation.
793+
*
794+
* <p>This record captures the outcome of migrating one file, including
795+
* the formatted report string (if reporting is enabled), the time
796+
* taken for the migration, and the optional diagnostic output (if
797+
* {@code --diagnostics} was enabled).</p>
798+
*
799+
* @param report the formatted migration report string, empty if reporting is disabled
800+
* or if the file was skipped (already at target version)
801+
* @param duration the time elapsed during the migration process, including file I/O
802+
* @param diagnosticOutput the formatted diagnostic report string, empty if diagnostics are disabled
803+
* @see #processFile(File, AetherDataFixer, FormatHandler, TypeReference, DataVersion)
804+
* @see ReportFormatter
805+
*/
806+
private record MigrationResult(String report, Duration duration, String diagnosticOutput) {
723807
}
724808
}

aether-datafixers-cli/src/main/java/de/splatgames/aether/datafixers/cli/report/JsonReportFormatter.java

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,12 @@
2525
import com.google.common.base.Preconditions;
2626
import com.google.gson.Gson;
2727
import com.google.gson.GsonBuilder;
28+
import com.google.gson.JsonArray;
2829
import com.google.gson.JsonObject;
30+
import de.splatgames.aether.datafixers.api.diagnostic.FieldOperation;
31+
import de.splatgames.aether.datafixers.api.diagnostic.FixExecution;
32+
import de.splatgames.aether.datafixers.api.diagnostic.MigrationReport;
33+
import de.splatgames.aether.datafixers.api.diagnostic.RuleApplication;
2934
import org.jetbrains.annotations.NotNull;
3035

3136
import java.time.Duration;
@@ -122,4 +127,113 @@ public String formatSimple(
122127

123128
return GSON.toJson(json);
124129
}
130+
131+
/**
132+
* Formats a diagnostic migration report as a JSON object.
133+
*
134+
* <p>The output is a pretty-printed JSON object with the following structure:</p>
135+
* <pre>{@code
136+
* {
137+
* "file": "player.json",
138+
* "type": "player",
139+
* "fromVersion": 100,
140+
* "toVersion": 200,
141+
* "durationMs": 42,
142+
* "fixCount": 2,
143+
* "ruleCount": 3,
144+
* "fieldOperationCount": 5,
145+
* "fixes": [
146+
* {
147+
* "name": "PlayerV1ToV2Fix",
148+
* "fromVersion": 100,
149+
* "toVersion": 150,
150+
* "durationMs": 20,
151+
* "rules": [
152+
* {
153+
* "name": "renameField",
154+
* "matched": true,
155+
* "durationMs": 5,
156+
* "fieldOperations": [
157+
* {
158+
* "type": "RENAME",
159+
* "field": "oldName",
160+
* "target": "newName"
161+
* }
162+
* ]
163+
* }
164+
* ]
165+
* }
166+
* ],
167+
* "warnings": []
168+
* }
169+
* }</pre>
170+
*
171+
* @param fileName the name of the migrated file
172+
* @param type the type reference ID (e.g., "player", "world")
173+
* @param report the diagnostic migration report containing fix and field operation details
174+
* @return a pretty-printed JSON string representing the diagnostic report
175+
*/
176+
@Override
177+
@NotNull
178+
public String formatDiagnostic(
179+
@NotNull final String fileName,
180+
@NotNull final String type,
181+
@NotNull final MigrationReport report
182+
) {
183+
Preconditions.checkNotNull(fileName, "fileName must not be null");
184+
Preconditions.checkNotNull(type, "type must not be null");
185+
Preconditions.checkNotNull(report, "report must not be null");
186+
187+
final JsonObject json = new JsonObject();
188+
json.addProperty("file", fileName);
189+
json.addProperty("type", type);
190+
json.addProperty("fromVersion", report.fromVersion().getVersion());
191+
json.addProperty("toVersion", report.toVersion().getVersion());
192+
json.addProperty("durationMs", report.totalDuration().toMillis());
193+
json.addProperty("fixCount", report.fixCount());
194+
json.addProperty("ruleCount", report.ruleApplicationCount());
195+
json.addProperty("fieldOperationCount", report.totalFieldOperationCount());
196+
197+
final JsonArray fixes = new JsonArray();
198+
for (final FixExecution fix : report.fixExecutions()) {
199+
final JsonObject fixObj = new JsonObject();
200+
fixObj.addProperty("name", fix.fixName());
201+
fixObj.addProperty("fromVersion", fix.fromVersion().getVersion());
202+
fixObj.addProperty("toVersion", fix.toVersion().getVersion());
203+
fixObj.addProperty("durationMs", fix.durationMillis());
204+
205+
final JsonArray rules = new JsonArray();
206+
for (final RuleApplication rule : fix.ruleApplications()) {
207+
final JsonObject ruleObj = new JsonObject();
208+
ruleObj.addProperty("name", rule.ruleName());
209+
ruleObj.addProperty("matched", rule.matched());
210+
ruleObj.addProperty("durationMs", rule.durationMillis());
211+
212+
final JsonArray fieldOps = new JsonArray();
213+
for (final FieldOperation fieldOp : rule.fieldOperations()) {
214+
final JsonObject opObj = new JsonObject();
215+
opObj.addProperty("type", fieldOp.operationType().name());
216+
opObj.addProperty("field", fieldOp.fieldPathString());
217+
fieldOp.targetFieldNameOpt()
218+
.ifPresent(target -> opObj.addProperty("target", target));
219+
fieldOp.descriptionOpt()
220+
.ifPresent(desc -> opObj.addProperty("description", desc));
221+
fieldOps.add(opObj);
222+
}
223+
ruleObj.add("fieldOperations", fieldOps);
224+
rules.add(ruleObj);
225+
}
226+
fixObj.add("rules", rules);
227+
fixes.add(fixObj);
228+
}
229+
json.add("fixes", fixes);
230+
231+
final JsonArray warnings = new JsonArray();
232+
for (final String warning : report.warnings()) {
233+
warnings.add(warning);
234+
}
235+
json.add("warnings", warnings);
236+
237+
return GSON.toJson(json);
238+
}
125239
}

aether-datafixers-cli/src/main/java/de/splatgames/aether/datafixers/cli/report/ReportFormatter.java

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
package de.splatgames.aether.datafixers.cli.report;
2424

2525
import com.google.common.base.Preconditions;
26+
import de.splatgames.aether.datafixers.api.diagnostic.MigrationReport;
2627
import org.jetbrains.annotations.NotNull;
2728

2829
import java.time.Duration;
@@ -72,6 +73,25 @@ String formatSimple(
7273
@NotNull Duration duration
7374
);
7475

76+
/**
77+
* Formats a diagnostic migration report including field-level operation details.
78+
*
79+
* <p>When diagnostics are enabled, the report includes comprehensive information
80+
* about each fix execution and its field-level operations (renames, removals,
81+
* additions, transforms, etc.).</p>
82+
*
83+
* @param fileName the name of the migrated file
84+
* @param type the type reference ID
85+
* @param report the diagnostic migration report
86+
* @return the formatted diagnostic report string
87+
*/
88+
@NotNull
89+
String formatDiagnostic(
90+
@NotNull String fileName,
91+
@NotNull String type,
92+
@NotNull MigrationReport report
93+
);
94+
7595
/**
7696
* Gets a formatter by format name.
7797
*

0 commit comments

Comments
 (0)