Remove Variant class, parse variants into Object.

Change-Id: I9b4b3079aea42b74f6fcf6341305b6fded9234f4
diff --git a/README.md b/README.md
index dead043..b607610 100644
--- a/README.md
+++ b/README.md
@@ -5,11 +5,7 @@
 
 ## Overview
 
-The two foundational classes are `Value` and `Decoder`.
-
-`Value` is a sum type (sealed interface) that represents a
-[GVariant][] value.  Its subtypes represent the different types of
-values that [GVariant][] supports.
+The foundational class is `Decoder`.
 
 Instances of `Decoder` read a given concrete subtype of `Value` from a
 [ByteBuffer][].  The class also contains factory methods to create
diff --git a/src/main/java/eu/mulk/jgvariant/core/Decoder.java b/src/main/java/eu/mulk/jgvariant/core/Decoder.java
index bb479ff..9833998 100644
--- a/src/main/java/eu/mulk/jgvariant/core/Decoder.java
+++ b/src/main/java/eu/mulk/jgvariant/core/Decoder.java
@@ -7,6 +7,7 @@
 import java.nio.ByteBuffer;
 import java.nio.ByteOrder;
 import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
@@ -14,7 +15,7 @@
 import org.jetbrains.annotations.Nullable;
 
 /**
- * Type class for decodable {@link Variant} types.
+ * Type class for decodable types.
  *
  * <p>Use the {@code of*} family of constructor methods to acquire a suitable {@link Decoder} for
  * the type you wish to decode.
@@ -111,7 +112,7 @@
   }
 
   /**
-   * Creates a {@link Decoder} for a {@code Structure} type.
+   * Creates a {@link Decoder} for a {@code Structure} type, decoding into a {@link Record}.
    *
    * @param recordType the {@link Record} type that represents the components of the structure.
    * @param componentDecoders a {@link Decoder} for each component of the structure.
@@ -124,11 +125,38 @@
   }
 
   /**
-   * Creates a {@link Decoder} for the {@link Variant} type.
+   * Creates a {@link Decoder} for a {@code Structure} type, decoding into a {@link List}.
+   *
+   * <p>Prefer {@link #ofStructure(Class, Decoder[])} if possible, which is both more type-safe and
+   * more convenient.
+   *
+   * @param componentDecoders a {@link Decoder} for each component of the structure.
+   * @return a new {@link Decoder}.
+   */
+  public static Decoder<Object[]> ofStructure(Decoder<?>... componentDecoders) {
+    return new TupleDecoder(componentDecoders);
+  }
+
+  /**
+   * Creates a {@link Decoder} for the {@code Variant} type.
+   *
+   * <p>The returned {@link Object} can be of one of the following types:
+   *
+   * <ul>
+   *   <li>{@link Boolean}
+   *   <li>{@link Byte}
+   *   <li>{@link Short}
+   *   <li>{@link Integer}
+   *   <li>{@link Long}
+   *   <li>{@link String}
+   *   <li>{@link Optional} (a GVariant {@code Maybe} type)
+   *   <li>{@link List} (a GVariant array)
+   *   <li>{@link Object[]} (a GVariant structure)
+   * </ul>
    *
    * @return a new {@link Decoder}.
    */
-  public static Decoder<Variant> ofVariant() {
+  public static Decoder<Object> ofVariant() {
     return new VariantDecoder();
   }
 
@@ -142,7 +170,7 @@
   }
 
   /**
-   * Creates a {@link Decoder} for the 8-bit {@ode byte} type.
+   * Creates a {@link Decoder} for the 8-bit {@code byte} type.
    *
    * <p><strong>Note:</strong> It is often useful to apply {@link #withByteOrder(ByteOrder)} to the
    * result of this method.
@@ -314,21 +342,56 @@
 
   private static class StructureDecoder<U extends Record> extends Decoder<U> {
 
-    private final RecordComponent[] recordComponents;
     private final Class<U> recordType;
-    private final Decoder<?>[] componentDecoders;
+    private final TupleDecoder tupleDecoder;
 
     StructureDecoder(Class<U> recordType, Decoder<?>... componentDecoders) {
       var recordComponents = recordType.getRecordComponents();
-
       if (componentDecoders.length != recordComponents.length) {
         throw new IllegalArgumentException(
             "number of decoders (%d) does not match number of structure components (%d)"
                 .formatted(componentDecoders.length, recordComponents.length));
       }
 
-      this.recordComponents = recordComponents;
       this.recordType = recordType;
+      this.tupleDecoder = new TupleDecoder(componentDecoders);
+    }
+
+    @Override
+    public byte alignment() {
+      return tupleDecoder.alignment();
+    }
+
+    @Override
+    public Integer fixedSize() {
+      return tupleDecoder.fixedSize();
+    }
+
+    @Override
+    public U decode(ByteBuffer byteSlice) {
+      Object[] recordConstructorArguments = tupleDecoder.decode(byteSlice);
+
+      try {
+        var recordComponentTypes =
+            Arrays.stream(recordType.getRecordComponents())
+                .map(RecordComponent::getType)
+                .toArray(Class<?>[]::new);
+        var recordConstructor = recordType.getDeclaredConstructor(recordComponentTypes);
+        return recordConstructor.newInstance(recordConstructorArguments);
+      } catch (NoSuchMethodException
+          | InstantiationException
+          | IllegalAccessException
+          | InvocationTargetException e) {
+        throw new IllegalStateException(e);
+      }
+    }
+  }
+
+  private static class TupleDecoder extends Decoder<Object[]> {
+
+    private final Decoder<?>[] componentDecoders;
+
+    TupleDecoder(Decoder<?>... componentDecoders) {
       this.componentDecoders = componentDecoders;
     }
 
@@ -358,10 +421,10 @@
     }
 
     @Override
-    public U decode(ByteBuffer byteSlice) {
+    public Object[] decode(ByteBuffer byteSlice) {
       int framingOffsetSize = byteCount(byteSlice.limit());
 
-      var recordConstructorArguments = new Object[recordComponents.length];
+      var objects = new Object[componentDecoders.length];
 
       int position = 0;
       int framingOffsetIndex = 0;
@@ -371,14 +434,14 @@
 
         var fixedComponentSize = componentDecoder.fixedSize();
         if (fixedComponentSize != null) {
-          recordConstructorArguments[componentIndex] =
+          objects[componentIndex] =
               componentDecoder.decode(byteSlice.slice(position, fixedComponentSize));
           position += fixedComponentSize;
         } else {
-          if (componentIndex == recordComponents.length - 1) {
+          if (componentIndex == componentDecoders.length - 1) {
             // The last component never has a framing offset.
             int endPosition = byteSlice.limit() - framingOffsetIndex * framingOffsetSize;
-            recordConstructorArguments[componentIndex] =
+            objects[componentIndex] =
                 componentDecoder.decode(byteSlice.slice(position, endPosition - position));
             position = endPosition;
           } else {
@@ -387,7 +450,7 @@
                     byteSlice.slice(
                         byteSlice.limit() - (1 + framingOffsetIndex) * framingOffsetSize,
                         framingOffsetSize));
-            recordConstructorArguments[componentIndex] =
+            objects[componentIndex] =
                 componentDecoder.decode(byteSlice.slice(position, framingOffset - position));
             position = framingOffset;
             ++framingOffsetIndex;
@@ -397,23 +460,11 @@
         ++componentIndex;
       }
 
-      try {
-        var recordComponentTypes =
-            Arrays.stream(recordType.getRecordComponents())
-                .map(RecordComponent::getType)
-                .toArray(Class<?>[]::new);
-        var recordConstructor = recordType.getDeclaredConstructor(recordComponentTypes);
-        return recordConstructor.newInstance(recordConstructorArguments);
-      } catch (NoSuchMethodException
-          | InstantiationException
-          | IllegalAccessException
-          | InvocationTargetException e) {
-        throw new IllegalStateException(e);
-      }
+      return objects;
     }
   }
 
-  private static class VariantDecoder extends Decoder<Variant> {
+  private static class VariantDecoder extends Decoder<Object> {
 
     @Override
     public byte alignment() {
@@ -427,9 +478,55 @@
     }
 
     @Override
-    public Variant decode(ByteBuffer byteSlice) {
-      // TODO
-      throw new UnsupportedOperationException("not implemented");
+    public Object decode(ByteBuffer byteSlice) {
+      for (int i = byteSlice.limit() - 1; i >= 0; --i) {
+        if (byteSlice.get(i) != 0) {
+          continue;
+        }
+
+        var data = byteSlice.slice(0, i);
+        var signature = byteSlice.slice(i + 1, byteSlice.limit() - (i + 1));
+
+        Decoder<?> decoder = parseSignature(signature);
+        return decoder.decode(data);
+      }
+
+      throw new IllegalArgumentException("variant signature not found");
+    }
+
+    private static Decoder<?> parseSignature(ByteBuffer signature) {
+      char c = (char) signature.get();
+      return switch (c) {
+        case 'b' -> Decoder.ofBoolean();
+        case 'y' -> Decoder.ofByte();
+        case 'n', 'q' -> Decoder.ofShort();
+        case 'i', 'u' -> Decoder.ofInt();
+        case 'x', 't' -> Decoder.ofLong();
+        case 'd' -> Decoder.ofDouble();
+        case 's', 'o', 'g' -> Decoder.ofString(StandardCharsets.UTF_8);
+        case 'v' -> Decoder.ofVariant();
+        case 'm' -> Decoder.ofMaybe(parseSignature(signature));
+        case 'a' -> Decoder.ofArray(parseSignature(signature));
+        case '(', '{' -> Decoder.ofStructure(parseTupleTypes(signature).toArray(new Decoder<?>[0]));
+        default -> throw new IllegalArgumentException(
+            String.format("encountered unknown signature byte '%c'", c));
+      };
+    }
+
+    private static List<Decoder<?>> parseTupleTypes(ByteBuffer signature) {
+      List<Decoder<?>> decoders = new ArrayList<>();
+
+      while (true) {
+        char c = (char) signature.get(signature.position());
+        if (c == ')' || c == '}') {
+          signature.get();
+          break;
+        }
+
+        decoders.add(parseSignature(signature));
+      }
+
+      return decoders;
     }
   }
 
diff --git a/src/main/java/eu/mulk/jgvariant/core/Variant.java b/src/main/java/eu/mulk/jgvariant/core/Variant.java
deleted file mode 100644
index 05e28d5..0000000
--- a/src/main/java/eu/mulk/jgvariant/core/Variant.java
+++ /dev/null
@@ -1,102 +0,0 @@
-package eu.mulk.jgvariant.core;
-
-import java.util.List;
-import java.util.Optional;
-
-/**
- * A value representable by the <a href="https://docs.gtk.org/glib/struct.Variant.html">GVariant</a>
- * serialization format, tagged with its type.
- *
- * <p>{@link Variant} is a sum type (sealed interface) that represents a GVariant value. Its
- * subtypes represent the different types of values that GVariant supports.
- *
- * @see Decoder#ofVariant()
- */
-public sealed interface Variant {
-
-  /**
-   * A homogeneous sequence of GVariant values.
-   *
-   * <p>Arrays of fixed width (i.e. of values of fixed size) are represented in a similar way to
-   * plain C arrays. Arrays of variable width require additional space for padding and framing.
-   *
-   * <p>Heterogeneous sequences are represented by {@code Array<Variant>}.
-   *
-   * @param <T> the type of the elements of the array.
-   * @see Decoder#ofArray
-   */
-  record Array<T>(List<T> values) implements Variant {}
-
-  /**
-   * A value that is either present or absent.
-   *
-   * @param <T> the contained type.
-   * @see Decoder#ofMaybe
-   */
-  record Maybe<T>(Optional<T> value) implements Variant {}
-
-  /**
-   * A tuple of values of fixed types.
-   *
-   * <p>GVariant structures are represented as {@link Record} types. For example, a two-element
-   * structure consisting of a string and an int can be modelled as follows:
-   *
-   * <pre>{@code
-   * record TestRecord(String s, int i) {}
-   * var testStruct = new Structure<>(new TestRecord("hello", 123);
-   * }</pre>
-   *
-   * @param <T> the {@link Record} type that represents the components of the structure.
-   * @see Decoder#ofStructure
-   */
-  record Structure<T extends Record>(T values) implements Variant {}
-
-  /**
-   * Either true or false.
-   *
-   * @see Decoder#ofBoolean()
-   */
-  record Bool(boolean value) implements Variant {}
-
-  /**
-   * A {@code byte}-sized integer.
-   *
-   * @see Decoder#ofByte()
-   */
-  record Byte(byte value) implements Variant {}
-
-  /**
-   * A {@code short}-sized integer.
-   *
-   * @see Decoder#ofShort()
-   */
-  record Short(short value) implements Variant {}
-
-  /**
-   * An {@code int}-sized integer.
-   *
-   * @see Decoder#ofInt()
-   */
-  record Int(int value) implements Variant {}
-
-  /**
-   * A {@code long}-sized integer.
-   *
-   * @see Decoder#ofLong()
-   */
-  record Long(long value) implements Variant {}
-
-  /**
-   * A double-precision floating point number.
-   *
-   * @see Decoder#ofDouble()
-   */
-  record Double(double value) implements Variant {}
-
-  /**
-   * A character string.
-   *
-   * @see Decoder#ofString
-   */
-  record String(java.lang.String value) implements Variant {}
-}
diff --git a/src/main/java/eu/mulk/jgvariant/core/package-info.java b/src/main/java/eu/mulk/jgvariant/core/package-info.java
index 1b819c5..1754096 100644
--- a/src/main/java/eu/mulk/jgvariant/core/package-info.java
+++ b/src/main/java/eu/mulk/jgvariant/core/package-info.java
@@ -1,14 +1,9 @@
 /**
- * Provides {@link eu.mulk.jgvariant.core.Variant} and {@link eu.mulk.jgvariant.core.Decoder}, the
- * foundational classes for <a href="https://docs.gtk.org/glib/struct.Variant.html">GVariant</a>
- * parsing.
+ * Provides {@link eu.mulk.jgvariant.core.Decoder}, the foundational class for <a
+ * href="https://docs.gtk.org/glib/struct.Variant.html">GVariant</a> parsing.
  *
- * <p>{@link eu.mulk.jgvariant.core.Variant} is a sum type (sealed interface) that represents a
- * GVariant value. Its subtypes represent the different types of values that GVariant supports.
- *
- * <p>Instances of {@link eu.mulk.jgvariant.core.Decoder} read a given concrete subtype of {@link
- * eu.mulk.jgvariant.core.Variant} from a {@link java.nio.ByteBuffer}. The class also contains
- * factory methods to create those instances.
+ * <p>Instances of {@link eu.mulk.jgvariant.core.Decoder} read a given value type from a {@link
+ * java.nio.ByteBuffer}. The class also contains factory methods to create those instances.
  *
  * <p><strong>Example</strong>
  *
diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java
index af28413..39e91b8 100644
--- a/src/main/java/module-info.java
+++ b/src/main/java/module-info.java
@@ -1,11 +1,9 @@
-import eu.mulk.jgvariant.core.Variant;
-
 /**
  * Provides a parser for the <a href="https://docs.gtk.org/glib/struct.Variant.html">GVariant</a>
  * serialization format.
  *
- * <p>The {@link eu.mulk.jgvariant.core} package contains the {@link Variant} and {@link
- * eu.mulk.jgvariant.core.Decoder} types. which form the foundation of this library.
+ * <p>The {@link eu.mulk.jgvariant.core} package contains the {@link eu.mulk.jgvariant.core.Decoder}
+ * type. which forms the foundation of this library.
  */
 module eu.mulk.jgvariant.core {
   requires com.google.errorprone.annotations;
diff --git a/src/test/java/eu/mulk/jgvariant/core/DecoderTest.java b/src/test/java/eu/mulk/jgvariant/core/DecoderTest.java
index d37f6a2..0e16973 100644
--- a/src/test/java/eu/mulk/jgvariant/core/DecoderTest.java
+++ b/src/test/java/eu/mulk/jgvariant/core/DecoderTest.java
@@ -2,6 +2,8 @@
 
 import static java.nio.ByteOrder.LITTLE_ENDIAN;
 import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.junit.jupiter.api.Assertions.assertAll;
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
 import static org.junit.jupiter.api.Assertions.assertEquals;
 
 import java.nio.ByteBuffer;
@@ -126,6 +128,23 @@
   }
 
   @Test
+  void testNestedStructureVariant() {
+    var data =
+        new byte[] {
+          0x69, 0x63, 0x61, 0x6E, 0x00, 0x68, 0x61, 0x73, 0x00, 0x73, 0x74, 0x72, 0x69, 0x6E, 0x67,
+          0x73, 0x3F, 0x00, 0x04, 0x0d, 0x05, 0x00, 0x28, 0x28, 0x79, 0x73, 0x29, 0x61, 0x73, 0x29
+        };
+
+    var decoder = Decoder.ofVariant();
+    var result = (Object[]) decoder.decode(ByteBuffer.wrap(data));
+
+    assertAll(
+        () -> assertEquals(2, result.length),
+        () -> assertArrayEquals(new Object[] {(byte) 0x69, "can"}, (Object[]) result[0]),
+        () -> assertEquals(List.of("has", "strings?"), result[1]));
+  }
+
+  @Test
   void testSimpleStructure() {
     var data = new byte[] {0x60, 0x70};