Skip to content

Commit fd4b048

Browse files
committed
Add BatchTransform implementation for efficient multi-field processing and update documentation and tests.
1 parent 7ba89dc commit fd4b048

7 files changed

Lines changed: 1339 additions & 38 deletions

File tree

Lines changed: 338 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,338 @@
1+
/*
2+
* Copyright (c) 2025 Splatgames.de Software and Contributors
3+
*
4+
* Permission is hereby granted, free of charge, to any person obtaining a copy
5+
* of this software and associated documentation files (the "Software"), to deal
6+
* in the Software without restriction, including without limitation the rights
7+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8+
* copies of the Software, and to permit persons to whom the Software is
9+
* furnished to do so, subject to the following conditions:
10+
*
11+
* The above copyright notice and this permission notice shall be included in all
12+
* copies or substantial portions of the Software.
13+
*
14+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20+
* SOFTWARE.
21+
*/
22+
23+
package de.splatgames.aether.datafixers.api.rewrite;
24+
25+
import com.google.common.base.Preconditions;
26+
import de.splatgames.aether.datafixers.api.dynamic.Dynamic;
27+
import de.splatgames.aether.datafixers.api.dynamic.DynamicOps;
28+
import org.jetbrains.annotations.NotNull;
29+
30+
import java.util.ArrayList;
31+
import java.util.List;
32+
import java.util.function.Function;
33+
34+
/**
35+
* A builder for batching multiple field operations into a single transformation pass.
36+
*
37+
* <p>When performing multiple field operations (rename, remove, set, transform),
38+
* using separate rules causes each operation to perform its own encode/decode cycle.
39+
* {@code BatchTransform} collects all operations and applies them in a single pass,
40+
* significantly improving performance for complex migrations.</p>
41+
*
42+
* <h2>Performance Comparison</h2>
43+
* <pre>{@code
44+
* // Slow: 4 encode/decode cycles
45+
* Rules.seq(
46+
* Rules.renameField(ops, "playerName", "name"),
47+
* Rules.renameField(ops, "xp", "experience"),
48+
* Rules.removeField(ops, "deprecated"),
49+
* Rules.addField(ops, "version", defaultVersion)
50+
* )
51+
*
52+
* // Fast: 1 encode/decode cycle
53+
* Rules.batch(ops, b -> b
54+
* .rename("playerName", "name")
55+
* .rename("xp", "experience")
56+
* .remove("deprecated")
57+
* .set("version", d -> d.createInt(2))
58+
* )
59+
* }</pre>
60+
*
61+
* <h2>Supported Operations</h2>
62+
* <ul>
63+
* <li>{@link #rename(String, String)} - Rename a field</li>
64+
* <li>{@link #remove(String)} - Remove a field</li>
65+
* <li>{@link #set(String, Function)} - Set a field value (computed)</li>
66+
* <li>{@link #setStatic(String, Dynamic)} - Set a field to a static value</li>
67+
* <li>{@link #transform(String, Function)} - Transform an existing field value</li>
68+
* <li>{@link #addIfMissing(String, Function)} - Add field only if missing</li>
69+
* </ul>
70+
*
71+
* <h2>Thread Safety</h2>
72+
* <p>This class is not thread-safe during construction. Once built into a
73+
* {@link TypeRewriteRule} via {@link Rules#batch}, the resulting rule is thread-safe.</p>
74+
*
75+
* @param <T> the underlying data format type (e.g., JsonElement)
76+
* @author Erik Pförtner
77+
* @see Rules#batch(DynamicOps, java.util.function.Consumer)
78+
* @since 0.3.0
79+
*/
80+
public final class BatchTransform<T> {
81+
82+
private final List<FieldOperation<T>> operations = new ArrayList<>();
83+
private final DynamicOps<T> ops;
84+
85+
/**
86+
* Creates a new batch transform builder.
87+
*
88+
* @param ops the dynamic operations for the data format, must not be {@code null}
89+
*/
90+
public BatchTransform(@NotNull final DynamicOps<T> ops) {
91+
Preconditions.checkNotNull(ops, "DynamicOps<T> ops must not be null");
92+
this.ops = ops;
93+
}
94+
95+
/**
96+
* Adds a rename operation to the batch.
97+
*
98+
* <p>If the source field doesn't exist, this operation is a no-op for that field.</p>
99+
*
100+
* @param from the current field name, must not be {@code null}
101+
* @param to the new field name, must not be {@code null}
102+
* @return this builder for chaining
103+
* @throws NullPointerException if any argument is {@code null}
104+
*/
105+
@NotNull
106+
public BatchTransform<T> rename(@NotNull final String from, @NotNull final String to) {
107+
Preconditions.checkNotNull(from, "String from must not be null");
108+
Preconditions.checkNotNull(to, "String to must not be null");
109+
operations.add(new RenameOp<>(from, to));
110+
return this;
111+
}
112+
113+
/**
114+
* Adds a remove operation to the batch.
115+
*
116+
* <p>If the field doesn't exist, this operation is a no-op.</p>
117+
*
118+
* @param field the field name to remove, must not be {@code null}
119+
* @return this builder for chaining
120+
* @throws NullPointerException if field is {@code null}
121+
*/
122+
@NotNull
123+
public BatchTransform<T> remove(@NotNull final String field) {
124+
Preconditions.checkNotNull(field, "String field must not be null");
125+
operations.add(new RemoveOp<>(field));
126+
return this;
127+
}
128+
129+
/**
130+
* Adds a set operation to the batch with a computed value.
131+
*
132+
* <p>The value function receives the current Dynamic and should return
133+
* the value to set. This always overwrites any existing value.</p>
134+
*
135+
* @param field the field name to set, must not be {@code null}
136+
* @param valueSupplier function that computes the value from the current dynamic, must not be {@code null}
137+
* @return this builder for chaining
138+
* @throws NullPointerException if any argument is {@code null}
139+
*/
140+
@NotNull
141+
public BatchTransform<T> set(@NotNull final String field,
142+
@NotNull final Function<Dynamic<T>, Dynamic<T>> valueSupplier) {
143+
Preconditions.checkNotNull(field, "String field must not be null");
144+
Preconditions.checkNotNull(valueSupplier, "Function valueSupplier must not be null");
145+
operations.add(new SetOp<>(field, valueSupplier));
146+
return this;
147+
}
148+
149+
/**
150+
* Adds a set operation to the batch with a static value.
151+
*
152+
* <p>This is a convenience method when the value doesn't depend on the current dynamic.</p>
153+
*
154+
* @param field the field name to set, must not be {@code null}
155+
* @param value the value to set, must not be {@code null}
156+
* @return this builder for chaining
157+
* @throws NullPointerException if any argument is {@code null}
158+
*/
159+
@NotNull
160+
public BatchTransform<T> setStatic(@NotNull final String field,
161+
@NotNull final Dynamic<T> value) {
162+
Preconditions.checkNotNull(field, "String field must not be null");
163+
Preconditions.checkNotNull(value, "Dynamic<T> value must not be null");
164+
operations.add(new SetOp<>(field, d -> value));
165+
return this;
166+
}
167+
168+
/**
169+
* Adds a transform operation to the batch.
170+
*
171+
* <p>The transform function receives the current field value and should return
172+
* the new value. If the field doesn't exist, this operation is a no-op.</p>
173+
*
174+
* @param field the field name to transform, must not be {@code null}
175+
* @param transform the transformation function, must not be {@code null}
176+
* @return this builder for chaining
177+
* @throws NullPointerException if any argument is {@code null}
178+
*/
179+
@NotNull
180+
public BatchTransform<T> transform(@NotNull final String field,
181+
@NotNull final Function<Dynamic<T>, Dynamic<T>> transform) {
182+
Preconditions.checkNotNull(field, "String field must not be null");
183+
Preconditions.checkNotNull(transform, "Function transform must not be null");
184+
operations.add(new TransformOp<>(field, transform));
185+
return this;
186+
}
187+
188+
/**
189+
* Adds an operation that sets a field only if it doesn't exist.
190+
*
191+
* <p>If the field already exists, this operation is a no-op.
192+
* Equivalent to {@code Rules.addField}.</p>
193+
*
194+
* @param field the field name, must not be {@code null}
195+
* @param valueSupplier function that computes the default value, must not be {@code null}
196+
* @return this builder for chaining
197+
* @throws NullPointerException if any argument is {@code null}
198+
*/
199+
@NotNull
200+
public BatchTransform<T> addIfMissing(@NotNull final String field,
201+
@NotNull final Function<Dynamic<T>, Dynamic<T>> valueSupplier) {
202+
Preconditions.checkNotNull(field, "String field must not be null");
203+
Preconditions.checkNotNull(valueSupplier, "Function valueSupplier must not be null");
204+
operations.add(new AddIfMissingOp<>(field, valueSupplier));
205+
return this;
206+
}
207+
208+
/**
209+
* Adds an operation that sets a field only if it doesn't exist (static value).
210+
*
211+
* @param field the field name, must not be {@code null}
212+
* @param value the default value, must not be {@code null}
213+
* @return this builder for chaining
214+
* @throws NullPointerException if any argument is {@code null}
215+
*/
216+
@NotNull
217+
public BatchTransform<T> addIfMissingStatic(@NotNull final String field,
218+
@NotNull final Dynamic<T> value) {
219+
Preconditions.checkNotNull(field, "String field must not be null");
220+
Preconditions.checkNotNull(value, "Dynamic<T> value must not be null");
221+
operations.add(new AddIfMissingOp<>(field, d -> value));
222+
return this;
223+
}
224+
225+
/**
226+
* Applies all batched operations to the input dynamic in a single pass.
227+
*
228+
* <p>Operations are applied in the order they were added.</p>
229+
*
230+
* @param input the input dynamic, must not be {@code null}
231+
* @return the transformed dynamic, never {@code null}
232+
* @throws NullPointerException if input is {@code null}
233+
*/
234+
@NotNull
235+
public Dynamic<T> apply(@NotNull final Dynamic<T> input) {
236+
Preconditions.checkNotNull(input, "Dynamic<T> input must not be null");
237+
238+
Dynamic<T> result = input;
239+
for (final FieldOperation<T> op : operations) {
240+
result = op.apply(result);
241+
}
242+
return result;
243+
}
244+
245+
/**
246+
* Returns the number of operations in this batch.
247+
*
248+
* @return the operation count
249+
*/
250+
public int size() {
251+
return operations.size();
252+
}
253+
254+
/**
255+
* Returns whether this batch has any operations.
256+
*
257+
* @return {@code true} if empty, {@code false} otherwise
258+
*/
259+
public boolean isEmpty() {
260+
return operations.isEmpty();
261+
}
262+
263+
// ==================== Internal Operation Classes ====================
264+
265+
/**
266+
* Base interface for field operations.
267+
*/
268+
private interface FieldOperation<T> {
269+
@NotNull
270+
Dynamic<T> apply(@NotNull Dynamic<T> dynamic);
271+
}
272+
273+
/**
274+
* Rename operation: moves value from one field to another.
275+
*/
276+
private record RenameOp<T>(String from, String to) implements FieldOperation<T> {
277+
@Override
278+
@NotNull
279+
public Dynamic<T> apply(@NotNull final Dynamic<T> dynamic) {
280+
final Dynamic<T> value = dynamic.get(from);
281+
if (value == null) {
282+
return dynamic;
283+
}
284+
return dynamic.remove(from).set(to, value);
285+
}
286+
}
287+
288+
/**
289+
* Remove operation: removes a field.
290+
*/
291+
private record RemoveOp<T>(String field) implements FieldOperation<T> {
292+
@Override
293+
@NotNull
294+
public Dynamic<T> apply(@NotNull final Dynamic<T> dynamic) {
295+
return dynamic.remove(field);
296+
}
297+
}
298+
299+
/**
300+
* Set operation: sets a field to a computed value (overwrites existing).
301+
*/
302+
private record SetOp<T>(String field, Function<Dynamic<T>, Dynamic<T>> valueSupplier) implements FieldOperation<T> {
303+
@Override
304+
@NotNull
305+
public Dynamic<T> apply(@NotNull final Dynamic<T> dynamic) {
306+
return dynamic.set(field, valueSupplier.apply(dynamic));
307+
}
308+
}
309+
310+
/**
311+
* Transform operation: transforms an existing field value.
312+
*/
313+
private record TransformOp<T>(String field, Function<Dynamic<T>, Dynamic<T>> transform) implements FieldOperation<T> {
314+
@Override
315+
@NotNull
316+
public Dynamic<T> apply(@NotNull final Dynamic<T> dynamic) {
317+
final Dynamic<T> value = dynamic.get(field);
318+
if (value == null) {
319+
return dynamic;
320+
}
321+
return dynamic.set(field, transform.apply(value));
322+
}
323+
}
324+
325+
/**
326+
* Add if missing operation: sets a field only if it doesn't exist.
327+
*/
328+
private record AddIfMissingOp<T>(String field, Function<Dynamic<T>, Dynamic<T>> valueSupplier) implements FieldOperation<T> {
329+
@Override
330+
@NotNull
331+
public Dynamic<T> apply(@NotNull final Dynamic<T> dynamic) {
332+
if (dynamic.get(field) != null) {
333+
return dynamic;
334+
}
335+
return dynamic.set(field, valueSupplier.apply(dynamic));
336+
}
337+
}
338+
}

0 commit comments

Comments
 (0)