Skip to content

Commit 8603d0f

Browse files
committed
Add field-level diagnostics documentation and examples
Document field-level diagnostics for rewrite rules, provide usage examples, and update API references across related pages. Signed-off-by: Erik Pförtner <splatcrafter@splatgames.de>
1 parent 6b21ce6 commit 8603d0f

3 files changed

Lines changed: 466 additions & 52 deletions

File tree

Lines changed: 337 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,337 @@
1+
# How to Use Field-Level Diagnostics
2+
3+
This guide shows how to capture and inspect field-level diagnostic metadata during data migrations, giving you fine-grained visibility into which fields each rule affects.
4+
5+
## Overview
6+
7+
Standard type-level diagnostics tell you that a rule matched on a particular type, but not which fields were renamed, removed, or transformed. Field-level diagnostics solve this by capturing structured `FieldOperation` metadata for every field-aware rule application.
8+
9+
Key points:
10+
11+
- Field-level diagnostics are **opt-in** via `DiagnosticOptions.captureFieldDetails(true)` (enabled by default in `defaults()`)
12+
- Rules created by the field operation methods in `Rules` automatically implement `FieldAwareRule`
13+
- After migration, inspect `RuleApplication.fieldOperations()` for per-field metadata
14+
15+
## Quick Start
16+
17+
```java
18+
import de.splatgames.aether.datafixers.api.diagnostic.*;
19+
import de.splatgames.aether.datafixers.api.rewrite.Rules;
20+
21+
// 1. Create rules using Rules factory methods
22+
TypeRewriteRule rule = Rules.renameField(ops, "playerName", "name");
23+
24+
// 2. The rule automatically implements FieldAwareRule — no extra work needed
25+
26+
// 3. Run migration with a DiagnosticContext
27+
DiagnosticContext context = DiagnosticContext.create();
28+
Dynamic<JsonElement> result = fixer.update(
29+
TypeReferences.PLAYER, inputData,
30+
new DataVersion(1), new DataVersion(2),
31+
context
32+
);
33+
34+
// 4. Inspect field operations in the report
35+
MigrationReport report = context.getReport();
36+
for (FixExecution fix : report.fixExecutions()) {
37+
for (RuleApplication rule : fix.ruleApplications()) {
38+
for (FieldOperation op : rule.fieldOperations()) {
39+
System.out.println(op.toSummary());
40+
// Output: RENAME(playerName -> name)
41+
}
42+
}
43+
}
44+
```
45+
46+
## Which Rules Are Field-Aware?
47+
48+
All field operation methods in `Rules` produce rules that implement `FieldAwareRule`:
49+
50+
**Single-field operations:**
51+
- `renameField`, `removeField`, `addField`, `transformField`, `setField`
52+
53+
**Batch operations:**
54+
- `renameFields`, `removeFields`
55+
56+
**Structural operations:**
57+
- `groupFields`, `flattenField`, `moveField`, `copyField`
58+
59+
**Path-based operations (nested fields):**
60+
- `renameFieldAt`, `removeFieldAt`, `addFieldAt`, `transformFieldAt`
61+
62+
**Conditional operations:**
63+
- `ifFieldExists`, `ifFieldMissing`, `ifFieldEquals`
64+
65+
**Batch transform:**
66+
- `batch()` via `BatchTransform`
67+
68+
**Composition methods** (`seq`, `seqAll`, `choice`) aggregate field operations from their children. If all children are field-aware, the composed rule is also a `FieldAwareRule`.
69+
70+
**Non-field-aware** (traversal/structural): `all`, `one`, `everywhere`, `bottomUp`, `topDown`, `dynamicTransform` — these do not carry field-level metadata.
71+
72+
## The FieldOperation Record
73+
74+
`FieldOperation` is a record with four components:
75+
76+
| Component | Type | Description |
77+
|-------------------|------------------------|----------------------------------------------------------|
78+
| `operationType` | `FieldOperationType` | The kind of field operation (RENAME, REMOVE, etc.) |
79+
| `fieldPath` | `List<String>` | Path segments to the affected field |
80+
| `targetFieldName` | `String` (nullable) | Target field name for operations that have one |
81+
| `description` | `String` (nullable) | Optional human-readable description |
82+
83+
### Factory Methods
84+
85+
```java
86+
FieldOperation.rename("old", "new") // RENAME, ["old"], target="new"
87+
FieldOperation.remove("field") // REMOVE, ["field"]
88+
FieldOperation.add("field") // ADD, ["field"]
89+
FieldOperation.transform("field") // TRANSFORM, ["field"]
90+
FieldOperation.set("field") // SET, ["field"]
91+
FieldOperation.move("a.b", "c.d") // MOVE, ["a","b"], target="c.d"
92+
FieldOperation.copy("a.b", "c.d") // COPY, ["a","b"], target="c.d"
93+
FieldOperation.renamePath("pos.x", "posX") // RENAME, ["pos","x"], target="posX"
94+
FieldOperation.group("pos", "x", "y", "z") // GROUP, ["x","y","z"], target="pos"
95+
FieldOperation.flatten("position") // FLATTEN, ["position"]
96+
FieldOperation.conditional("field", "exists") // CONDITIONAL, ["field"], desc="exists"
97+
```
98+
99+
### Convenience Methods
100+
101+
| Method | Return Type | Description |
102+
|------------------------|---------------------|-------------------------------------------------------|
103+
| `fieldPathString()` | `String` | Dot-notation path (e.g., `"position.x"`) |
104+
| `isNested()` | `boolean` | `true` if field path has more than one segment |
105+
| `toSummary()` | `String` | Human-readable summary (e.g., `"RENAME(old -> new)"`) |
106+
| `targetFieldNameOpt()` | `Optional<String>` | Target field name as Optional |
107+
| `descriptionOpt()` | `Optional<String>` | Description as Optional |
108+
109+
## The FieldOperationType Enum
110+
111+
Ten operation types classify what a rule does to a field:
112+
113+
| Constant | Display Name | Requires Target | Structural |
114+
|-----------------|-----------------|-----------------|------------|
115+
| `RENAME` | `"rename"` | Yes | No |
116+
| `REMOVE` | `"remove"` | No | No |
117+
| `ADD` | `"add"` | No | No |
118+
| `TRANSFORM` | `"transform"` | No | No |
119+
| `SET` | `"set"` | No | No |
120+
| `MOVE` | `"move"` | Yes | Yes |
121+
| `COPY` | `"copy"` | Yes | Yes |
122+
| `GROUP` | `"group"` | Yes | Yes |
123+
| `FLATTEN` | `"flatten"` | No | Yes |
124+
| `CONDITIONAL` | `"conditional"` | No | No |
125+
126+
### Methods
127+
128+
| Method | Return Type | Description |
129+
|--------------------|-------------|-----------------------------------------------|
130+
| `displayName()` | `String` | Lowercase name (`"rename"`, `"remove"`, etc.) |
131+
| `requiresTarget()` | `boolean` | `true` for RENAME, MOVE, COPY, GROUP |
132+
| `isStructural()` | `boolean` | `true` for MOVE, COPY, GROUP, FLATTEN |
133+
| `toString()` | `String` | Returns `displayName()` |
134+
135+
## The FieldAwareRule Interface
136+
137+
Any rule can be checked for field-level metadata at runtime:
138+
139+
```java
140+
if (rule instanceof FieldAwareRule fieldAware) {
141+
List<FieldOperation> ops = fieldAware.fieldOperations();
142+
// inspect field operations
143+
}
144+
```
145+
146+
Custom rules can implement `FieldAwareRule` to participate in field-level diagnostics:
147+
148+
```java
149+
public class MyCustomRule implements TypeRewriteRule, FieldAwareRule {
150+
@Override
151+
public Optional<Typed<?>> rewrite(Type<?> type, Typed<?> input) {
152+
// rule logic
153+
}
154+
155+
@Override
156+
public List<FieldOperation> fieldOperations() {
157+
return List.of(FieldOperation.transform("myField"));
158+
}
159+
}
160+
```
161+
162+
## Composition Aggregation
163+
164+
Composition methods aggregate field operations from their children:
165+
166+
```java
167+
TypeRewriteRule composed = Rules.seq(
168+
Rules.renameField(ops, "old", "new"), // 1 RENAME
169+
Rules.addField(ops, "score", defaultVal) // 1 ADD
170+
);
171+
172+
// composed instanceof FieldAwareRule → true
173+
// fieldOperations() contains 2 entries: RENAME + ADD
174+
```
175+
176+
The same aggregation applies to `seqAll()`, `choice()`, and `batch()`.
177+
178+
If **all** children are non-field-aware (e.g., all are `dynamicTransform` rules), the composition is **not** a `FieldAwareRule`.
179+
180+
## Aggregation in Reports
181+
182+
### Per-Rule
183+
184+
```java
185+
for (RuleApplication rule : fix.ruleApplications()) {
186+
if (rule.hasFieldOperations()) {
187+
for (FieldOperation op : rule.fieldOperations()) {
188+
System.out.println(op.toSummary());
189+
}
190+
}
191+
}
192+
```
193+
194+
### Per-Fix
195+
196+
```java
197+
List<FieldOperation> allOps = fix.allFieldOperations();
198+
int count = fix.fieldOperationCount();
199+
```
200+
201+
### Per-Migration
202+
203+
```java
204+
int totalOps = report.totalFieldOperationCount();
205+
```
206+
207+
### Filtering by Type
208+
209+
```java
210+
List<FieldOperation> renames = rule.fieldOperationsOfType(FieldOperationType.RENAME);
211+
```
212+
213+
## Complete Example
214+
215+
```java
216+
import de.splatgames.aether.datafixers.api.diagnostic.*;
217+
import de.splatgames.aether.datafixers.api.rewrite.*;
218+
import de.splatgames.aether.datafixers.core.fix.SchemaDataFix;
219+
220+
public class PlayerV1ToV2Fix extends SchemaDataFix {
221+
222+
public PlayerV1ToV2Fix(Schema inputSchema, Schema outputSchema) {
223+
super("PlayerV1ToV2", inputSchema, outputSchema);
224+
}
225+
226+
@Override
227+
protected TypeRewriteRule makeRule(Schema inputSchema, Schema outputSchema) {
228+
return Rules.seq(
229+
Rules.renameField(ops(), "playerName", "name"),
230+
Rules.removeField(ops(), "legacyId"),
231+
Rules.addField(ops(), "score", ops().createInt(0)),
232+
Rules.transformField(ops(), "health", value ->
233+
ops().createFloat(Math.max(0f, ops().getFloat(value))))
234+
);
235+
}
236+
}
237+
238+
// --- Using the fix with diagnostics ---
239+
240+
DiagnosticContext context = DiagnosticContext.create(
241+
DiagnosticOptions.builder()
242+
.captureRuleDetails(true)
243+
.captureFieldDetails(true)
244+
.build()
245+
);
246+
247+
Dynamic<JsonElement> result = fixer.update(
248+
TypeReferences.PLAYER, inputData,
249+
new DataVersion(1), new DataVersion(2),
250+
context
251+
);
252+
253+
MigrationReport report = context.getReport();
254+
255+
for (FixExecution fix : report.fixExecutions()) {
256+
System.out.println(fix.fixName() + ": " + fix.fieldOperationCount() + " field ops");
257+
258+
for (RuleApplication rule : fix.ruleApplications()) {
259+
if (rule.hasFieldOperations()) {
260+
for (FieldOperation op : rule.fieldOperations()) {
261+
System.out.println(" " + op.toSummary());
262+
}
263+
}
264+
}
265+
}
266+
267+
// Output:
268+
// PlayerV1ToV2: 4 field ops
269+
// RENAME(playerName -> name)
270+
// REMOVE(legacyId)
271+
// ADD(score)
272+
// TRANSFORM(health)
273+
274+
System.out.println("Total field operations: " + report.totalFieldOperationCount());
275+
```
276+
277+
## Performance Considerations
278+
279+
- **Field metadata is collected at rule construction time** — there is zero runtime cost for the metadata itself
280+
- `captureFieldDetails(false)` skips the `instanceof` check and list copy in `DiagnosticRuleWrapper`
281+
- `captureRuleDetails` must also be `true` for field details to have any effect
282+
- For production, consider `DiagnosticOptions.minimal()` which disables both rule details and field details
283+
284+
```java
285+
// Development/debugging — full field-level diagnostics
286+
DiagnosticContext devContext = DiagnosticContext.create(DiagnosticOptions.defaults());
287+
288+
// Production — timing only, no field details
289+
DiagnosticContext prodContext = DiagnosticContext.create(DiagnosticOptions.minimal());
290+
291+
// Best: No context for maximum performance
292+
fixer.update(type, data, from, to); // No context = no overhead
293+
```
294+
295+
## API Reference
296+
297+
### FieldOperation
298+
299+
| Method / Component | Type | Description |
300+
|------------------------|-------------------------|-------------------------------------------------|
301+
| `operationType()` | `FieldOperationType` | The kind of field operation |
302+
| `fieldPath()` | `List<String>` | Path segments to the affected field |
303+
| `targetFieldName()` | `String` (nullable) | Target field name (rename target, destination) |
304+
| `description()` | `String` (nullable) | Optional human-readable description |
305+
| `fieldPathString()` | `String` | Dot-notation path string |
306+
| `isNested()` | `boolean` | Whether path has more than one segment |
307+
| `targetFieldNameOpt()` | `Optional<String>` | Target as Optional |
308+
| `descriptionOpt()` | `Optional<String>` | Description as Optional |
309+
| `toSummary()` | `String` | Human-readable summary |
310+
311+
### FieldOperationType
312+
313+
| Constant | `displayName()` | `requiresTarget()` | `isStructural()` |
314+
|----------------|------------------|----------------------|--------------------|
315+
| `RENAME` | `"rename"` | `true` | `false` |
316+
| `REMOVE` | `"remove"` | `false` | `false` |
317+
| `ADD` | `"add"` | `false` | `false` |
318+
| `TRANSFORM` | `"transform"` | `false` | `false` |
319+
| `SET` | `"set"` | `false` | `false` |
320+
| `MOVE` | `"move"` | `true` | `true` |
321+
| `COPY` | `"copy"` | `true` | `true` |
322+
| `GROUP` | `"group"` | `true` | `true` |
323+
| `FLATTEN` | `"flatten"` | `false` | `true` |
324+
| `CONDITIONAL` | `"conditional"` | `false` | `false` |
325+
326+
### FieldAwareRule
327+
328+
| Method | Return Type | Description |
329+
|---------------------|------------------------|---------------------------------|
330+
| `fieldOperations()` | `List<FieldOperation>` | Field-level operations metadata |
331+
332+
## Related
333+
334+
- [Use Diagnostics](use-diagnostics.md)
335+
- [Compose Fixes](compose-fixes.md)
336+
- [Batch Operations](batch-operations.md)
337+
- [Conditional Rules](conditional-rules.md)

docs/how-to/index.md

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -32,12 +32,13 @@ This section contains task-oriented guides that show you how to accomplish speci
3232

3333
## Development & Testing
3434

35-
| Guide | Description |
36-
|-----------------------------------------|-----------------------------------------|
37-
| [Debug Migrations](debug-migrations.md) | Troubleshoot migration issues |
38-
| [Test Migrations](test-migrations.md) | Write unit tests for your fixes |
39-
| [Log Migrations](log-migrations.md) | Add logging to track migration progress |
40-
| [Use Diagnostics](use-diagnostics.md) | Capture structured migration reports |
35+
| Guide | Description |
36+
|-------------------------------------------------------|------------------------------------------|
37+
| [Debug Migrations](debug-migrations.md) | Troubleshoot migration issues |
38+
| [Test Migrations](test-migrations.md) | Write unit tests for your fixes |
39+
| [Log Migrations](log-migrations.md) | Add logging to track migration progress |
40+
| [Use Diagnostics](use-diagnostics.md) | Capture structured migration reports |
41+
| [Field-Level Diagnostics](field-level-diagnostics.md) | Inspect which fields each rule affects |
4142

4243
## Advanced Usage
4344

0 commit comments

Comments
 (0)