diff --git a/aether-datafixers-api/src/main/java/de/splatgames/aether/datafixers/api/codec/Codecs.java b/aether-datafixers-api/src/main/java/de/splatgames/aether/datafixers/api/codec/Codecs.java index 45d75eb..b308c5c 100644 --- a/aether-datafixers-api/src/main/java/de/splatgames/aether/datafixers/api/codec/Codecs.java +++ b/aether-datafixers-api/src/main/java/de/splatgames/aether/datafixers/api/codec/Codecs.java @@ -31,7 +31,6 @@ import java.util.ArrayList; import java.util.List; -import java.util.Objects; import java.util.Optional; /** @@ -766,8 +765,8 @@ public DataResult encode(@NotNull final Pair input, Preconditions.checkNotNull(input, "input must not be null"); Preconditions.checkNotNull(ops, "ops must not be null"); Preconditions.checkNotNull(prefix, "prefix must not be null"); - return first.encode(Objects.requireNonNull(input.first()), ops, prefix) - .flatMap(t -> second.encode(Objects.requireNonNull(input.second()), ops, t)); + return first.encode(Preconditions.checkNotNull(input.first(), "pair first must not be null"), ops, prefix) + .flatMap(t -> second.encode(Preconditions.checkNotNull(input.second(), "pair second must not be null"), ops, t)); } @NotNull @@ -777,13 +776,13 @@ public DataResult, T>> decode(@NotNull final DynamicOps o Preconditions.checkNotNull(ops, "ops must not be null"); Preconditions.checkNotNull(input, "input must not be null"); return first.decode(ops, input).flatMap(p1 -> - second.decode(ops, Objects.requireNonNull(p1.second())).map(p2 -> + second.decode(ops, Preconditions.checkNotNull(p1.second(), "first decode remainder must not be null")).map(p2 -> Pair.of( Pair.of( - Objects.requireNonNull(p1.first()), - Objects.requireNonNull(p2.first()) + Preconditions.checkNotNull(p1.first(), "first decode value must not be null"), + Preconditions.checkNotNull(p2.first(), "second decode value must not be null") ), - Objects.requireNonNull(p2.second()) + Preconditions.checkNotNull(p2.second(), "second decode remainder must not be null") ) ) ); diff --git a/aether-datafixers-api/src/main/java/de/splatgames/aether/datafixers/api/codec/RecordCodecBuilder.java b/aether-datafixers-api/src/main/java/de/splatgames/aether/datafixers/api/codec/RecordCodecBuilder.java index 2a4ee4b..f6466b0 100644 --- a/aether-datafixers-api/src/main/java/de/splatgames/aether/datafixers/api/codec/RecordCodecBuilder.java +++ b/aether-datafixers-api/src/main/java/de/splatgames/aether/datafixers/api/codec/RecordCodecBuilder.java @@ -79,9 +79,9 @@ * } * *

Supported Field Counts

- *

The builder supports records with 1 to 6 fields via the corresponding - * {@link Builder1} through {@link Builder6} classes. For records with more fields, - * consider using nested records or custom codec implementations.

+ *

The builder supports records with 1 to 8 fields via the corresponding + * {@link Builder1} through {@link Builder8} classes. For records with more than + * 8 fields, consider using nested records or custom codec implementations.

* *

Thread Safety

*

The {@link #create(Function)} method and resulting codecs are thread-safe.

@@ -273,6 +273,66 @@ public interface Function6 { R apply(A a, B b, C c, D d, E e, F f); } + /** + * A function that takes seven arguments and produces a result. + * + * @param the type of the first argument + * @param the type of the second argument + * @param the type of the third argument + * @param the type of the fourth argument + * @param the type of the fifth argument + * @param the type of the sixth argument + * @param the type of the seventh argument + * @param the type of the result + */ + @FunctionalInterface + public interface Function7 { + /** + * Applies this function to the given arguments. + * + * @param a the first argument + * @param b the second argument + * @param c the third argument + * @param d the fourth argument + * @param e the fifth argument + * @param f the sixth argument + * @param g the seventh argument + * @return the function result + */ + R apply(A a, B b, C c, D d, E e, F f, G g); + } + + /** + * A function that takes eight arguments and produces a result. + * + * @param the type of the first argument + * @param the type of the second argument + * @param the type of the third argument + * @param the type of the fourth argument + * @param the type of the fifth argument + * @param the type of the sixth argument + * @param the type of the seventh argument + * @param the type of the eighth argument + * @param the type of the result + */ + @FunctionalInterface + public interface Function8 { + /** + * Applies this function to the given arguments. + * + * @param a the first argument + * @param b the second argument + * @param c the third argument + * @param d the fourth argument + * @param e the fifth argument + * @param f the sixth argument + * @param g the seventh argument + * @param h the eighth argument + * @return the function result + */ + R apply(A a, B b, C c, D d, E e, F f, G g, H h); + } + /** * A field definition pairing a {@link MapCodec} with a getter function. * @@ -485,6 +545,66 @@ public Builder6 group(@NotNull final Fie return new Builder6<>(f1, f2, f3, f4, f5, f6); } + /** + * Groups seven fields into a builder. + * + * @param f1 the first field + * @param f2 the second field + * @param f3 the third field + * @param f4 the fourth field + * @param f5 the fifth field + * @param f6 the sixth field + * @param f7 the seventh field + * @param the type of the first field + * @param the type of the second field + * @param the type of the third field + * @param the type of the fourth field + * @param the type of the fifth field + * @param the type of the sixth field + * @param the type of the seventh field + * @return a {@link Builder7} for applying a constructor, never {@code null} + */ + @NotNull + public Builder7 group( + @NotNull final Field f1, @NotNull final Field f2, + @NotNull final Field f3, @NotNull final Field f4, + @NotNull final Field f5, @NotNull final Field f6, + @NotNull final Field f7 + ) { + return new Builder7<>(f1, f2, f3, f4, f5, f6, f7); + } + + /** + * Groups eight fields into a builder. + * + * @param f1 the first field + * @param f2 the second field + * @param f3 the third field + * @param f4 the fourth field + * @param f5 the fifth field + * @param f6 the sixth field + * @param f7 the seventh field + * @param f8 the eighth field + * @param the type of the first field + * @param the type of the second field + * @param the type of the third field + * @param the type of the fourth field + * @param the type of the fifth field + * @param the type of the sixth field + * @param the type of the seventh field + * @param the type of the eighth field + * @return a {@link Builder8} for applying a constructor, never {@code null} + */ + @NotNull + public Builder8 group( + @NotNull final Field f1, @NotNull final Field f2, + @NotNull final Field f3, @NotNull final Field f4, + @NotNull final Field f5, @NotNull final Field f6, + @NotNull final Field f7, @NotNull final Field f8 + ) { + return new Builder8<>(f1, f2, f3, f4, f5, f6, f7, f8); + } + /** * Creates a constant value codec in the applicative context. * @@ -1010,4 +1130,220 @@ private record Tuple4(A a, B b, C c, D d) { */ private record Tuple5(A a, B b, C c, D d, E e) { } + + /** + * Internal tuple for accumulating 6 decoded values before applying the constructor. + */ + private record Tuple6(A a, B b, C c, D d, E e, F f) { + } + + /** + * Internal tuple for accumulating 7 decoded values before applying the constructor. + */ + private record Tuple7(A a, B b, C c, D d, E e, F f, G g) { + } + + // ==================== Builder7 ==================== + + /** + * Intermediate builder holding seven grouped fields for record codec construction. + * + *

Created by {@link Instance#group(Field, Field, Field, Field, Field, Field, Field)}. + * Call {@link #apply} with a seven-argument constructor to produce the final codec.

+ * + * @param f1 the first field + * @param f2 the second field + * @param f3 the third field + * @param f4 the fourth field + * @param f5 the fifth field + * @param f6 the sixth field + * @param f7 the seventh field + * @param the record type + * @param
the type of the first field + * @param the type of the second field + * @param the type of the third field + * @param the type of the fourth field + * @param the type of the fifth field + * @param the type of the sixth field + * @param the type of the seventh field + */ + public record Builder7( + @NotNull Field f1, @NotNull Field f2, + @NotNull Field f3, @NotNull Field f4, + @NotNull Field f5, @NotNull Field f6, + @NotNull Field f7 + ) { + /** + * Creates a new Builder7 with validation. + * + * @throws NullPointerException if any field is {@code null} + */ + public Builder7 { + Preconditions.checkNotNull(f1, "f1 must not be null"); + Preconditions.checkNotNull(f2, "f2 must not be null"); + Preconditions.checkNotNull(f3, "f3 must not be null"); + Preconditions.checkNotNull(f4, "f4 must not be null"); + Preconditions.checkNotNull(f5, "f5 must not be null"); + Preconditions.checkNotNull(f6, "f6 must not be null"); + Preconditions.checkNotNull(f7, "f7 must not be null"); + } + + /** + * Applies a constructor function to create the final codec. + * + * @param instance the builder instance (for type inference) + * @param constructor the function to construct the record from the field values + * @return a {@link MapCodec} for the record type, never {@code null} + */ + @NotNull + public MapCodec apply(@NotNull final Instance instance, + @NotNull final Function7 constructor) { + Preconditions.checkNotNull(instance, "instance must not be null"); + Preconditions.checkNotNull(constructor, "constructor must not be null"); + return new MapCodec<>() { + @NotNull + @Override + public DataResult encode(@NotNull final O input, + @NotNull final DynamicOps ops, + @NotNull final T map) { + Preconditions.checkNotNull(input, "input must not be null"); + Preconditions.checkNotNull(ops, "ops must not be null"); + Preconditions.checkNotNull(map, "map must not be null"); + return f1.codec.encode(f1.getter.apply(input), ops, map) + .flatMap(m -> f2.codec.encode(f2.getter.apply(input), ops, m)) + .flatMap(m -> f3.codec.encode(f3.getter.apply(input), ops, m)) + .flatMap(m -> f4.codec.encode(f4.getter.apply(input), ops, m)) + .flatMap(m -> f5.codec.encode(f5.getter.apply(input), ops, m)) + .flatMap(m -> f6.codec.encode(f6.getter.apply(input), ops, m)) + .flatMap(m -> f7.codec.encode(f7.getter.apply(input), ops, m)); + } + + @NotNull + @Override + public DataResult decode(@NotNull final DynamicOps ops, + @NotNull final T input) { + Preconditions.checkNotNull(ops, "ops must not be null"); + Preconditions.checkNotNull(input, "input must not be null"); + final DataResult a = f1.codec.decode(ops, input); + final DataResult b = f2.codec.decode(ops, input); + final DataResult c = f3.codec.decode(ops, input); + final DataResult d = f4.codec.decode(ops, input); + final DataResult e = f5.codec.decode(ops, input); + final DataResult f = f6.codec.decode(ops, input); + final DataResult g = f7.codec.decode(ops, input); + return a.apply2(b, Pair::of) + .apply2(c, (ab, cv) -> new Tuple3<>(ab.first(), ab.second(), cv)) + .apply2(d, (abc, dv) -> new Tuple4<>(abc.a, abc.b, abc.c, dv)) + .apply2(e, (abcd, ev) -> new Tuple5<>(abcd.a, abcd.b, abcd.c, abcd.d, ev)) + .apply2(f, (abcde, fv) -> new Tuple6<>(abcde.a, abcde.b, abcde.c, abcde.d, abcde.e, fv)) + .apply2(g, (abcdef, gv) -> constructor.apply(abcdef.a, abcdef.b, abcdef.c, abcdef.d, abcdef.e, abcdef.f, gv)); + } + }; + } + } + + // ==================== Builder8 ==================== + + /** + * Intermediate builder holding eight grouped fields for record codec construction. + * + *

Created by {@link Instance#group(Field, Field, Field, Field, Field, Field, Field, Field)}. + * Call {@link #apply} with an eight-argument constructor to produce the final codec.

+ * + * @param f1 the first field + * @param f2 the second field + * @param f3 the third field + * @param f4 the fourth field + * @param f5 the fifth field + * @param f6 the sixth field + * @param f7 the seventh field + * @param f8 the eighth field + * @param the record type + * @param
the type of the first field + * @param the type of the second field + * @param the type of the third field + * @param the type of the fourth field + * @param the type of the fifth field + * @param the type of the sixth field + * @param the type of the seventh field + * @param the type of the eighth field + */ + public record Builder8( + @NotNull Field f1, @NotNull Field f2, + @NotNull Field f3, @NotNull Field f4, + @NotNull Field f5, @NotNull Field f6, + @NotNull Field f7, @NotNull Field f8 + ) { + /** + * Creates a new Builder8 with validation. + * + * @throws NullPointerException if any field is {@code null} + */ + public Builder8 { + Preconditions.checkNotNull(f1, "f1 must not be null"); + Preconditions.checkNotNull(f2, "f2 must not be null"); + Preconditions.checkNotNull(f3, "f3 must not be null"); + Preconditions.checkNotNull(f4, "f4 must not be null"); + Preconditions.checkNotNull(f5, "f5 must not be null"); + Preconditions.checkNotNull(f6, "f6 must not be null"); + Preconditions.checkNotNull(f7, "f7 must not be null"); + Preconditions.checkNotNull(f8, "f8 must not be null"); + } + + /** + * Applies a constructor function to create the final codec. + * + * @param instance the builder instance (for type inference) + * @param constructor the function to construct the record from the field values + * @return a {@link MapCodec} for the record type, never {@code null} + */ + @NotNull + public MapCodec apply(@NotNull final Instance instance, + @NotNull final Function8 constructor) { + Preconditions.checkNotNull(instance, "instance must not be null"); + Preconditions.checkNotNull(constructor, "constructor must not be null"); + return new MapCodec<>() { + @NotNull + @Override + public DataResult encode(@NotNull final O input, + @NotNull final DynamicOps ops, + @NotNull final T map) { + Preconditions.checkNotNull(input, "input must not be null"); + Preconditions.checkNotNull(ops, "ops must not be null"); + Preconditions.checkNotNull(map, "map must not be null"); + return f1.codec.encode(f1.getter.apply(input), ops, map) + .flatMap(m -> f2.codec.encode(f2.getter.apply(input), ops, m)) + .flatMap(m -> f3.codec.encode(f3.getter.apply(input), ops, m)) + .flatMap(m -> f4.codec.encode(f4.getter.apply(input), ops, m)) + .flatMap(m -> f5.codec.encode(f5.getter.apply(input), ops, m)) + .flatMap(m -> f6.codec.encode(f6.getter.apply(input), ops, m)) + .flatMap(m -> f7.codec.encode(f7.getter.apply(input), ops, m)) + .flatMap(m -> f8.codec.encode(f8.getter.apply(input), ops, m)); + } + + @NotNull + @Override + public DataResult decode(@NotNull final DynamicOps ops, + @NotNull final T input) { + Preconditions.checkNotNull(ops, "ops must not be null"); + Preconditions.checkNotNull(input, "input must not be null"); + final DataResult a = f1.codec.decode(ops, input); + final DataResult b = f2.codec.decode(ops, input); + final DataResult c = f3.codec.decode(ops, input); + final DataResult d = f4.codec.decode(ops, input); + final DataResult e = f5.codec.decode(ops, input); + final DataResult f = f6.codec.decode(ops, input); + final DataResult g = f7.codec.decode(ops, input); + final DataResult h = f8.codec.decode(ops, input); + return a.apply2(b, Pair::of) + .apply2(c, (ab, cv) -> new Tuple3<>(ab.first(), ab.second(), cv)) + .apply2(d, (abc, dv) -> new Tuple4<>(abc.a, abc.b, abc.c, dv)) + .apply2(e, (abcd, ev) -> new Tuple5<>(abcd.a, abcd.b, abcd.c, abcd.d, ev)) + .apply2(f, (abcde, fv) -> new Tuple6<>(abcde.a, abcde.b, abcde.c, abcde.d, abcde.e, fv)) + .apply2(g, (abcdef, gv) -> new Tuple7<>(abcdef.a, abcdef.b, abcdef.c, abcdef.d, abcdef.e, abcdef.f, gv)) + .apply2(h, (abcdefg, hv) -> constructor.apply(abcdefg.a, abcdefg.b, abcdefg.c, abcdefg.d, abcdefg.e, abcdefg.f, abcdefg.g, hv)); + } + }; + } + } } diff --git a/aether-datafixers-api/src/main/java/de/splatgames/aether/datafixers/api/diagnostic/FixExecution.java b/aether-datafixers-api/src/main/java/de/splatgames/aether/datafixers/api/diagnostic/FixExecution.java index 7c24420..44f24cf 100644 --- a/aether-datafixers-api/src/main/java/de/splatgames/aether/datafixers/api/diagnostic/FixExecution.java +++ b/aether-datafixers-api/src/main/java/de/splatgames/aether/datafixers/api/diagnostic/FixExecution.java @@ -59,8 +59,10 @@ * @param startTime the instant when the fix started * @param duration the total time taken to apply the fix * @param ruleApplications list of individual rule applications within this fix - * @param beforeSnapshot optional snapshot of data before the fix was applied - * @param afterSnapshot optional snapshot of data after the fix was applied + * @param beforeSnapshot snapshot of data before the fix was applied, or {@code null} + * if snapshot capture was disabled in {@link DiagnosticOptions} + * @param afterSnapshot snapshot of data after the fix was applied, or {@code null} + * if snapshot capture was disabled or the fix failed * @author Erik Pförtner * @see RuleApplication * @see MigrationReport diff --git a/aether-datafixers-api/src/main/java/de/splatgames/aether/datafixers/api/exception/DataFixerException.java b/aether-datafixers-api/src/main/java/de/splatgames/aether/datafixers/api/exception/DataFixerException.java index 4c542a2..f8d5ed8 100644 --- a/aether-datafixers-api/src/main/java/de/splatgames/aether/datafixers/api/exception/DataFixerException.java +++ b/aether-datafixers-api/src/main/java/de/splatgames/aether/datafixers/api/exception/DataFixerException.java @@ -25,6 +25,8 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import java.io.Serial; + /** * Base exception class for all data fixer related errors. * @@ -74,6 +76,15 @@ */ public class DataFixerException extends RuntimeException { + /** + * Serial version UID for serialization compatibility. + */ + @Serial + private static final long serialVersionUID = 2074356976574888083L; + + /** + * Optional context information about where the error occurred (e.g., field path, type name). + */ @Nullable private final String context; @@ -145,9 +156,10 @@ public String getContext() { */ @Override public String toString() { + final String base = getClass().getSimpleName() + ": " + getMessage(); if (this.context != null) { - return super.toString() + " [context: " + this.context + "]"; + return base + " [context: " + this.context + "]"; } - return super.toString(); + return base; } } diff --git a/aether-datafixers-api/src/main/java/de/splatgames/aether/datafixers/api/util/Unit.java b/aether-datafixers-api/src/main/java/de/splatgames/aether/datafixers/api/util/Unit.java index 981383f..6a4feeb 100644 --- a/aether-datafixers-api/src/main/java/de/splatgames/aether/datafixers/api/util/Unit.java +++ b/aether-datafixers-api/src/main/java/de/splatgames/aether/datafixers/api/util/Unit.java @@ -24,6 +24,9 @@ import org.jetbrains.annotations.NotNull; +import java.io.Serial; +import java.io.Serializable; + /** * A singleton type representing the absence of a meaningful value. * @@ -65,7 +68,7 @@ * @see de.splatgames.aether.datafixers.api.result.DataResult * @since 0.1.0 */ -public final class Unit { +public final class Unit implements Serializable { /** * The singleton instance of Unit. @@ -76,6 +79,15 @@ public final class Unit { @NotNull public static final Unit INSTANCE = new Unit(); + /** + * Serial version UID for serialization compatibility. + * + *

This value is arbitrary but should be changed if the class structure changes in a way that affects + * serialization.

+ */ + @Serial + private static final long serialVersionUID = -7628105959309438762L; + /** * Private constructor to prevent external instantiation. * @@ -120,4 +132,14 @@ public int hashCode() { public boolean equals(final Object obj) { return obj instanceof Unit; } + + /** + * Preserves the singleton guarantee during deserialization. + * + * @return {@link #INSTANCE} + */ + @Serial + private Object readResolve() { + return INSTANCE; + } } diff --git a/aether-datafixers-testkit/src/main/java/de/splatgames/aether/datafixers/testkit/TestDataBuilder.java b/aether-datafixers-testkit/src/main/java/de/splatgames/aether/datafixers/testkit/TestDataBuilder.java index e29bf68..5501516 100644 --- a/aether-datafixers-testkit/src/main/java/de/splatgames/aether/datafixers/testkit/TestDataBuilder.java +++ b/aether-datafixers-testkit/src/main/java/de/splatgames/aether/datafixers/testkit/TestDataBuilder.java @@ -37,7 +37,9 @@ * A fluent builder for creating {@link Dynamic} objects. * *

This builder provides a clean, readable API for constructing test data - * without the boilerplate of manual JSON construction. It supports primitives, + * without the boilerplate of manual JSON construction. Each builder instance should + * only be used for a single {@link #build()} call — create a new instance for each + * Dynamic value. It supports primitives, * nested objects, and lists through method chaining.

* *

Basic Usage

diff --git a/aether-datafixers-testkit/src/main/java/de/splatgames/aether/datafixers/testkit/TestDataListBuilder.java b/aether-datafixers-testkit/src/main/java/de/splatgames/aether/datafixers/testkit/TestDataListBuilder.java index 560b493..cddde51 100644 --- a/aether-datafixers-testkit/src/main/java/de/splatgames/aether/datafixers/testkit/TestDataListBuilder.java +++ b/aether-datafixers-testkit/src/main/java/de/splatgames/aether/datafixers/testkit/TestDataListBuilder.java @@ -320,6 +320,22 @@ public TestDataListBuilder addAll(final double... values) { return this; } + /** + * Adds multiple float elements to the list. + * + * @param values the float values + * @return this builder for chaining + * @throws NullPointerException if {@code values} is null + */ + @NotNull + public TestDataListBuilder addAll(final float... values) { + Preconditions.checkNotNull(values, "values must not be null"); + for (final float value : values) { + this.add(value); + } + return this; + } + /** * Adds multiple boolean elements to the list. *