Skip to content

Commit 0e06805

Browse files
authored
Merge pull request #253 from aether-framework/bugfix/145-146-149-151-153-154-155-156-157-160-codec-and-core-fixes
Refactor error handling and schema validation workflows
2 parents b2f151b + 54ae1e5 commit 0e06805

8 files changed

Lines changed: 66 additions & 8 deletions

File tree

aether-datafixers-codec/src/main/java/de/splatgames/aether/datafixers/codec/json/jackson/JacksonJsonOps.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,9 @@
2525
import com.fasterxml.jackson.databind.JsonNode;
2626
import com.fasterxml.jackson.databind.ObjectMapper;
2727
import com.fasterxml.jackson.databind.node.ArrayNode;
28+
import com.fasterxml.jackson.databind.node.BigIntegerNode;
2829
import com.fasterxml.jackson.databind.node.BooleanNode;
30+
import com.fasterxml.jackson.databind.node.DecimalNode;
2931
import com.fasterxml.jackson.databind.node.DoubleNode;
3032
import com.fasterxml.jackson.databind.node.FloatNode;
3133
import com.fasterxml.jackson.databind.node.IntNode;
@@ -43,6 +45,8 @@
4345
import org.jetbrains.annotations.NotNull;
4446
import org.jetbrains.annotations.Nullable;
4547

48+
import java.math.BigDecimal;
49+
import java.math.BigInteger;
4650
import java.util.Iterator;
4751
import java.util.Map;
4852
import java.util.Spliterator;
@@ -689,6 +693,12 @@ public JsonNode createNumeric(@NotNull final Number value) {
689693
if (value instanceof Byte) {
690694
return ShortNode.valueOf(value.byteValue());
691695
}
696+
if (value instanceof BigDecimal bd) {
697+
return DecimalNode.valueOf(bd);
698+
}
699+
if (value instanceof BigInteger bi) {
700+
return BigIntegerNode.valueOf(bi);
701+
}
692702
return DoubleNode.valueOf(value.doubleValue());
693703
}
694704

aether-datafixers-codec/src/main/java/de/splatgames/aether/datafixers/codec/toml/jackson/JacksonTomlOps.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -935,6 +935,12 @@ public DataResult<JsonNode> mergeToList(@NotNull final JsonNode list,
935935
return DataResult.error("Not an array: " + list);
936936
}
937937
final ArrayNode result = list.isNull() ? this.nodeFactory.arrayNode() : ((ArrayNode) list).deepCopy();
938+
// TOML requires homogeneous arrays — validate element type consistency
939+
if (!result.isEmpty() && result.get(0).getNodeType() != value.getNodeType()) {
940+
return DataResult.error(
941+
"TOML requires homogeneous arrays: expected "
942+
+ result.get(0).getNodeType() + " but got " + value.getNodeType());
943+
}
938944
result.add(value);
939945
return DataResult.success(result);
940946
}
@@ -1089,6 +1095,10 @@ public DataResult<JsonNode> mergeToMap(@NotNull final JsonNode map,
10891095
if (!key.isTextual()) {
10901096
return DataResult.error("Key is not a string: " + key);
10911097
}
1098+
// TOML does not support null values
1099+
if (value.isNull()) {
1100+
return DataResult.error("TOML does not support null values for key: " + key.asText());
1101+
}
10921102
final ObjectNode result = map.isNull() ? this.nodeFactory.objectNode() : ((ObjectNode) map).deepCopy();
10931103
result.set(key.asText(), value);
10941104
return DataResult.success(result);

aether-datafixers-codec/src/main/java/de/splatgames/aether/datafixers/codec/yaml/snakeyaml/SnakeYamlOps.java

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -828,7 +828,7 @@ public Object createMap(@NotNull final Stream<Pair<Object, Object>> entries) {
828828
return; // Skip entries with null keys
829829
}
830830
final String key = keyObj.toString();
831-
map.put(key, valueObj);
831+
map.put(key, valueObj == null ? YamlNull.INSTANCE : valueObj);
832832
});
833833
return map;
834834
}
@@ -1254,9 +1254,21 @@ public <U> Object convertTo(@NotNull final DynamicOps<U> sourceOps,
12541254
* @return a deep copy of the value, or the value itself if it is immutable;
12551255
* {@code null} if the input is {@code null}
12561256
*/
1257+
private static final int MAX_DEEP_COPY_DEPTH = 512;
1258+
12571259
@Nullable
12581260
@SuppressWarnings("unchecked")
12591261
private Object deepCopy(@Nullable final Object value) {
1262+
return deepCopy(value, 0);
1263+
}
1264+
1265+
@Nullable
1266+
@SuppressWarnings("unchecked")
1267+
private Object deepCopy(@Nullable final Object value, final int depth) {
1268+
if (depth > MAX_DEEP_COPY_DEPTH) {
1269+
throw new IllegalStateException(
1270+
"YAML structure too deeply nested (depth > " + MAX_DEEP_COPY_DEPTH + ")");
1271+
}
12601272
if (value == null) {
12611273
return null;
12621274
}
@@ -1267,15 +1279,15 @@ private Object deepCopy(@Nullable final Object value) {
12671279
final Map<String, Object> original = (Map<String, Object>) value;
12681280
final Map<String, Object> copy = new LinkedHashMap<>();
12691281
for (final Map.Entry<String, Object> entry : original.entrySet()) {
1270-
copy.put(entry.getKey(), deepCopy(entry.getValue()));
1282+
copy.put(entry.getKey(), deepCopy(entry.getValue(), depth + 1));
12711283
}
12721284
return copy;
12731285
}
12741286
if (value instanceof List) {
12751287
final List<Object> original = (List<Object>) value;
12761288
final List<Object> copy = new ArrayList<>();
12771289
for (final Object element : original) {
1278-
copy.add(deepCopy(element));
1290+
copy.add(deepCopy(element, depth + 1));
12791291
}
12801292
return copy;
12811293
}

aether-datafixers-codec/src/test/java/de/splatgames/aether/datafixers/codec/yaml/snakeyaml/SnakeYamlOpsTest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -959,7 +959,7 @@ void createMapHandlesNullValues() {
959959

960960
@SuppressWarnings("unchecked")
961961
final Map<String, Object> resultMap = (Map<String, Object>) map;
962-
assertThat(resultMap.get("key")).isNull();
962+
assertThat(resultMap.get("key")).isSameAs(SnakeYamlOps.NULL);
963963
}
964964
}
965965
}

aether-datafixers-core/src/main/java/de/splatgames/aether/datafixers/core/diagnostic/MigrationReportImpl.java

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ private MigrationReportImpl(final BuilderImpl builder) {
6868
this.fromVersion = builder.fromVersion;
6969
this.toVersion = builder.toVersion;
7070
this.startTime = builder.startTime;
71-
this.endTime = Instant.now();
71+
this.endTime = builder.endTime;
7272
this.fixExecutions = List.copyOf(builder.fixExecutions);
7373
this.touchedTypes = Set.copyOf(builder.touchedTypes);
7474
this.warnings = List.copyOf(builder.warnings);
@@ -164,6 +164,7 @@ public static final class BuilderImpl implements MigrationReport.Builder {
164164
private DataVersion fromVersion;
165165
private DataVersion toVersion;
166166
private Instant startTime;
167+
private Instant endTime;
167168
private final List<FixExecution> fixExecutions = new ArrayList<>();
168169
private final Set<TypeReference> touchedTypes = new LinkedHashSet<>();
169170
private final List<String> warnings = new ArrayList<>();
@@ -259,14 +260,22 @@ public Builder endFix(
259260
this.fixExecutions.add(execution);
260261

261262
// Reset current fix tracking
263+
this.resetFixState();
264+
265+
return this;
266+
}
267+
268+
/**
269+
* Resets fix-tracking state. Called after endFix() or on error paths
270+
* to prevent stale state from corrupting subsequent fix records.
271+
*/
272+
private void resetFixState() {
262273
this.currentFixName = null;
263274
this.currentFixFromVersion = null;
264275
this.currentFixToVersion = null;
265276
this.currentFixStartTime = null;
266277
this.currentRuleApplications.clear();
267278
this.currentFixBeforeSnapshot = null;
268-
269-
return this;
270279
}
271280

272281
@Override
@@ -301,6 +310,7 @@ public MigrationReport build() {
301310
);
302311
}
303312

313+
this.endTime = Instant.now();
304314
return new MigrationReportImpl(this);
305315
}
306316
}

aether-datafixers-core/src/main/java/de/splatgames/aether/datafixers/core/fix/DataFixRegistry.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -216,7 +216,7 @@ public boolean hasFixesInRange(
216216
*
217217
* <p>This method is idempotent - calling it multiple times has no effect after the first call.</p>
218218
*/
219-
public void freeze() {
219+
public synchronized void freeze() {
220220
if (!this.frozen) {
221221
// Create an immutable deep copy
222222
final Map<TypeReference, NavigableMap<DataVersion, List<DataFix<?>>>> immutableCopy = new HashMap<>();

aether-datafixers-core/src/main/java/de/splatgames/aether/datafixers/core/fix/DataFixerImpl.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,8 +187,16 @@ public <T> Dynamic<T> update(
187187
diagCtx.reportBuilder().endFix(fix, duration, afterSnapshot);
188188
}
189189
} catch (final FixException e) {
190+
if (diagCtx != null) {
191+
final Duration duration = Duration.between(fixStart, Instant.now());
192+
diagCtx.reportBuilder().endFix(fix, duration, null);
193+
}
190194
throw e; // Re-throw FixException as-is
191195
} catch (final Exception e) {
196+
if (diagCtx != null) {
197+
final Duration duration = Duration.between(fixStart, Instant.now());
198+
diagCtx.reportBuilder().endFix(fix, duration, null);
199+
}
192200
throw new FixException(
193201
"Fix '" + fix.name() + "' failed: " + e.getMessage(),
194202
fix.name(),

aether-datafixers-core/src/main/java/de/splatgames/aether/datafixers/core/fix/SchemaDataFix.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
import de.splatgames.aether.datafixers.api.TypeReference;
2828
import de.splatgames.aether.datafixers.api.diagnostic.DiagnosticContext;
2929
import de.splatgames.aether.datafixers.api.dynamic.Dynamic;
30+
import de.splatgames.aether.datafixers.api.exception.FixException;
3031
import de.splatgames.aether.datafixers.api.fix.DataFix;
3132
import de.splatgames.aether.datafixers.api.fix.DataFixerContext;
3233
import de.splatgames.aether.datafixers.api.rewrite.TypeRewriteRule;
@@ -170,6 +171,13 @@ protected SchemaDataFix(
170171
final Typed<?> typedIn = new Typed<>((Type<Object>) logical, input);
171172
final Typed<?> typedOut = rule.apply(typedIn);
172173

174+
if (!(typedOut.value() instanceof Dynamic)) {
175+
throw new FixException(
176+
"Rule produced non-Dynamic output for type '" + type.getId()
177+
+ "' in fix '" + this.name() + "': " + typedOut.value().getClass().getName()
178+
);
179+
}
180+
173181
@SuppressWarnings("unchecked")
174182
final Dynamic<Object> result = (Dynamic<Object>) typedOut.value();
175183

0 commit comments

Comments
 (0)