Skip to content

Commit 0e8d115

Browse files
authored
Merge pull request #223 from aether-framework/bugfix/117-118-119-120-121-122-123-api-bugfixes
Remove unused methods and improve functionality in core classes
2 parents f697089 + 56f8352 commit 0e8d115

11 files changed

Lines changed: 222 additions & 56 deletions

File tree

aether-datafixers-api/src/main/java/de/splatgames/aether/datafixers/api/dynamic/TaggedDynamic.java

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,4 +179,68 @@ public TypeReference type() {
179179
public Dynamic<?> value() {
180180
return this.value;
181181
}
182+
183+
/**
184+
* Indicates whether some other object is "equal to" this one.
185+
*
186+
* <p>Two {@code TaggedDynamic} instances are considered equal if they have the same type reference and the same
187+
* dynamic value.
188+
* This means that both the type and the data must match for two tagged dynamics to be considered equal.</p>
189+
*
190+
* <h4>Example</h4>
191+
* <pre>{@code
192+
* TaggedDynamic tagged1 = new TaggedDynamic(typeRef, dynamicValue);
193+
* TaggedDynamic tagged2 = new TaggedDynamic(typeRef, dynamicValue);
194+
*
195+
* // tagged1 and tagged2 are equal because they have the same type and value
196+
* boolean isEqual = tagged1.equals(tagged2); // true
197+
* }</pre>
198+
*
199+
* @param obj the reference object with which to compare
200+
* @return {@code true} if this object is the same as the obj argument; {@code false} otherwise
201+
*/
202+
@Override
203+
public boolean equals(final Object obj) {
204+
if (this == obj) {
205+
return true;
206+
}
207+
if (obj == null || getClass() != obj.getClass()) {
208+
return false;
209+
}
210+
211+
final TaggedDynamic that = (TaggedDynamic) obj;
212+
return this.type.equals(that.type) && this.value.equals(that.value);
213+
}
214+
215+
/**
216+
* Returns a hash code value for the object.
217+
*
218+
* <p>The hash code is computed based on both the type reference and the dynamic value. This ensures that equal
219+
* {@code TaggedDynamic} instances will have the same hash code, which is important for correct behavior in
220+
* hash-based collections.</p>
221+
*
222+
* @return a hash code value for this object
223+
*/
224+
@Override
225+
public int hashCode() {
226+
int result = this.type.hashCode();
227+
result = 31 * result + this.value.hashCode();
228+
return result;
229+
}
230+
231+
/**
232+
* Returns a string representation of the object.
233+
*
234+
* <p>The string representation includes the type reference and the dynamic value, providing a human-readable
235+
* summary of the tagged dynamic's contents. This can be useful for debugging and logging purposes.</p>
236+
*
237+
* @return a string representation of the object
238+
*/
239+
@Override
240+
public String toString() {
241+
return "TaggedDynamic{" +
242+
"type=" + this.type +
243+
", value=" + this.value +
244+
'}';
245+
}
182246
}

aether-datafixers-api/src/main/java/de/splatgames/aether/datafixers/api/fix/Fixes.java

Lines changed: 35 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ private Fixes() {
121121
* @param rewrite the function that transforms the typed value; must not be {@code null}
122122
* @return a type rewrite rule that applies the transformation to matching types; never {@code null}
123123
* @throws NullPointerException if any parameter is {@code null}
124-
* @see #fixTypeEverywhere(String, Type, Function)
124+
* @see #fixTypeEverywhere(String, DynamicOps, Type, Function)
125125
*/
126126
@NotNull
127127
public static TypeRewriteRule fixTypeEverywhereTyped(@NotNull final String name,
@@ -156,22 +156,44 @@ public String toString() {
156156
/**
157157
* Creates a rule that transforms the dynamic representation of a type.
158158
*
159-
* @param name the fix name
160-
* @param type the type to transform
161-
* @param rewrite the dynamic transformation function
162-
* @return a type rewrite rule
159+
* <p>This method creates a {@link TypeRewriteRule} that matches values of the specified
160+
* type and transforms them by encoding to {@link Dynamic}, applying the rewrite function, and decoding back. The
161+
* rule only matches types with the same {@link de.splatgames.aether.datafixers.api.TypeReference} as the target
162+
* type.</p>
163+
*
164+
* <h4>Example</h4>
165+
* <pre>{@code
166+
* TypeRewriteRule rule = Fixes.fixTypeEverywhere(
167+
* "addTimestamp",
168+
* GsonOps.INSTANCE,
169+
* playerType,
170+
* dynamic -> dynamic.set("timestamp", dynamic.createLong(0L))
171+
* );
172+
* }</pre>
173+
*
174+
* @param <T> the dynamic type
175+
* @param name the fix name; must not be {@code null}
176+
* @param ops the dynamic ops for encoding/decoding; must not be {@code null}
177+
* @param type the type to transform; must not be {@code null}
178+
* @param rewrite the dynamic transformation function; must not be {@code null}
179+
* @return a type rewrite rule; never {@code null}
180+
* @throws NullPointerException if any parameter is {@code null}
181+
* @see #fixTypeEverywhereTyped(String, Type, Type, Function)
163182
*/
164183
@NotNull
165-
public static TypeRewriteRule fixTypeEverywhere(@NotNull final String name,
166-
@NotNull final Type<?> type,
167-
@NotNull final Function<Dynamic<?>, Dynamic<?>> rewrite) {
184+
public static <T> TypeRewriteRule fixTypeEverywhere(@NotNull final String name,
185+
@NotNull final DynamicOps<T> ops,
186+
@NotNull final Type<?> type,
187+
@NotNull final Function<Dynamic<?>, Dynamic<?>> rewrite) {
168188
Preconditions.checkNotNull(name, "name must not be null");
189+
Preconditions.checkNotNull(ops, "ops must not be null");
169190
Preconditions.checkNotNull(type, "type must not be null");
170191
Preconditions.checkNotNull(rewrite, "rewrite must not be null");
171192

172193
return new TypeRewriteRule() {
173194
@NotNull
174195
@Override
196+
@SuppressWarnings({"unchecked", "rawtypes"})
175197
public Optional<Typed<?>> rewrite(@NotNull final Type<?> inputType,
176198
@NotNull final Typed<?> input) {
177199
Preconditions.checkNotNull(inputType, "inputType must not be null");
@@ -180,9 +202,11 @@ public Optional<Typed<?>> rewrite(@NotNull final Type<?> inputType,
180202
return Optional.empty();
181203
}
182204

183-
// We need to get DynamicOps from somewhere - this requires the caller to provide it
184-
// For now, we'll use a simplified approach
185-
return Optional.of(input);
205+
final DataResult<Dynamic<T>> encodeResult = input.encode(ops);
206+
return encodeResult.flatMap(dynamic -> {
207+
final Dynamic<?> transformed = rewrite.apply(dynamic);
208+
return ((Type) type).read(transformed);
209+
}).map(newValue -> new Typed<>((Type) type, newValue)).result();
186210
}
187211

188212
@Override

aether-datafixers-api/src/main/java/de/splatgames/aether/datafixers/api/optic/Finder.java

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -179,8 +179,10 @@ public Dynamic<?> set(@NotNull final Dynamic<?> root,
179179
* Creates a finder that navigates to an element by index in a list/array structure.
180180
*
181181
* <p>The returned finder extracts and modifies the element at the specified
182-
* index position. If the index is out of bounds (negative or beyond the list size), {@link #get} returns
183-
* {@code null} and {@link #set} returns the root unchanged.</p>
182+
* index position. A negative {@code index} is rejected immediately by throwing
183+
* {@link IllegalArgumentException} at construction time. If the index is non-negative
184+
* but beyond the list size, {@link #get} returns {@code null} and {@link #set} returns
185+
* the root unchanged.</p>
184186
*
185187
* <h4>Example</h4>
186188
* <pre>{@code
@@ -195,16 +197,21 @@ public Dynamic<?> set(@NotNull final Dynamic<?> root,
195197
* Dynamic<?> updated = firstScoreFinder.set(data, data.createInt(100));
196198
* // updated: {"scores": [100, 92, 78]}
197199
*
198-
* // Out of bounds access
200+
* // Out-of-range (positive) index — get returns null, set returns root unchanged
199201
* Finder<?> outOfBounds = scoresFinder.then(Finder.index(10));
200202
* Dynamic<?> missing = outOfBounds.get(data); // null
203+
*
204+
* // Negative index — throws IllegalArgumentException immediately
205+
* Finder.index(-1); // throws IllegalArgumentException
201206
* }</pre>
202207
*
203-
* @param index the zero-based index of the element to focus on
208+
* @param index the zero-based index of the element to focus on; must not be negative
209+
* @throws IllegalArgumentException if {@code index} is negative
204210
* @return a finder that navigates to the element at the specified index, never {@code null}
205211
*/
206212
@NotNull
207213
static Finder<Object> index(final int index) {
214+
Preconditions.checkArgument(index >= 0, "index must not be negative, got: %s", index);
208215
return new Finder<>() {
209216
@NotNull
210217
@Override
@@ -232,7 +239,7 @@ public String id() {
232239
return root;
233240
}
234241
final var list = listResult.result().orElseThrow().toList();
235-
if (index < 0 || index >= list.size()) {
242+
if (index >= list.size()) {
236243
return root;
237244
}
238245
final List<Dynamic<Object>> newList = new ArrayList<>();

aether-datafixers-api/src/main/java/de/splatgames/aether/datafixers/api/optic/Iso.java

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -384,13 +384,6 @@ public A modify(@NotNull final B source,
384384
Preconditions.checkNotNull(modifier, "modifier must not be null");
385385
return from(modifier.apply(to(source)));
386386
}
387-
388-
@NotNull
389-
@Override
390-
public <C, D> Optic<B, A, C, D> compose(@NotNull final Optic<T, S, C, D> other) {
391-
Preconditions.checkNotNull(other, "other must not be null");
392-
throw new UnsupportedOperationException("Compose on reversed iso");
393-
}
394387
};
395388
}
396389

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

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1568,9 +1568,13 @@ public static <T> TypeRewriteRule flattenField(@NotNull final DynamicOps<T> ops,
15681568
Dynamic<T> result = typedDynamic.remove(fieldName);
15691569
final var entries = entriesResult.result().orElse(java.util.stream.Stream.empty()).toList();
15701570
for (final var entry : entries) {
1571-
final String key = entry.first().asString().result().orElse(null);
1571+
final Dynamic<?> keyDynamic = entry.first();
1572+
final Dynamic<T> value = entry.second();
1573+
if (keyDynamic == null || value == null) {
1574+
continue;
1575+
}
1576+
final String key = keyDynamic.asString().result().orElse(null);
15721577
if (key != null) {
1573-
final Dynamic<T> value = entry.second();
15741578
result = result.set(key, value);
15751579
}
15761580
}

aether-datafixers-api/src/main/java/de/splatgames/aether/datafixers/api/schema/Schema.java

Lines changed: 27 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,8 @@
9494
public class Schema {
9595
private final DataVersion version;
9696
private final Schema parent;
97-
private TypeRegistry types;
97+
private volatile TypeRegistry types;
98+
private TypeRegistry buildingTypes;
9899

99100
/**
100101
* Creates a new schema for the specified version with the given types.
@@ -161,10 +162,17 @@ public Schema parent() {
161162
*/
162163
@NotNull
163164
public TypeRegistry types() {
164-
if (this.types == null) {
165-
this.types = this.buildTypes();
165+
TypeRegistry result = this.types;
166+
if (result == null) {
167+
synchronized (this) {
168+
result = this.types;
169+
if (result == null) {
170+
result = this.buildTypes();
171+
this.types = result;
172+
}
173+
}
166174
}
167-
return this.types;
175+
return result;
168176
}
169177

170178
/**
@@ -178,18 +186,22 @@ public TypeRegistry types() {
178186
@NotNull
179187
private TypeRegistry buildTypes() {
180188
final TypeRegistry registry = this.createTypeRegistry();
181-
this.types = registry;
189+
this.buildingTypes = registry;
182190

183191
// Inherit types from parent if present
184192
if (this.parent != null) {
185-
// Copy types from parent
186-
this.parent.types();
187-
// Parent types are already registered in parent's registry
188-
// For now, we don't copy - subclass must re-register all types it needs
193+
final TypeRegistry parentTypes = this.parent.types();
194+
for (final TypeReference ref : parentTypes.references()) {
195+
final Type<?> parentType = parentTypes.get(ref);
196+
if (parentType != null) {
197+
registry.register(parentType);
198+
}
199+
}
189200
}
190201

191202
// Let subclass register types
192203
this.registerTypes();
204+
this.buildingTypes = null;
193205

194206
return registry;
195207
}
@@ -232,8 +244,9 @@ protected void registerTypes() {
232244
*/
233245
protected final void registerType(@NotNull final Type<?> type) {
234246
Preconditions.checkNotNull(type, "type must not be null");
235-
Preconditions.checkState(this.types != null, "Cannot register types before types() is called");
236-
this.types.register(type);
247+
final TypeRegistry registry = this.buildingTypes;
248+
Preconditions.checkState(registry != null, "Cannot register types outside of registerTypes()");
249+
registry.register(type);
237250
}
238251

239252
/**
@@ -267,13 +280,14 @@ protected final void registerType(@NotNull final TypeReference reference,
267280
@NotNull final TypeTemplate template) {
268281
Preconditions.checkNotNull(reference, "reference must not be null");
269282
Preconditions.checkNotNull(template, "template must not be null");
270-
Preconditions.checkState(this.types != null, "Cannot register types before types() is called");
283+
final TypeRegistry registry = this.buildingTypes;
284+
Preconditions.checkState(registry != null, "Cannot register types outside of registerTypes()");
271285

272286
// Apply the template with an empty family to get the concrete type
273287
final Type<?> templateType = template.apply(TypeFamily.empty());
274288

275289
// Wrap the template type with the reference
276-
this.types.register(new TemplateBasedType<>(reference, templateType));
290+
registry.register(new TemplateBasedType<>(reference, templateType));
277291
}
278292

279293
/**

aether-datafixers-api/src/main/java/de/splatgames/aether/datafixers/api/type/Typed.java

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -384,7 +384,8 @@ public <T> DataResult<Typed<A>> updateDynamic(@NotNull final DynamicOps<T> ops,
384384
* @param ops the dynamic operations for encoding, must not be {@code null}
385385
* @param finder the finder that locates the desired sub-value, must not be {@code null}
386386
* @param <T> the underlying data format type
387-
* @return a {@link DataResult} containing the found dynamic value or {@code null} if not found, never {@code null}
387+
* @return a {@link DataResult} containing the found dynamic value on success, or an error result if the path
388+
* was not found; never {@code null}
388389
* @throws NullPointerException if {@code ops} or {@code finder} is {@code null}
389390
* @see #updateAt(DynamicOps, Finder, Function)
390391
*/
@@ -393,12 +394,12 @@ public <T> DataResult<Dynamic<T>> getAt(@NotNull final DynamicOps<T> ops,
393394
@NotNull final Finder<?> finder) {
394395
Preconditions.checkNotNull(ops, "ops must not be null");
395396
Preconditions.checkNotNull(finder, "finder must not be null");
396-
return encode(ops).map(dynamic -> {
397+
return encode(ops).flatMap(dynamic -> {
397398
final Dynamic<?> found = finder.get(dynamic);
398399
if (found == null) {
399-
return null;
400+
return DataResult.error("Path not found: " + finder);
400401
}
401-
return found.convert(ops);
402+
return DataResult.success(found.convert(ops));
402403
});
403404
}
404405

aether-datafixers-api/src/test/java/de/splatgames/aether/datafixers/api/optic/FinderTest.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
import java.util.Map;
3333

3434
import static org.assertj.core.api.Assertions.assertThat;
35+
import static org.assertj.core.api.Assertions.assertThatThrownBy;
3536

3637
/**
3738
* Unit tests for {@link Finder}.
@@ -167,6 +168,14 @@ void idReturnsDescriptiveIdentifier() {
167168

168169
assertThat(finder.id()).isEqualTo("index[3]");
169170
}
171+
172+
@Test
173+
@DisplayName("index() rejects negative index")
174+
void indexRejectsNegativeIndex() {
175+
assertThatThrownBy(() -> Finder.index(-1))
176+
.isInstanceOf(IllegalArgumentException.class)
177+
.hasMessageContaining("must not be negative");
178+
}
170179
}
171180

172181
@Nested

aether-datafixers-api/src/test/java/de/splatgames/aether/datafixers/api/optic/IsoTest.java

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,26 @@ void reverseAddsReverseToId() {
196196
assertThat(unwrapIso.reverse().id()).isEqualTo("wrapper.unwrap.reverse");
197197
}
198198

199+
@Test
200+
@DisplayName("reverse() supports compose()")
201+
void reverseSupportsCompose() {
202+
// reversed: String -> Wrapper<String>
203+
final Iso<String, String, Wrapper<String>, Wrapper<String>> reversed = unwrapIso.reverse();
204+
205+
// another iso: Wrapper<String> -> String (extract value and uppercase)
206+
final Iso<Wrapper<String>, Wrapper<String>, String, String> extractUpper = Iso.of(
207+
"wrapper.upper",
208+
w -> w.value().toUpperCase(),
209+
s -> new Wrapper<>(s.toLowerCase())
210+
);
211+
212+
// compose: String -> Wrapper<String> -> String (uppercased)
213+
final Iso<String, String, String, String> composed = reversed.compose(extractUpper);
214+
215+
assertThat(composed.to("hello")).isEqualTo("HELLO");
216+
assertThat(composed.from("HELLO")).isEqualTo("hello");
217+
}
218+
199219
@Test
200220
@DisplayName("double reverse returns to original")
201221
void doubleReverseReturnsToOriginal() {

0 commit comments

Comments
 (0)