Add Decoder#ofDictionary.

Change-Id: I53873f743ce84d9bf50da4cb5238a6f4d82de986
diff --git a/jgvariant-core/src/main/java/eu/mulk/jgvariant/core/Decoder.java b/jgvariant-core/src/main/java/eu/mulk/jgvariant/core/Decoder.java
index 96e1332..8297d79 100644
--- a/jgvariant-core/src/main/java/eu/mulk/jgvariant/core/Decoder.java
+++ b/jgvariant-core/src/main/java/eu/mulk/jgvariant/core/Decoder.java
@@ -1,6 +1,7 @@
 package eu.mulk.jgvariant.core;
 
 import static java.nio.ByteOrder.LITTLE_ENDIAN;
+import static java.util.stream.Collectors.toMap;
 
 import java.lang.reflect.InvocationTargetException;
 import java.lang.reflect.RecordComponent;
@@ -13,6 +14,7 @@
 import java.util.Arrays;
 import java.util.List;
 import java.util.Map;
+import java.util.Map.Entry;
 import java.util.Optional;
 import java.util.function.Function;
 import org.apiguardian.api.API;
@@ -110,6 +112,18 @@
   }
 
   /**
+   * Creates a {@link Decoder} for a {@code Dictionary} type.
+   *
+   * @param keyDecoder a {@link Decoder} for the key component of the dictionary entry.
+   * @param valueDecoder a {@link Decoder} for the value component of the dictionary entry.
+   * @return a new {@link Decoder}.
+   */
+  public static <K, V> Decoder<Map<K, V>> ofDictionary(
+      Decoder<K> keyDecoder, Decoder<V> valueDecoder) {
+    return new DictionaryDecoder<>(keyDecoder, valueDecoder);
+  }
+
+  /**
    * Creates a {@link Decoder} for an {@code Array} type of element type {@code byte} into a
    * primitive {@code byte[]} array.
    *
@@ -346,6 +360,32 @@
     }
   }
 
+  private static class DictionaryDecoder<K, V> extends Decoder<Map<K, V>> {
+
+    private final ArrayDecoder<Map.Entry<K, V>> entryArrayDecoder;
+
+    DictionaryDecoder(Decoder<K> keyDecoder, Decoder<V> valueDecoder) {
+      this.entryArrayDecoder =
+          new ArrayDecoder<>(new DictionaryEntryDecoder<>(keyDecoder, valueDecoder));
+    }
+
+    @Override
+    public byte alignment() {
+      return entryArrayDecoder.alignment();
+    }
+
+    @Override
+    public Integer fixedSize() {
+      return entryArrayDecoder.fixedSize();
+    }
+
+    @Override
+    public Map<K, V> decode(ByteBuffer byteSlice) {
+      List<Map.Entry<K, V>> entries = entryArrayDecoder.decode(byteSlice);
+      return entries.stream().collect(toMap(Entry::getKey, Entry::getValue));
+    }
+  }
+
   private static class ByteArrayDecoder extends Decoder<byte[]> {
 
     private static final int ELEMENT_SIZE = 1;
diff --git a/jgvariant-core/src/test/java/eu/mulk/jgvariant/core/DecoderTest.java b/jgvariant-core/src/test/java/eu/mulk/jgvariant/core/DecoderTest.java
index ab7de44..efbcafa 100644
--- a/jgvariant-core/src/test/java/eu/mulk/jgvariant/core/DecoderTest.java
+++ b/jgvariant-core/src/test/java/eu/mulk/jgvariant/core/DecoderTest.java
@@ -99,6 +99,40 @@
   }
 
   @Test
+  void testDictionary() {
+    var data =
+        new byte[] {
+          0x68,
+          0x69,
+          0x00,
+          0x00,
+          (byte) 0xfe,
+          (byte) 0xff,
+          (byte) 0xff,
+          (byte) 0xff,
+          0x03,
+          0x00,
+          0x00,
+          0x00,
+          0x62,
+          0x79,
+          0x65,
+          0x00,
+          (byte) 0xff,
+          (byte) 0xff,
+          (byte) 0xff,
+          (byte) 0xff,
+          0x04,
+          0x09,
+          0x15
+        };
+
+    var decoder =
+        Decoder.ofDictionary(Decoder.ofString(UTF_8), Decoder.ofInt().withByteOrder(LITTLE_ENDIAN));
+    assertEquals(Map.of("hi", -2, "bye", -1), decoder.decode(ByteBuffer.wrap(data)));
+  }
+
+  @Test
   void testStringArray() {
     var data =
         new byte[] {