|
23 | 23 | package de.splatgames.aether.datafixers.api.rewrite; |
24 | 24 |
|
25 | 25 | import com.google.common.base.Preconditions; |
| 26 | +import de.splatgames.aether.datafixers.api.diagnostic.DiagnosticContext; |
26 | 27 | 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; |
27 | 30 | import de.splatgames.aether.datafixers.api.dynamic.Dynamic; |
28 | 31 | import de.splatgames.aether.datafixers.api.dynamic.DynamicOps; |
29 | 32 | import de.splatgames.aether.datafixers.api.optic.Finder; |
|
43 | 46 | import java.util.function.Predicate; |
44 | 47 |
|
45 | 48 | /** |
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. |
47 | 51 | * |
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> |
52 | 69 | * |
53 | 70 | * <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> |
54 | 78 | * <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> |
63 | 91 | * </ul> |
64 | 92 | * |
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> |
71 | 110 | * |
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> |
75 | 123 | * |
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))) |
79 | 225 | * ); |
80 | 226 | * |
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). |
83 | 231 | * }</pre> |
84 | 232 | * |
85 | | - * <h2>Sequencing vs Choice</h2> |
| 233 | + * <h2>Sequencing vs Choice — Quick Reference</h2> |
86 | 234 | * <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> |
90 | 241 | * </ul> |
91 | 242 | * |
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> |
94 | 245 | * <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> |
98 | 254 | * </ul> |
99 | 255 | * |
| 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 | + * |
100 | 264 | * <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> |
103 | 281 | * |
104 | 282 | * @author Erik Pförtner |
105 | 283 | * @see TypeRewriteRule |
| 284 | + * @see FieldAwareRule |
| 285 | + * @see FieldOperation |
106 | 286 | * @see Finder |
| 287 | + * @see MigrationReport |
| 288 | + * @see DiagnosticContext |
107 | 289 | * @since 0.1.0 |
108 | 290 | */ |
109 | 291 | public final class Rules { |
|
0 commit comments