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};