A248

MapLiaison DRAFT

Nov 19th, 2025
161
0
358 days
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Java 14.93 KB | None | 0 0
  1. /*
  2.  * DazzleConf
  3.  * Copyright © 2025 Anand Beh
  4.  *
  5.  * DazzleConf is free software: you can redistribute it and/or modify
  6.  * it under the terms of the GNU Lesser General Public License as published by
  7.  * the Free Software Foundation, either version 3 of the License, or
  8.  * (at your option) any later version.
  9.  *
  10.  * DazzleConf is distributed in the hope that it will be useful,
  11.  * but WITHOUT ANY WARRANTY; without even the implied warranty of
  12.  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  13.  * GNU Lesser General Public License for more details.
  14.  *
  15.  * You should have received a copy of the GNU Lesser General Public License
  16.  * along with DazzleConf. If not, see <https://www.gnu.org/licenses/>
  17.  * and navigate to version 3 of the GNU Lesser General Public License.
  18.  */
  19.  
  20. package space.arim.dazzleconf.engine.liaison;
  21.  
  22. import org.checkerframework.checker.nullness.qual.NonNull;
  23. import org.checkerframework.checker.nullness.qual.Nullable;
  24. import org.checkerframework.dataflow.qual.SideEffectFree;
  25. import space.arim.dazzleconf.DeveloperMistakeException;
  26. import space.arim.dazzleconf.ErrorContext;
  27. import space.arim.dazzleconf.LoadResult;
  28. import space.arim.dazzleconf.backend.DataEntry;
  29. import space.arim.dazzleconf.backend.DataTree;
  30. import space.arim.dazzleconf.backend.KeyPath;
  31. import space.arim.dazzleconf.engine.DefaultValues;
  32. import space.arim.dazzleconf.engine.DeserializeInput;
  33. import space.arim.dazzleconf.engine.SerializeDeserialize;
  34. import space.arim.dazzleconf.engine.SerializeOutput;
  35. import space.arim.dazzleconf.engine.TypeLiaison;
  36. import space.arim.dazzleconf.engine.UpdateReason;
  37. import space.arim.dazzleconf.reflect.ReifiedType;
  38. import space.arim.dazzleconf.reflect.TypeToken;
  39.  
  40. import java.util.Arrays;
  41. import java.util.Collections;
  42. import java.util.LinkedHashMap;
  43. import java.util.Map;
  44.  
  45. /**
  46.  * Liaison for maps.
  47.  * <p>
  48.  * This liaison will match all {@code Map<K, V>}. It will use the relevant serializers for the key and value type,
  49.  * respectively, to build the relevant map.
  50.  * <p>
  51.  * <b>Limitations</b>
  52.  * <p>
  53.  * This liaison does not, and <b>cannot</b>, preserve entry metadata, such as comments, on map values across separate
  54.  * acts of deserialization and serialization. Only a {@link SerializeDeserialize#deserializeUpdate} operation can
  55.  * preserve this entry metadata.
  56.  * <p>
  57.  * <b>Duplicate Keys</b>
  58.  * <p>
  59.  * Note that despite this liaison using a {@link space.arim.dazzleconf.backend.DataTree} as its input, it is still
  60.  * possible for duplicate keys to be encountered. Deserialization, in general, is not a 1-to-1 mapping, and the chosen
  61.  * key type can implement its own equality semantics, in addition to mapping from different input types.
  62.  * <p>
  63.  * The policy of this liaison is to silently skip duplicate keys arising from user input. There is no sort of "merge"
  64.  * algorithm for values. Additionally, it is not defined which key will be selected.
  65.  * <p>
  66.  * During serialization, duplicate keys are another possibility, if the key serializer implementation chooses to produce
  67.  * identical output keys. <i>This liaison considers such duplication a mistake</i>, and it will throw a
  68.  * {@link DeveloperMistakeException} if conflicting output keys are detected.
  69.  * <p>
  70.  * <b>Consistent Order</b>
  71.  * <p>
  72.  * This liaison provides a consistent order across deserialization and re-serialization. It is implemented internally
  73.  * over an immutable wrapper of {@link java.util.LinkedHashSet}.
  74.  */
  75. public final class MapLiaison implements TypeLiaison {
  76.  
  77.     /**
  78.      * Creates the liaison
  79.      *
  80.      */
  81.     public MapLiaison() {}
  82.  
  83.     @Override
  84.     @SideEffectFree
  85.     public @Nullable <V> Agent<V> makeAgent(@NonNull TypeToken<V> typeToken, @NonNull Handshake handshake) {
  86.         if (typeToken.getRawType().equals(Map.class)) {
  87.             ReifiedType.Annotated reifiedType = typeToken.getReifiedType();
  88.             TypeToken<?> keyToken = new TypeToken<>(reifiedType.argumentAt(0));
  89.             TypeToken<?> valueToken = new TypeToken<>(reifiedType.argumentAt(1));
  90.             Agent<?> agentImpl = new AgentImpl<>(
  91.                     handshake.getOtherSerializer(keyToken), handshake.getOtherSerializer(valueToken)
  92.             );
  93.             @SuppressWarnings("unchecked")
  94.             Agent<V> castAgent = (Agent<V>) agentImpl;
  95.             return castAgent;
  96.         }
  97.         return null;
  98.     }
  99.  
  100.     private static final class AgentImpl<K, V> implements Agent<Map<K, V>>, SerializeDeserialize<Map<K, V>> {
  101.  
  102.         private final SerializeDeserialize<K> keySerializer;
  103.         private final SerializeDeserialize<V> valueSerializer;
  104.  
  105.         private AgentImpl(SerializeDeserialize<K> keySerializer, SerializeDeserialize<V> valueSerializer) {
  106.             this.keySerializer = keySerializer;
  107.             this.valueSerializer = valueSerializer;
  108.         }
  109.  
  110.         @Override
  111.         public @Nullable DefaultValues<Map<K, V>> loadDefaultValues(@NonNull DefaultInit defaultInit) {
  112.             return null;
  113.         }
  114.  
  115.         @Override
  116.         public @NonNull SerializeDeserialize<Map<K, V>> makeSerializer() {
  117.             return this;
  118.         }
  119.  
  120.         private <D extends DataTree> @NonNull LoadResult<@NonNull Map<K, V>> implDeserialize(
  121.                 @NonNull DeserializeInput deser, @NonNull ImplDeserialize<D, K, V> implDeserialize
  122.         ) {
  123.             // In order to reduce stack depth, avoid functions like LoadResult#flatMap
  124.             LoadResult<DataTree> dataTreeResult = deser.requireDataTree();
  125.             if (dataTreeResult.isFailure()) {
  126.                 return LoadResult.failure(dataTreeResult.getErrorContexts());
  127.             }
  128.             D input = implDeserialize.prepare(dataTreeResult.getOrThrow());
  129.             // Error handling - get a certain maximum before quitting, becomes non-null if we find at least 1 error
  130.             ErrorContext[] collectedErrors = null;
  131.             int errorCount = 0;
  132.  
  133.             Map<K, V> built = new LinkedHashMap<>();
  134.  
  135.             for (Object inputKey : input.keySet()) {
  136.                 // Deserialize the key
  137.                 DataEntry inputEntry = input.get(inputKey);
  138.                 if (inputEntry == null) {
  139.                     throw new IllegalStateException(
  140.                             "Key " + inputKey + " is part of keySet() but not in the data tree itself"
  141.                     );
  142.                 }
  143.                 LoadResult<K> keyResult = implDeserialize.deserialize(keySerializer, deser.makeChild(inputKey));
  144.                 if (keyResult.isFailure()) {
  145.                     if (collectedErrors == null) {
  146.                         collectedErrors = new ErrorContext[deser.maximumErrorCollect()];
  147.                     }
  148.                     for (ErrorContext errorToAppend : keyResult.getErrorContexts()) {
  149.                         // Append this error
  150.                         collectedErrors[errorCount++] = errorToAppend;
  151.                         // Check if maxed out
  152.                         if (errorCount == collectedErrors.length) {
  153.                             return LoadResult.failure(collectedErrors);
  154.                         }
  155.                     }
  156.                     continue;
  157.                 }
  158.                 K key = keyResult.getOrThrow();
  159.                 Object keyUpdate = implDeserialize.getUpdateFromDeserializeCall();
  160.  
  161.                 LoadResult<V> valueResult = implDeserialize.deserialize(valueSerializer, deser.makeChild(inputEntry.getValue()));
  162.                 if (valueResult.isFailure()) {
  163.                     if (collectedErrors == null) {
  164.                         collectedErrors = new ErrorContext[deser.maximumErrorCollect()];
  165.                     }
  166.                     for (ErrorContext errorToAppend : keyResult.getErrorContexts()) {
  167.                         // Append this error
  168.                         collectedErrors[errorCount++] = errorToAppend;
  169.                         // Check if maxed out
  170.                         if (errorCount == collectedErrors.length) {
  171.                             return LoadResult.failure(collectedErrors);
  172.                         }
  173.                     }
  174.                     continue;
  175.                 }
  176.                 V value = valueResult.getOrThrow();
  177.                 Object valueUpdate = implDeserialize.getUpdateFromDeserializeCall();
  178.                 implDeserialize.updateIfDesired(input, inputKey, inputEntry, keyUpdate, valueUpdate);
  179.  
  180.                 built.put(key, value);
  181.             }
  182.             // Error handling
  183.             if (collectedErrors != null) {
  184.                 return LoadResult.failure(Arrays.copyOf(collectedErrors, errorCount));
  185.             }
  186.             // Finish recording updates - check if size changed
  187.             if (input.size() != built.size()) {
  188.                 // Note that size-related updates can't happen during iteration itself (concurrent modification)
  189.                 implDeserialize.updateSizeShrunk(deser, built);
  190.             } else {
  191.                 implDeserialize.updateMaybeOtherwise(deser, input);
  192.             }
  193.             return LoadResult.of(Collections.unmodifiableMap(built));
  194.         }
  195.  
  196.         interface ImplDeserialize<D extends DataTree, K, V> {
  197.  
  198.             D prepare(DataTree dataTree);
  199.  
  200.             <E> LoadResult<E> deserialize(SerializeDeserialize<E> serializer, DeserializeInput deser);
  201.  
  202.             @Nullable Object getUpdateFromDeserializeCall();
  203.  
  204.             void updateIfDesired(D updatableInput, Object inputKey, DataEntry inputEntry,
  205.                                  @Nullable Object keyUpdate, @Nullable Object valueUpdate);
  206.  
  207.             void updateSizeShrunk(DeserializeInput deser, Map<K, V> built);
  208.  
  209.             void updateMaybeOtherwise(DeserializeInput deser, D updatableInput);
  210.  
  211.         }
  212.         @Override
  213.         public @NonNull LoadResult<@NonNull Map<K, V>> deserialize(@NonNull DeserializeInput deser) {
  214.             return implDeserialize(deser, new ImplDeserialize<DataTree, K, V>() {
  215.                 @Override
  216.                 public DataTree prepare(DataTree dataTree) {
  217.                     return dataTree;
  218.                 }
  219.  
  220.                 @Override
  221.                 public <E> LoadResult<E> deserialize(SerializeDeserialize<E> serializer, DeserializeInput deser) {
  222.                     return serializer.deserialize(deser);
  223.                 }
  224.  
  225.                 @Override
  226.                 public @Nullable Object getUpdateFromDeserializeCall() {
  227.                     return null;
  228.                 }
  229.  
  230.                 @Override
  231.                 public void updateIfDesired(DataTree updatableInput, Object inputKey, DataEntry inputEntry,
  232.                                             @Nullable Object keyUpdate, @Nullable Object valueUpdate) {}
  233.  
  234.                 @Override
  235.                 public void updateSizeShrunk(DeserializeInput deser, Map<K, V> built) {
  236.                     deser.notifyUpdate(KeyPath.empty(), UpdateReason.OTHER);
  237.                 }
  238.  
  239.                 @Override
  240.                 public void updateMaybeOtherwise(DeserializeInput deser, DataTree updatableInput) {}
  241.             });
  242.         }
  243.  
  244.         @Override
  245.         public @NonNull LoadResult<@NonNull Map<K, V>> deserializeUpdate(@NonNull DeserializeInput deser, @NonNull SerializeOutput updateTo) {
  246.             return implDeserialize(deser, new ImplDeserialize<DataTree.Mut, K, V>() {
  247.  
  248.                 private boolean updated;
  249.  
  250.                 @Override
  251.                 public DataTree.Mut prepare(DataTree dataTree) {
  252.                     return dataTree.intoMut();
  253.                 }
  254.  
  255.                 @Override
  256.                 public <E> LoadResult<E> deserialize(SerializeDeserialize<E> serializer, DeserializeInput deser) {
  257.                     return serializer.deserializeUpdate(deser, updateTo);
  258.                 }
  259.  
  260.                 @Override
  261.                 public @Nullable Object getUpdateFromDeserializeCall() {
  262.                     return updateTo.getAndClearLastOutput();
  263.                 }
  264.  
  265.                 @Override
  266.                 public void updateIfDesired(DataTree.Mut updatableInput, Object inputKey, DataEntry inputEntry, @Nullable Object keyUpdate, @Nullable Object valueUpdate) {
  267.                     boolean updateKey = keyUpdate != null && !inputKey.equals(keyUpdate);
  268.                     boolean updateValue = valueUpdate != null && !inputEntry.getValue().equals(valueUpdate);
  269.                     if (updateKey || updateValue) {
  270.                         DataEntry entry;
  271.                         if (updateValue) {
  272.                             entry = inputEntry.withValue(valueUpdate);
  273.                         } else {
  274.                             entry = inputEntry;
  275.                         }
  276.                         if (updateKey) {
  277.                             updatableInput.remove(inputKey);
  278.                             updatableInput.put(keyUpdate, entry);
  279.                         } else {
  280.                             updatableInput.put(inputKey, entry);
  281.                         }
  282.                         updated = true;
  283.                     }
  284.                 }
  285.  
  286.                 @Override
  287.                 public void updateSizeShrunk(DeserializeInput deser, Map<K, V> built) {
  288.                     deser.notifyUpdate(KeyPath.empty(), UpdateReason.OTHER);
  289.                     serialize(built, updateTo); // Reserialize the whole map
  290.                 }
  291.  
  292.                 @Override
  293.                 public void updateMaybeOtherwise(DeserializeInput deser, DataTree.Mut updatableInput) {
  294.                     // If the size didn't shrink, then perform our update if applicable
  295.                     if (updated) {
  296.                         deser.notifyUpdate(KeyPath.empty(), UpdateReason.UPDATED);
  297.                         updateTo.outDataTree(updatableInput);
  298.                     }
  299.                 }
  300.             });
  301.         }
  302.  
  303.         @Override
  304.         public void serialize(@NonNull Map<K, V> value, @NonNull SerializeOutput ser) {
  305.             DataTree.Mut output = new DataTree.Mut();
  306.             for (Map.Entry<K, V> entry : value.entrySet()) {
  307.                 keySerializer.serialize(entry.getKey(), ser);
  308.                 Object keyOutput = ser.getAndClearLastOutput();
  309.                 if (keyOutput == null) {
  310.                     throw new DeveloperMistakeException(
  311.                             "Key serializer " + keySerializer + " did not produce output"
  312.                     );
  313.                 }
  314.                 valueSerializer.serialize(entry.getValue(), ser);
  315.                 Object valueOutput = ser.getAndClearLastOutput();
  316.                 if (valueOutput == null) {
  317.                     throw new DeveloperMistakeException("Value serializer " + valueSerializer + " did not produce output");
  318.                 }
  319.                 DataEntry previousValue = output.put(keyOutput, new DataEntry(valueOutput));
  320.                 if (previousValue != null) {
  321.                     throw new DeveloperMistakeException(
  322.                             "The key serializer " + keySerializer + " produced a duplicate output key " + keyOutput
  323.                     );
  324.                 }
  325.             }
  326.             ser.outDataTree(output);
  327.         }
  328.     }
  329. }
  330.  
Advertisement
Add Comment
Please, Sign In to add comment