Skip to content

Commit 8cbfb46

Browse files
committed
Expand Rules Javadoc with comprehensive field-aware API documentation
- Added detailed descriptions for combinators, traversal methods, field operations, and diagnostic integration. - Included examples and usage scenarios for migration rules and field-level metadata handling. - Enhanced guidance on diagnostics, batch operations, and path-based transformations. Signed-off-by: Erik Pförtner <splatcrafter@splatgames.de>
1 parent b9209ad commit 8cbfb46

1 file changed

Lines changed: 220 additions & 38 deletions

File tree

  • aether-datafixers-api/src/main/java/de/splatgames/aether/datafixers/api/rewrite

aether-datafixers-api/src/main/java/de/splatgames/aether/datafixers/api/rewrite/Rules.java

Lines changed: 220 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,10 @@
2323
package de.splatgames.aether.datafixers.api.rewrite;
2424

2525
import com.google.common.base.Preconditions;
26+
import de.splatgames.aether.datafixers.api.diagnostic.DiagnosticContext;
2627
import de.splatgames.aether.datafixers.api.diagnostic.FieldOperation;
28+
import de.splatgames.aether.datafixers.api.diagnostic.FieldOperationType;
29+
import de.splatgames.aether.datafixers.api.diagnostic.MigrationReport;
2730
import de.splatgames.aether.datafixers.api.dynamic.Dynamic;
2831
import de.splatgames.aether.datafixers.api.dynamic.DynamicOps;
2932
import de.splatgames.aether.datafixers.api.optic.Finder;
@@ -43,67 +46,246 @@
4346
import java.util.function.Predicate;
4447

4548
/**
46-
* Factory class providing common combinators for building {@link TypeRewriteRule} instances.
49+
* Factory class providing combinators and field-level operations for building
50+
* {@link TypeRewriteRule} instances.
4751
*
48-
* <p>The {@code Rules} class is a comprehensive toolkit for constructing data migration rules.
49-
* It provides a rich set of combinators that allow complex migration logic to be built from simple, composable
50-
* primitives. These combinators follow functional programming patterns and enable declarative specification of data
51-
* transformations.</p>
52+
* <p>{@code Rules} is the canonical entry point for constructing data migration
53+
* logic in Aether Datafixers. It exposes a rich, type-safe DSL of small,
54+
* composable primitives that can be combined into arbitrarily complex
55+
* transformations. Every factory method here returns a stateless, thread-safe
56+
* {@link TypeRewriteRule} that can be reused across migrations.</p>
57+
*
58+
* <p>Every field-operation method (rename, remove, add, transform, batch
59+
* variants, path-based variants, and conditionals) returns a rule that
60+
* implements {@link FieldAwareRule} and carries structured
61+
* {@link FieldOperation} metadata. When such a rule runs inside a
62+
* {@link DiagnosticContext}, the
63+
* resulting {@link MigrationReport
64+
* MigrationReport} captures exactly which fields were touched and how — not
65+
* merely <i>that</i> a rule ran. The composition combinators
66+
* ({@link #seq}, {@link #seqAll}, {@link #choice}, {@link #batch})
67+
* transparently aggregate this metadata from their children, so a single
68+
* composed rule surfaces all of its sub-operations as a unified group.</p>
5269
*
5370
* <h2>Combinator Categories</h2>
71+
*
72+
* <h3>1. Basic Composition (sequence and choice)</h3>
73+
* <p>These combinators stitch other rules together. They preserve and aggregate
74+
* field operation metadata from their children, so a {@code seq} of three
75+
* field-aware rules surfaces as a single rule whose
76+
* {@link FieldAwareRule#fieldOperations()} contains all three operations
77+
* flattened in order.</p>
5478
* <ul>
55-
* <li><strong>Basic Combinators:</strong> {@link #seq}, {@link #seqAll}, {@link #choice},
56-
* {@link #checkOnce}, {@link #tryOnce}</li>
57-
* <li><strong>Traversal Combinators:</strong> {@link #all}, {@link #one}, {@link #everywhere},
58-
* {@link #bottomUp}, {@link #topDown}</li>
59-
* <li><strong>Type-Specific:</strong> {@link #ifType}, {@link #transformType}</li>
60-
* <li><strong>Field Operations:</strong> {@link #renameField}, {@link #removeField},
61-
* {@link #addField}, {@link #transformField}</li>
62-
* <li><strong>Utilities:</strong> {@link #noop}, {@link #log}</li>
79+
* <li>{@link #seq(TypeRewriteRule...) seq} — Apply rules in order; all must
80+
* succeed. The result of each rule feeds the next (AND semantics).</li>
81+
* <li>{@link #seqAll(TypeRewriteRule...) seqAll} — Like {@code seq}, but
82+
* failures are tolerated and the next rule receives the previous output
83+
* unchanged (forgiving AND).</li>
84+
* <li>{@link #choice(TypeRewriteRule...) choice} — First successful rule wins
85+
* (OR semantics). Field operations from <i>all</i> alternatives are
86+
* aggregated into the metadata, since any of them might match at runtime.</li>
87+
* <li>{@link #checkOnce(TypeRewriteRule) checkOnce} — Apply the inner rule a
88+
* single time without recursing into the result.</li>
89+
* <li>{@link #tryOnce(TypeRewriteRule) tryOnce} — Like {@code checkOnce}, but
90+
* silently swallows failures (returns the input unchanged).</li>
6391
* </ul>
6492
*
65-
* <h2>Usage Example</h2>
66-
* <pre>{@code
67-
* // Build a complex migration rule using combinators
68-
* TypeRewriteRule migration = Rules.seq(
69-
* // First, rename the old field
70-
* Rules.renameField(GsonOps.INSTANCE, "playerName", "name"),
93+
* <h3>2. Traversal Combinators</h3>
94+
* <p>These walk recursive data structures and apply a rule at one or more
95+
* positions. Each combinator has two overloads: one with an explicit
96+
* {@link DynamicOps} for {@link Dynamic}-based traversal, and a higher-level
97+
* overload that operates on the {@link Type} system.</p>
98+
* <ul>
99+
* <li>{@link #all(TypeRewriteRule) all} — Apply the rule to every immediate
100+
* child of the current node. All children must succeed.</li>
101+
* <li>{@link #one(TypeRewriteRule) one} — Apply the rule to exactly one
102+
* child; succeed as soon as one match is found.</li>
103+
* <li>{@link #everywhere(TypeRewriteRule) everywhere} — Apply the rule at the
104+
* current node and recursively at every descendant.</li>
105+
* <li>{@link #bottomUp(TypeRewriteRule) bottomUp} — Recurse first, then apply
106+
* the rule on the way back up (leaves before parents).</li>
107+
* <li>{@link #topDown(TypeRewriteRule) topDown} — Apply the rule to the
108+
* current node first, then recurse into the result (parents before leaves).</li>
109+
* </ul>
71110
*
72-
* // Then add a default score if missing
73-
* Rules.addField(GsonOps.INSTANCE, "score",
74-
* new Dynamic<>(GsonOps.INSTANCE, JsonPrimitive(0))),
111+
* <h3>3. Type Filters and Type-Aware Updates</h3>
112+
* <ul>
113+
* <li>{@link #ifType(Type, TypeRewriteRule) ifType} — Apply the inner rule
114+
* only if the current value matches the given {@link Type}; otherwise
115+
* leave the value unchanged.</li>
116+
* <li>{@link #transformType(String, Type, Function) transformType} — Apply a
117+
* value-level {@code A -> A} transformation to every occurrence of a
118+
* given {@link Type} in the structure, named for diagnostics.</li>
119+
* <li>{@link #updateAt(String, DynamicOps, Finder, Function) updateAt} —
120+
* Update the {@link Dynamic} at a position located by a {@link Finder},
121+
* leaving everything else intact.</li>
122+
* </ul>
75123
*
76-
* // Finally, transform the level field
77-
* Rules.transformField(GsonOps.INSTANCE, "level",
78-
* d -> d.createInt(d.asInt().orElse(0) + 1))
124+
* <h3>4. Top-Level Field Operations</h3>
125+
* <p>These are the most common building blocks. Each operates on a single,
126+
* top-level field of a {@link Dynamic} map and returns a
127+
* {@link FieldAwareRule} carrying the corresponding {@link FieldOperation}.</p>
128+
* <ul>
129+
* <li>{@link #renameField(DynamicOps, String, String) renameField} — Rename
130+
* a field, preserving its value.</li>
131+
* <li>{@link #removeField(DynamicOps, String) removeField} — Drop a field
132+
* entirely.</li>
133+
* <li>{@link #addField(DynamicOps, String, Dynamic) addField} — Add a field
134+
* with a default value, only if it does not already exist.</li>
135+
* <li>{@link #transformField(DynamicOps, String, Function) transformField} —
136+
* Apply a {@code Dynamic -> Dynamic} function to an existing field.</li>
137+
* <li>{@link #setField(DynamicOps, String, Dynamic) setField} — Unconditionally
138+
* set a field's value, overwriting any existing value.</li>
139+
* </ul>
140+
*
141+
* <h3>5. Batch Field Operations</h3>
142+
* <p>Equivalents that operate on many fields at once for performance — useful
143+
* when migrating dozens of fields in the same step. They return a single rule
144+
* whose field-operation metadata contains one entry per affected field.</p>
145+
* <ul>
146+
* <li>{@link #renameFields(DynamicOps, Map) renameFields} — Rename many
147+
* fields in a single pass, given an old-name → new-name map.</li>
148+
* <li>{@link #removeFields(DynamicOps, String...) removeFields} — Remove
149+
* multiple fields in a single pass.</li>
150+
* <li>{@link #groupFields(DynamicOps, String, String...) groupFields} —
151+
* Collapse a set of flat fields into a nested object.</li>
152+
* <li>{@link #flattenField(DynamicOps, String) flattenField} — The inverse:
153+
* lift the entries of a nested object into the parent.</li>
154+
* <li>{@link #moveField(DynamicOps, String, String) moveField} — Relocate a
155+
* field (possibly across nesting levels), removing the source.</li>
156+
* <li>{@link #copyField(DynamicOps, String, String) copyField} — Like
157+
* {@code moveField} but keeps the source intact.</li>
158+
* <li>{@link #batch(DynamicOps, Consumer) batch} — Imperative builder for
159+
* composing many of the above operations into a single rule with shared
160+
* metadata; useful for very large per-step migrations.</li>
161+
* </ul>
162+
*
163+
* <h3>6. Path-Based (Nested) Field Operations</h3>
164+
* <p>Variants of the top-level operations that accept a dot-notation path
165+
* (e.g. {@code "position.x"}) for navigating into nested objects. Internally
166+
* they use {@link Finder} optics; the resulting rule carries a
167+
* {@link FieldOperation} whose {@code fieldPath} reflects the nested structure.</p>
168+
* <ul>
169+
* <li>{@link #transformFieldAt(DynamicOps, String, Function) transformFieldAt}</li>
170+
* <li>{@link #renameFieldAt(DynamicOps, String, String) renameFieldAt}</li>
171+
* <li>{@link #removeFieldAt(DynamicOps, String) removeFieldAt}</li>
172+
* <li>{@link #addFieldAt(DynamicOps, String, Dynamic) addFieldAt}</li>
173+
* </ul>
174+
*
175+
* <h3>7. Conditional Field Operations</h3>
176+
* <p>Apply an inner rule only when a field-level condition is satisfied. These
177+
* are typically composed with the field combinators above to express "migrate
178+
* X only when Y looks like Z" patterns. The diagnostic metadata records the
179+
* condition itself as a {@link FieldOperation} of type
180+
* {@link FieldOperationType#CONDITIONAL}.</p>
181+
* <ul>
182+
* <li>{@link #ifFieldExists(DynamicOps, String, TypeRewriteRule) ifFieldExists} —
183+
* Apply the inner rule only when a named field is present.</li>
184+
* <li>{@link #ifFieldMissing(DynamicOps, String, TypeRewriteRule) ifFieldMissing} —
185+
* Apply the inner rule only when a named field is absent.</li>
186+
* <li>{@link #ifFieldEquals(DynamicOps, String, Object, TypeRewriteRule) ifFieldEquals} —
187+
* Apply the inner rule only when a field equals a given value.</li>
188+
* <li>{@link #conditionalTransform(DynamicOps, Predicate, Function) conditionalTransform} —
189+
* General-purpose predicate-based transformation for cases the
190+
* specialised helpers do not cover.</li>
191+
* </ul>
192+
*
193+
* <h3>8. Escape Hatches and Utilities</h3>
194+
* <ul>
195+
* <li>{@link #dynamicTransform(String, DynamicOps, Function) dynamicTransform} —
196+
* Wrap an arbitrary {@code Dynamic -> Dynamic} function as a rule. Use
197+
* this when none of the higher-level combinators fits; the resulting
198+
* rule does <i>not</i> carry field-operation metadata, so the diagnostic
199+
* system reports it as opaque.</li>
200+
* <li>{@link #noop() noop} — A rule that returns its input unchanged. Useful
201+
* as a placeholder or as the {@code else}-branch of a choice.</li>
202+
* <li>{@link #log(String, TypeRewriteRule) log} — Wrap a rule in SLF4J
203+
* logging for debugging migrations.</li>
204+
* </ul>
205+
*
206+
* <h2>Putting It Together — A Realistic Migration</h2>
207+
* <pre>{@code
208+
* // Migrate a player save from v1 to v2:
209+
* // - rename "playerName" to "name"
210+
* // - drop the legacy "lastSeen" field
211+
* // - regroup x/y/z coordinates into a nested "position" object
212+
* // - add a default "health" field
213+
* // - bump "level" by one — but only if it currently exists
214+
* // - all bundled into a single sequence so the diagnostics report
215+
* // attributes every change to the migration step.
216+
* TypeRewriteRule playerV1ToV2 = Rules.seq(
217+
* Rules.renameField(GsonOps.INSTANCE, "playerName", "name"),
218+
* Rules.removeField(GsonOps.INSTANCE, "lastSeen"),
219+
* Rules.groupFields(GsonOps.INSTANCE, "position", "x", "y", "z"),
220+
* Rules.addField(GsonOps.INSTANCE, "health",
221+
* new Dynamic<>(GsonOps.INSTANCE, GsonOps.INSTANCE.createInt(100))),
222+
* Rules.ifFieldExists(GsonOps.INSTANCE, "level",
223+
* Rules.transformField(GsonOps.INSTANCE, "level",
224+
* d -> d.createInt(d.asInt().result().orElse(0) + 1)))
79225
* );
80226
*
81-
* // Apply the migration
82-
* Typed<?> result = migration.apply(inputData);
227+
* // When run with a DiagnosticContext, the resulting MigrationReport contains
228+
* // one FieldOperation per top-level rule above (5 entries: RENAME, REMOVE,
229+
* // GROUP, ADD, CONDITIONAL — the inner TRANSFORM is recorded under the
230+
* // CONDITIONAL wrapper).
83231
* }</pre>
84232
*
85-
* <h2>Sequencing vs Choice</h2>
233+
* <h2>Sequencing vs Choice — Quick Reference</h2>
86234
* <ul>
87-
* <li>{@link #seq} - All rules must succeed (AND-like)</li>
88-
* <li>{@link #seqAll} - Apply all rules, continue on failure (forgiving AND)</li>
89-
* <li>{@link #choice} - First successful rule wins (OR-like)</li>
235+
* <li>{@link #seq} — All rules must succeed (AND-like). The output of each
236+
* rule feeds the next.</li>
237+
* <li>{@link #seqAll} — Apply all rules, but tolerate individual failures
238+
* (forgiving AND). The next rule always sees the previous output.</li>
239+
* <li>{@link #choice} — First successful rule wins (OR-like). Subsequent
240+
* alternatives are not evaluated.</li>
90241
* </ul>
91242
*
92-
* <h2>Traversal Strategies</h2>
93-
* <p>For recursive data structures:</p>
243+
* <h2>Traversal Strategies — Quick Reference</h2>
244+
* <p>For recursive structures (lists, nested maps, sums of types):</p>
94245
* <ul>
95-
* <li>{@link #topDown} - Apply rule to parent first, then children</li>
96-
* <li>{@link #bottomUp} - Apply rule to children first, then parent</li>
97-
* <li>{@link #everywhere} - Apply rule at all levels</li>
246+
* <li>{@link #topDown} — Apply the rule at the parent first, then recurse
247+
* into the result. Use when the migration changes the shape of children
248+
* and the parent rule must see the original structure.</li>
249+
* <li>{@link #bottomUp} — Recurse into children first, then apply the rule
250+
* at the parent. Use when the parent rule needs the already-migrated
251+
* children to make a decision.</li>
252+
* <li>{@link #everywhere} — Apply at every node, parents and children, in a
253+
* single combined pass.</li>
98254
* </ul>
99255
*
256+
* <h2>Custom Rules and the Field-Aware Marker</h2>
257+
* <p>Rules created via {@link #dynamicTransform} or by hand-implementing
258+
* {@link TypeRewriteRule} are <i>not</i> field-aware by default — the
259+
* diagnostic system records them but cannot break them down by field. If you
260+
* write a custom rule and want diagnostic visibility, also implement
261+
* {@link FieldAwareRule} and return the operations your rule performs from
262+
* {@link FieldAwareRule#fieldOperations()}.</p>
263+
*
100264
* <h2>Thread Safety</h2>
101-
* <p>All factory methods return stateless, thread-safe rules. The same rule
102-
* instance can be used concurrently for multiple migrations.</p>
265+
* <p>All factory methods return stateless, thread-safe rules. A rule built
266+
* once at application start can be reused concurrently for any number of
267+
* migrations. The internal {@link Finder} cache used by the path-based
268+
* methods is also thread-safe.</p>
269+
*
270+
* <h2>Performance Notes</h2>
271+
* <ul>
272+
* <li>Path parsing for {@code *FieldAt} methods is cached, so repeating the
273+
* same path across many rules has no per-call cost after the first.</li>
274+
* <li>Composition combinators construct flat metadata lists eagerly when the
275+
* rule is built, not at apply time, so diagnostic capture imposes
276+
* essentially zero overhead per migration.</li>
277+
* <li>Prefer the batch variants ({@link #renameFields}, {@link #removeFields},
278+
* {@link #batch}) over many individual calls when migrating many fields
279+
* in the same step — they avoid repeated map traversals.</li>
280+
* </ul>
103281
*
104282
* @author Erik Pförtner
105283
* @see TypeRewriteRule
284+
* @see FieldAwareRule
285+
* @see FieldOperation
106286
* @see Finder
287+
* @see MigrationReport
288+
* @see DiagnosticContext
107289
* @since 0.1.0
108290
*/
109291
public final class Rules {

0 commit comments

Comments
 (0)