diff --git a/sdk/clientcore/core/checkstyle-suppressions.xml b/sdk/clientcore/core/checkstyle-suppressions.xml index fb96f9c57d74..321d99b2c49b 100644 --- a/sdk/clientcore/core/checkstyle-suppressions.xml +++ b/sdk/clientcore/core/checkstyle-suppressions.xml @@ -19,6 +19,7 @@ + diff --git a/sdk/clientcore/core/spotbugs-exclude.xml b/sdk/clientcore/core/spotbugs-exclude.xml index 6b3dfc376cd6..2858d26aad4c 100644 --- a/sdk/clientcore/core/spotbugs-exclude.xml +++ b/sdk/clientcore/core/spotbugs-exclude.xml @@ -35,7 +35,9 @@ + + @@ -280,6 +282,10 @@ + + + + diff --git a/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/GenericParameterizedType.java b/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/GenericParameterizedType.java new file mode 100644 index 000000000000..32523a90ce31 --- /dev/null +++ b/sdk/clientcore/core/src/main/java/io/clientcore/core/implementation/GenericParameterizedType.java @@ -0,0 +1,88 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package io.clientcore.core.implementation; + +import io.clientcore.core.instrumentation.logging.ClientLogger; + +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.util.Arrays; +import java.util.Objects; +import java.util.stream.Collectors; + +/** + * A {@link ParameterizedType} implementation that allows for reference type arguments. + */ +public final class GenericParameterizedType implements ParameterizedType { + private static final ClientLogger LOGGER = new ClientLogger(GenericParameterizedType.class); + + private final Class raw; + private final Type[] args; + private String cachedToString; + + /** + * Creates a new instance of {@link GenericParameterizedType}. + * + * @param raw The raw type. + * @param args The type arguments. + */ + public GenericParameterizedType(Class raw, Type... args) { + this.raw = raw; + + if (args == null) { + throw LOGGER.logThrowableAsError(new IllegalArgumentException("args cannot be null")); + } + + Type[] argsCopy = new Type[args.length]; + for (int i = 0; i < args.length; i++) { + if (args[i] == null) { + throw LOGGER.logThrowableAsError( + new IllegalArgumentException("args cannot contain null: null value in index " + i)); + } + argsCopy[i] = args[i]; + } + this.args = argsCopy; + } + + @Override + public Type[] getActualTypeArguments() { + return args; + } + + @Override + public Type getRawType() { + return raw; + } + + @Override + public Type getOwnerType() { + return null; + } + + @Override + public String toString() { + if (cachedToString == null) { + cachedToString = raw.getTypeName() + "<" + + Arrays.stream(args).map(Type::getTypeName).collect(Collectors.joining(", ")) + ">"; + } + return cachedToString; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + GenericParameterizedType that = (GenericParameterizedType) o; + return Objects.equals(raw, that.raw) && Objects.deepEquals(args, that.args); + } + + @Override + public int hashCode() { + return Objects.hash(raw, Arrays.hashCode(args)); + } +} diff --git a/sdk/clientcore/core/src/main/java/io/clientcore/core/util/Union.java b/sdk/clientcore/core/src/main/java/io/clientcore/core/util/Union.java new file mode 100644 index 000000000000..12920170d2c8 --- /dev/null +++ b/sdk/clientcore/core/src/main/java/io/clientcore/core/util/Union.java @@ -0,0 +1,357 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package io.clientcore.core.util; + +import io.clientcore.core.implementation.GenericParameterizedType; +import io.clientcore.core.instrumentation.logging.ClientLogger; + +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.function.Consumer; + +/** + * A class that represents a union of types. A type that is one of a finite set of sum types. + * This class is used to represent a union of types in a type-safe manner. + * + *

Create an instance

+ * + * + *
+ * Union union = Union.ofTypes(String.class, Integer.class);
+ * 
+ * + * + *

Create an instance from primitives

+ * + * + *
+ * Union unionPrimitives = Union.ofTypes(int.class, double.class);
+ * 
+ * + * + *

Create an instance from collections

+ * + * + *
+ * // GenericParameterizedType is a non-public helper class that allows us to specify a generic type with
+ * // a class and a type. User can define any similar class to achieve the same functionality.
+ * Union unionCollections = Union.ofTypes(
+ *     new GenericParameterizedType(List.class, String.class),
+ *     new GenericParameterizedType(List.class, Integer.class));
+ * 
+ * + * + *

Consume the value of the Union if it is of the expected type

+ * + * + *
+ * Union union = Union.ofTypes(String.class, Integer.class);
+ * union.setValue("Hello");
+ * Object value = union.getValue();
+ * // we can write an if-else block to consume the value in Java 8+, or switch pattern match in Java 17+
+ * if (value instanceof String) {
+ *     String s = (String) value;
+ *     System.out.println("String value: " + s);
+ * } else if (value instanceof Integer) {
+ *     Integer i = (Integer) value;
+ *     System.out.println("Integer value: " + i);
+ * } else {
+ *     throw new IllegalArgumentException("Unknown type: " + union.getCurrentType().getTypeName());
+ * }
+ * 
+ * + * + * or + * + * + *
+ * Union union = Union.ofTypes(String.class, Integer.class);
+ * union.setValue("Hello");
+ * union.tryConsume(
+ *     v -> System.out.println("String value: " + v), String.class);
+ * union.tryConsume(
+ *     v -> System.out.println("Integer value: " + v), Integer.class);
+ * 
+ * + * + */ +public final class Union { + private static final ClientLogger LOGGER = new ClientLogger(Union.class); + + private final List types; + private Object value; + private Type currentType; + + private Union(Type... types) { + if (types == null || types.length == 0) { + throw LOGGER.logThrowableAsError(new IllegalArgumentException("types cannot be null or empty")); + } + + ArrayList typeCopy = new ArrayList<>(types.length); + for (int i = 0; i < types.length; i++) { + final Type currentType = types[i]; + if (currentType == null) { + throw LOGGER.logThrowableAsError( + new IllegalArgumentException("types cannot contain null values: null value in index " + i)); + } else if (!(currentType instanceof Class || currentType instanceof ParameterizedType)) { + throw LOGGER.logThrowableAsError(new IllegalArgumentException( + String.format("types must be of type Class or ParameterizedType: type name is %s in index %d.", + currentType.getTypeName(), i))); + } + + typeCopy.add(types[i]); + } + this.types = Collections.unmodifiableList(typeCopy); + } + + /** + * Creates a new instance of {@link Union} with the provided types. + *

+ * Currently, the types can be of type {@link Class} or {@link ParameterizedType}. If the type is a {@link Class}, + * it represents a simple type. If the type is a {@link ParameterizedType}, it represents a generic type. + * For example, {@code List} would be represented as {@code new GenericParameterizedType(List.class, String.class)}. + *

+ * + * It throws {@link IllegalArgumentException} if: + *
    + *
  • value is not of one of the types in the union,
  • + *
  • value is null,
  • + *
  • types array is null or empty,
  • + *
  • types array contains a null value,
  • + *
  • types array contains a type that is not of type {@link Class} or {@link ParameterizedType}.
  • + *
+ * + * @param types The types of the union. + * @return A new instance of {@link Union}. + */ + public static Union ofTypes(Type... types) { + return new Union(types); + } + + /** + * Sets the value of the union. A new updated immutable union is returned. + * + * @param value The value of the union. + * @return A new updated immutable union. + * @throws IllegalArgumentException If the value is not of one of the types in the union. + */ + @SuppressWarnings("unchecked") + public Union setValue(Object value) { + if (value == null) { + this.value = null; + return this; + } + + for (Type type : types) { + if (isInstanceOfType(value, type) || isPrimitiveTypeMatch(value, type)) { + this.value = value; + this.currentType = type; + return this; + } + } + + throw LOGGER.logThrowableAsError(new IllegalArgumentException("Invalid type: " + value.getClass().getName())); + } + + /** + * Gets the type of the value. + * + * @return The type of the value. + */ + public Type getCurrentType() { + return currentType; + } + + /** + * Gets the types of the union. The types are unmodifiable. + * + * @return The types of the union. + */ + public List getSupportedTypes() { + return types; + } + + /** + * Gets the value of the union. + * + * @return The value of the union. + * @param The type of the value. + */ + @SuppressWarnings("unchecked") + public T getValue() { + return (T) value; + } + + /** + * Gets the value of the union if it is of the expected type. + * + * @param clazz The expected type of the value. + * @return The value of the union. + * @param The expected type of the value. + */ + @SuppressWarnings("unchecked") + public T getValue(Class clazz) { + if (clazz == currentType) { + return (T) value; + } + + if (clazz.isInstance(value)) { + return clazz.cast(value); + } + if (isPrimitiveTypeMatch(value, clazz)) { + return (T) value; + } + throw LOGGER.logThrowableAsError(new IllegalArgumentException("Value is not of type: " + clazz.getName())); + } + + /** + * Gets the value of the union if it is of the expected type. + * + * @param clazz The expected type of the value. + * @param genericTypes The generic types of the expected type. + * + * @return The value of the union. + * @param The expected type of the value. + */ + public T getValue(Class clazz, Class... genericTypes) { + return getValue(new GenericParameterizedType(clazz, genericTypes)); + } + + /** + * Gets the value of the union if it is of the expected type. + * + * @param type The expected type of the value. + * + * @return The value of the union. + * @param The expected type of the value. + */ + @SuppressWarnings("unchecked") + public T getValue(Type type) { + if (type == currentType) { + return (T) value; + } + + if (isInstanceOfType(value, type)) { + return (T) value; + } + throw LOGGER.logThrowableAsError(new IllegalArgumentException("Value is not of type: " + type.getTypeName())); + } + + /** + * This method is used to consume the value of the Union if it is of the expected type. + * + * @param consumer A consumer that will consume the value of the Union if it is of the expected type. + * @param clazz The expected type of the value. + * @return Returns true if the value was consumable by the consumer, and false if it was not. + * @param The value type expected by the consumer. + */ + @SuppressWarnings("unchecked") + public boolean tryConsume(Consumer consumer, Class clazz) { + if (clazz == currentType) { + consumer.accept((T) value); + return true; + } + + if (isInstanceOfType(value, clazz)) { + consumer.accept(clazz.cast(value)); + return true; + } + + if (isPrimitiveTypeMatch(value, clazz)) { + consumer.accept((T) value); + return true; + } + return false; + } + + /** + * This method is used to consume the value of the Union if it is of the expected type. + * + * @param consumer A consumer that will consume the value of the Union if it is of the expected type. + * @param clazz The expected type of the value. + * @param genericTypes A var-args representation of generic types that are expected by the consumer, for example, + * List<String> would be represented as
List.class, String.class
. + * @return Returns true if the value was consumable by the consumer, and false if it was not. + * @param The value type expected by the consumer. + */ + public boolean tryConsume(Consumer consumer, Class clazz, Class... genericTypes) { + return tryConsume(consumer, new GenericParameterizedType(clazz, genericTypes)); + } + + /** + * This method is used to consume the value of the Union if it is of the expected type. + * + * @param consumer A consumer that will consume the value of the Union if it is of the expected type. + * @param type The expected type of the value. + * @return Returns true if the value was consumable by the consumer, and false if it was not. + * @param The value type expected by the consumer. + */ + @SuppressWarnings("unchecked") + public boolean tryConsume(Consumer consumer, ParameterizedType type) { + if (type == currentType) { + consumer.accept((T) value); + return true; + } + + if (isInstanceOfType(value, type)) { + consumer.accept((T) value); + return true; + } + return false; + } + + @Override + public String toString() { + return value == null + ? "Union{types=" + types + ", value=null" + "}" + : "Union{types=" + types + ", type=" + (currentType == null ? null : currentType.getTypeName()) + ", value=" + + value + "}"; + } + + private boolean isInstanceOfType(Object value, Type type) { + if (value == null) { + return false; + } + + if (type instanceof ParameterizedType) { + ParameterizedType pType = (ParameterizedType) type; + if (pType.getRawType() instanceof Class && ((Class) pType.getRawType()).isInstance(value)) { + Type[] actualTypeArguments = pType.getActualTypeArguments(); + if (value instanceof Collection) { + Collection collection = (Collection) value; + return collection.stream() + .allMatch(element -> element != null + && Arrays.stream(actualTypeArguments).anyMatch(arg -> isInstanceOfType(element, arg))); + } + } + } else if (type instanceof Class) { + return ((Class) type).isInstance(value); + } + return false; + } + + private boolean isPrimitiveTypeMatch(Object value, Type type) { + if (type instanceof Class) { + Class clazz = (Class) type; + if (clazz.isPrimitive()) { + if ((clazz == int.class && value instanceof Integer) + || (clazz == long.class && value instanceof Long) + || (clazz == float.class && value instanceof Float) + || (clazz == double.class && value instanceof Double) + || (clazz == boolean.class && value instanceof Boolean) + || (clazz == byte.class && value instanceof Byte) + || (clazz == char.class && value instanceof Character) + || (clazz == short.class && value instanceof Short)) { + return true; + } + } + } + return false; + } +} diff --git a/sdk/clientcore/core/src/samples/java/io/clientcore/core/util/union/BasicUnion.java b/sdk/clientcore/core/src/samples/java/io/clientcore/core/util/union/BasicUnion.java new file mode 100644 index 000000000000..2567886c301b --- /dev/null +++ b/sdk/clientcore/core/src/samples/java/io/clientcore/core/util/union/BasicUnion.java @@ -0,0 +1,60 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package io.clientcore.core.util.union; + +import io.clientcore.core.util.Union; + +// This is a simple example of how to use the Union type. It allows for multiple types to be stored in a single +// property, and provides methods to consume the value based on its type. +public class BasicUnion { + public static void main(String[] args) { + Union union = Union.ofTypes(String.class, Integer.class, Double.class); + + // This union allows for String, Integer, and Double types + union.setValue("Hello"); + + // we can (attempt to) exhaustively consume the union using a switch statement... + handleUnion(union); + + // ... or we can pass in lambda expressions to consume the union for the types we care about + union.tryConsume(v -> System.out.println("String value from lambda: " + v), String.class); + + // ... or we can just get the value to the type we expect it to be using the getValue methods + String value = union.getValue(); + System.out.println("Value (from getValue()): " + value); + + value = union.getValue(String.class); + System.out.println("Value (from getValue(Class cls)): " + value); + + // Of course, this union supports Integer and Double types as well: + union.setValue(123); + handleUnion(union); + union.setValue(3.14); + handleUnion(union); + + // This will throw an IllegalArgumentException, as the union does not support the type Long + try { + union.setValue(123L); + } catch (IllegalArgumentException e) { + System.out.println("Caught exception: " + e.getMessage()); + } + } + + private static void handleUnion(Union union) { + // we can write an if-else block to consume the value in Java 8+, or switch pattern match in Java 17+ + Object value = union.getValue(); + if (value instanceof String) { + String s = (String) value; + System.out.println("String value from if-else: " + s); + } else if (value instanceof Integer) { + Integer i = (Integer) value; + System.out.println("Integer value from if-else: " + i); + } else if (value instanceof Double) { + Double d = (Double) value; + System.out.println("Double value from if-else: " + d); + } else { + throw new IllegalArgumentException("Unknown type: " + union.getCurrentType().getTypeName()); + } + } +} diff --git a/sdk/clientcore/core/src/samples/java/io/clientcore/core/util/union/GenericModelType.java b/sdk/clientcore/core/src/samples/java/io/clientcore/core/util/union/GenericModelType.java new file mode 100644 index 000000000000..a73a7569757f --- /dev/null +++ b/sdk/clientcore/core/src/samples/java/io/clientcore/core/util/union/GenericModelType.java @@ -0,0 +1,83 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package io.clientcore.core.util.union; + +import io.clientcore.core.implementation.GenericParameterizedType; +import io.clientcore.core.util.Union; + +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.util.Arrays; +import java.util.List; + +// This is an example of a Model class that uses the Union type to allow for multiple types to be stored in a single +// property, with the additional complexity that the types are all generic types (in this case, List, List, +// and List). +public class GenericModelType { + // We specify that the Union type can be one of three types: List, List, or List. + private Union prop = Union.ofTypes( + // GenericParameterizedType is a non-public helper class that allows us to specify a generic type with + // a class and a type. User can define any similar class to achieve the same functionality. + new GenericParameterizedType(List.class, String.class), + new GenericParameterizedType(List.class, Integer.class), + new GenericParameterizedType(List.class, Float.class)); + + // we give access to the Union type, so that the value can be modified and retrieved. + public Union getProp() { + return prop; + } + + // but our setter methods need to have more complex names, to differentiate them at runtime. + public GenericModelType setPropAsStrings(List strValues) { + prop.setValue(strValues); + return this; + } + public GenericModelType setPropAsIntegers(List intValues) { + prop.setValue(intValues); + return this; + } + public GenericModelType setPropAsFloats(List floatValues) { + prop.setValue(floatValues); + return this; + } + + public static void main(String[] args) { + GenericModelType model = new GenericModelType(); + model.setPropAsStrings(Arrays.asList("Hello", "World")); + + // in this case, it isn't possible to switch over the values easily (as we could in the ModelType class), as the + // types are all List types (and we would need to inspect the values inside the list to be sure). Instead, we + // can use the tryConsume method to consume the value if it is of the expected type. + List types = model.getProp().getSupportedTypes(); + for (Type type : types) { + if (type instanceof ParameterizedType) { + ParameterizedType parameterizedType = (ParameterizedType) type; + if (parameterizedType.getRawType() == List.class) { + Type actualType = parameterizedType.getActualTypeArguments()[0]; + if (actualType == String.class) { + model.getProp().tryConsume(strings -> System.out.println("Strings: " + strings), List.class, String.class); + break; + } else if (actualType == Integer.class) { + model.getProp().tryConsume(integers -> System.out.println("Integers: " + integers), List.class, Integer.class); + break; + } else if (actualType == Float.class) { + model.getProp().tryConsume(floats -> System.out.println("Floats: " + floats), List.class, Float.class); + break; + } + } + } + } + + // or, we can use the returned boolean value to determine if the value was consumed + if (model.getProp().tryConsume(integers -> System.out.println("Integers: " + integers), List.class, Integer.class)) { + System.out.println("Consumed as Integers"); + } else if (model.getProp().tryConsume(strings -> System.out.println("Strings: " + strings), List.class, String.class)) { + System.out.println("consumed as Strings"); + } else if (model.getProp().tryConsume(floats -> System.out.println("Floats: " + floats), List.class, Float.class)) { + System.out.println("consumed as Floats"); + } else { + System.out.println("Not consumed as Integers, Strings, Floats"); + } + } +} diff --git a/sdk/clientcore/core/src/samples/java/io/clientcore/core/util/union/ModelType.java b/sdk/clientcore/core/src/samples/java/io/clientcore/core/util/union/ModelType.java new file mode 100644 index 000000000000..ec4302e89537 --- /dev/null +++ b/sdk/clientcore/core/src/samples/java/io/clientcore/core/util/union/ModelType.java @@ -0,0 +1,61 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package io.clientcore.core.util.union; + +import io.clientcore.core.util.Union; + +// This is an example of a Model class that uses the Union type to allow for multiple types to be stored in a single +// property. This is useful when you have a property that can be one of a few types, but you want to ensure that the +// types are known at compile time, and that you can easily switch on the type of the value. +public class ModelType { + private Union prop = Union.ofTypes(String.class, Integer.class, Double.class); + + public Union getProp() { + return prop; + } + + // In this case, because all three values of the Union type are distinct, we can have three setter methods to + // modify the Union in a type-safe way. If the types were not distinct, we would need to use a single setter method + // that took an Object type, and then rely on the Union type to ensure that the value was of the correct type. + // This would be the case (as we see in GenericModelType) where there are multiple types of the same class, such as + // List, List, and List. + public ModelType setProp(String str) { + prop.setValue(str); + return this; + } + public ModelType setProp(Integer integer) { + prop.setValue(integer); + return this; + } + public ModelType setProp(Double dbl) { + prop.setValue(dbl); + return this; + } + + public static void main(String[] args) { + ModelType modelType = new ModelType(); + modelType.setProp(23); + + // we can just call the getValue(Class cls) method to get the value as the type we expect it to be + System.out.println(modelType.getProp().getValue(Integer.class)); + + // or we can use the tryConsume method + modelType.getProp().tryConsume(v -> System.out.println("Value from lambda: " + v), Integer.class); + + // or we can write an if-else block to consume the value in Java 8+, or switch pattern match in Java 17+ + Object value = modelType.getProp().getValue(); + if (value instanceof String) { + String s = (String) value; + System.out.println("String value from if-else: " + s); + } else if (value instanceof Integer) { + Integer i = (Integer) value; + System.out.println("Integer value from if-else: " + i); + } else if (value instanceof Double) { + Double d = (Double) value; + System.out.println("Double value from if-else: " + d); + } else { + throw new IllegalArgumentException("Unknown type: " + modelType.getProp().getCurrentType().getTypeName()); + } + } +} diff --git a/sdk/clientcore/core/src/samples/java/io/clientcore/core/util/union/NestedUnion.java b/sdk/clientcore/core/src/samples/java/io/clientcore/core/util/union/NestedUnion.java new file mode 100644 index 000000000000..9380e842b07d --- /dev/null +++ b/sdk/clientcore/core/src/samples/java/io/clientcore/core/util/union/NestedUnion.java @@ -0,0 +1,65 @@ +package io.clientcore.core.util.union; + +import io.clientcore.core.implementation.GenericParameterizedType; +import io.clientcore.core.util.Union; + +import java.util.Arrays; +import java.util.List; + +/** + * This is an example of a model class A that uses the Union type to allow for nested union type to be stored in a single + * property. + */ +public class NestedUnion { + public static void main(String[] args) { + NestedClassB nestedClassB = new NestedClassB(); + nestedClassB.setProp(Arrays.asList(1, 2, 3)); + System.out.println("Current Type of Nested Class B: " + nestedClassB.getProp().getCurrentType()); + System.out.println("Value from Nested Class B: " + + nestedClassB.getProp().getValue(new GenericParameterizedType(List.class, Integer.class))); + + ClassA outerClassA = new ClassA(); + outerClassA.setProp(nestedClassB); + NestedClassB nestedClassBFromA = outerClassA.getProp().getValue(NestedClassB.class); + System.out.println("Current Type of Nested Class B from Class A: " + nestedClassBFromA.getProp().getCurrentType()); + System.out.println("Value of Nested Class B from Class A: " + + nestedClassBFromA.getProp().getValue(new GenericParameterizedType(List.class, Integer.class))); + } + + private static class ClassA { + Union prop = Union.ofTypes(String.class, NestedClassB.class); + + public Union getProp() { + return prop; + } + + public ClassA setProp(String str) { + prop.setValue(str); + return this; + } + + // Nested Class B contains a Union type property. + public ClassA setProp(NestedClassB b) { + prop.setValue(b); + return this; + } + } + + private static class NestedClassB { + Union prop = Union.ofTypes(String.class, new GenericParameterizedType(List.class, Integer.class)); + + public Union getProp() { + return prop; + } + + public NestedClassB setProp(String str) { + prop.setValue(str); + return this; + } + + public NestedClassB setProp(List intList) { + prop.setValue(intList); + return this; + } + } +} diff --git a/sdk/clientcore/core/src/samples/java/io/clientcore/core/util/union/PrimitiveUnionType.java b/sdk/clientcore/core/src/samples/java/io/clientcore/core/util/union/PrimitiveUnionType.java new file mode 100644 index 000000000000..ac1d88a5251d --- /dev/null +++ b/sdk/clientcore/core/src/samples/java/io/clientcore/core/util/union/PrimitiveUnionType.java @@ -0,0 +1,64 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package io.clientcore.core.util.union; + + +import io.clientcore.core.util.Union; + +// This is an example of a Model class that uses the Union type to allow for multiple types to be stored in a single +// property. This is useful when you have a property that can be one of a few types, but you want to ensure that the +// types are known at compile time, and that you can easily switch on the type of the value. +public class PrimitiveUnionType { + private Union prop = Union.ofTypes(int.class, float.class, double.class); + + public Union getProp() { + return prop; + } + + // In this case, because all three values of the Union type are distinct, we can have three setter methods to + // modify the Union in a type-safe way. If the types were not distinct, we would need to use a single setter method + // that took an Object type, and then rely on the Union type to ensure that the value was of the correct type. + // This would be the case (as we see in GenericModelType) where there are multiple types of the same class, such as + // List, List, and List. + public PrimitiveUnionType setProp(int i) { + prop.setValue(i); + return this; + } + public PrimitiveUnionType setProp(float f) { + prop.setValue(f); + return this; + } + public PrimitiveUnionType setProp(double d) { + prop.setValue(d); + return this; + } + + public static void main(String[] args) { + PrimitiveUnionType modelType = new PrimitiveUnionType(); + modelType.setProp(23); + System.out.println(modelType.getProp().getCurrentType()); + + // we can just call the getValue(Class cls) method to get the value as the type we expect it to be + System.out.println(modelType.getProp().getValue(int.class)); + + // or we can use the tryConsume method + modelType.getProp().tryConsume(v -> System.out.println("Value from lambda: " + v), int.class); + + // or we can write an if-else block to consume the value in Java 8+, or switch pattern match in Java 17+ + // but the switch expression doesn't work directly - we need to rely on the autoboxing to save us (Integer works, int doesn't) + Object value = modelType.getProp().getValue(); + if (value instanceof String) { + String s = (String) value; + System.out.println("String value from if-else: " + s); + } else if (value instanceof Integer) { + Integer i = (Integer) value; + System.out.println("Integer value from if-else: " + i); + } else if (value instanceof Double) { + Double d = (Double) value; + System.out.println("Double value from if-else: " + d); + } else { + throw new IllegalArgumentException("Unknown type: " + modelType.getProp().getCurrentType().getTypeName()); + } + } +} diff --git a/sdk/clientcore/core/src/samples/java/io/clientcore/core/util/union/codesnippets/UnionJavaDocCodeSnippets.java b/sdk/clientcore/core/src/samples/java/io/clientcore/core/util/union/codesnippets/UnionJavaDocCodeSnippets.java new file mode 100644 index 000000000000..3f00d5ac6dba --- /dev/null +++ b/sdk/clientcore/core/src/samples/java/io/clientcore/core/util/union/codesnippets/UnionJavaDocCodeSnippets.java @@ -0,0 +1,64 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package io.clientcore.core.util.union.codesnippets; + +import io.clientcore.core.implementation.GenericParameterizedType; +import io.clientcore.core.util.Union; +import org.junit.jupiter.api.Test; + +import java.util.List; + +public class UnionJavaDocCodeSnippets { + + @Test + public void unionCreation() { + // BEGIN: io.clientcore.core.util.union.UnionJavaDocCodeSnippetsBasic + Union union = Union.ofTypes(String.class, Integer.class); + // END: io.clientcore.core.util.union.UnionJavaDocCodeSnippetsBasic + + // BEGIN: io.clientcore.core.util.union.UnionJavaDocCodeSnippetsPrimitiveType + Union unionPrimitives = Union.ofTypes(int.class, double.class); + // END: io.clientcore.core.util.union.UnionJavaDocCodeSnippetsPrimitiveType + + // BEGIN: io.clientcore.core.util.union.UnionJavaDocCodeSnippetsCollectionType + // GenericParameterizedType is a non-public helper class that allows us to specify a generic type with + // a class and a type. User can define any similar class to achieve the same functionality. + Union unionCollections = Union.ofTypes( + new GenericParameterizedType(List.class, String.class), + new GenericParameterizedType(List.class, Integer.class)); + // END: io.clientcore.core.util.union.UnionJavaDocCodeSnippetsCollectionType + } + + @Test + public void unionConsumeIfElseStatement() { + // BEGIN: io.clientcore.core.util.union.UnionJavaDocCodeSnippetsIfElseStatement + Union union = Union.ofTypes(String.class, Integer.class); + union.setValue("Hello"); + Object value = union.getValue(); + // we can write an if-else block to consume the value in Java 8+, or switch pattern match in Java 17+ + if (value instanceof String) { + String s = (String) value; + System.out.println("String value: " + s); + } else if (value instanceof Integer) { + Integer i = (Integer) value; + System.out.println("Integer value: " + i); + } else { + throw new IllegalArgumentException("Unknown type: " + union.getCurrentType().getTypeName()); + } + // END: io.clientcore.core.util.union.UnionJavaDocCodeSnippetsIfElseStatement + } + + @Test + public void unionConsumeLambda() { + // BEGIN: io.clientcore.core.util.union.UnionJavaDocCodeSnippetsLambda + Union union = Union.ofTypes(String.class, Integer.class); + union.setValue("Hello"); + union.tryConsume( + v -> System.out.println("String value: " + v), String.class); + union.tryConsume( + v -> System.out.println("Integer value: " + v), Integer.class); + // END: io.clientcore.core.util.union.UnionJavaDocCodeSnippetsLambda + } + +} diff --git a/sdk/clientcore/core/src/test/java/io/clientcore/core/util/UnionTests.java b/sdk/clientcore/core/src/test/java/io/clientcore/core/util/UnionTests.java new file mode 100644 index 000000000000..99e02e0f65c5 --- /dev/null +++ b/sdk/clientcore/core/src/test/java/io/clientcore/core/util/UnionTests.java @@ -0,0 +1,436 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package io.clientcore.core.util; + +import io.clientcore.core.implementation.GenericParameterizedType; +import org.junit.jupiter.api.Test; + +import java.lang.reflect.Type; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + +/** + * Test class for {@link Union}. + */ +public class UnionTests { + private static final String STRING_VALUE = "Hello, world!"; + private static final int INT_VALUE = 42; + private static final long LONG_VALUE = 42L; + private static final double DOUBLE_VALUE = 3.11d; + private static final float FLOAT_VALUE = 3.11f; + + private static final String[] STRING_ARRAY_VALUE = { "Hello", "world", "!" }; + private static final int[] INT_ARRAY_VALUE = { 1, 2, 3 }; + private static final long[] LONG_ARRAY_VALUE = { 1L, 2L, 3L }; + private static final float[] FLOAT_ARRAY_VALUE = { 1.1f, 2.2f, 3.3f }; + private static final double[] DOUBLE_ARRAY_VALUE = { 1.1d, 2.2d, 3.3d }; + + private static final GenericParameterizedType LIST_OF_STRING_TYPE + = new GenericParameterizedType(List.class, String.class); + private static final GenericParameterizedType LIST_OF_INTEGER_TYPE + = new GenericParameterizedType(List.class, Integer.class); + private static final GenericParameterizedType LIST_OF_LONG_TYPE + = new GenericParameterizedType(List.class, Long.class); + private static final GenericParameterizedType LIST_OF_FLOAT_TYPE + = new GenericParameterizedType(List.class, Float.class); + private static final GenericParameterizedType LIST_OF_DOUBLE_TYPE + = new GenericParameterizedType(List.class, Double.class); + private static final GenericParameterizedType SET_OF_STRING_TYPE + = new GenericParameterizedType(Set.class, String.class); + + private static final List LIST_OF_STRING_VALUE = Arrays.asList("Hello", "world", "!"); + private static final List LIST_OF_INTEGER_VALUE = Arrays.asList(1, 2, 3); + private static final List LIST_OF_LONG_VALUE = Arrays.asList(1L, 2L, 3L); + private static final List LIST_OF_FLOAT_VALUE = Arrays.asList(1.1f, 2.2f, 3.3f); + private static final List LIST_OF_DOUBLE_VALUE = Arrays.asList(1.1d, 2.2d, 3.3d); + + private static final Set SET_OF_STRING_VALUE + = Collections.unmodifiableSet(new HashSet<>(Arrays.asList("Hello", "world", "!"))); + + @Test + void createUnionWithMultipleTypes() { + Union union = Union.ofTypes(String.class, Integer.class, Double.class); + assertNotNull(union); + assertEquals(3, union.getSupportedTypes().size()); + } + + @Test + void setAndGetValue() { + Union union = Union.ofTypes(String.class, Integer.class, Double.class); + + union.setValue(STRING_VALUE); + assertEquals(String.class, union.getCurrentType()); + assertEquals(STRING_VALUE, union.getValue()); + + union.setValue(INT_VALUE); + assertEquals(Integer.class, union.getCurrentType()); + assertEquals(INT_VALUE, union.getValue(Integer.class)); + + union.setValue(DOUBLE_VALUE); + assertEquals(Double.class, union.getCurrentType()); + assertEquals(DOUBLE_VALUE, union.getValue(Double.class)); + } + + @Test + void setValueWithInvalidType() { + Union union = Union.ofTypes(String.class, Integer.class, Double.class); + assertThrows(IllegalArgumentException.class, () -> union.setValue(LONG_VALUE)); + assertThrows(IllegalArgumentException.class, () -> union.setValue(FLOAT_VALUE)); + } + + @Test + void tryConsumeWithValidType() { + Union union = Union.ofTypes(String.class, Integer.class, Double.class); + union.setValue(STRING_VALUE); + assertTrue(union.tryConsume(value -> assertEquals(STRING_VALUE, value), String.class)); + union.setValue(INT_VALUE); + assertTrue(union.tryConsume(value -> assertEquals(INT_VALUE, value), Integer.class)); + union.setValue(DOUBLE_VALUE); + assertTrue(union.tryConsume(value -> assertEquals(DOUBLE_VALUE, value), Double.class)); + } + + @Test + void tryConsumeWithInvalidType() { + Union union = Union.ofTypes(String.class, Integer.class, Double.class); + + union.setValue(INT_VALUE); + assertTrue(union.tryConsume(value -> assertEquals(INT_VALUE, value), Integer.class)); + assertFalse(union.tryConsume(value -> fail("Should not consume String"), String.class)); + assertFalse(union.tryConsume(value -> fail("Should not consume Double"), Double.class)); + assertFalse(union.tryConsume(value -> fail("Should not consume Long"), Long.class)); + assertFalse(union.tryConsume(value -> fail("Should not consume Float"), Float.class)); + } + + // Autoboxing tests + + @Test + void createUnionWithMultipleTypesAutoboxing() { + Union union = Union.ofTypes(String.class, int.class, double.class); + union.setValue(STRING_VALUE); + assertEquals(String.class, union.getCurrentType()); + assertEquals(STRING_VALUE, union.getValue()); + + union.setValue(42); + assertEquals(int.class, union.getCurrentType()); + assertEquals(42, union.getValue(int.class)); + + union.setValue(DOUBLE_VALUE); + assertEquals(double.class, union.getCurrentType()); + assertEquals(DOUBLE_VALUE, union.getValue(double.class)); + } + + @Test + void setAndGetValueAutoboxing() { + Union union = Union.ofTypes(String.class, double.class, int.class); + union.setValue(STRING_VALUE); + assertEquals(String.class, union.getCurrentType()); + assertEquals(STRING_VALUE, union.getValue(String.class)); + + union.setValue(INT_VALUE); + assertEquals(int.class, union.getCurrentType()); + assertEquals(INT_VALUE, union.getValue(int.class)); + + union.setValue(DOUBLE_VALUE); + assertEquals(double.class, union.getCurrentType()); + assertEquals(DOUBLE_VALUE, union.getValue(double.class)); + } + + @Test + void setValueWithInvalidTypeAutoboxing() { + Union union = Union.ofTypes(String.class, int.class, double.class); + assertThrows(IllegalArgumentException.class, () -> union.setValue(FLOAT_VALUE)); + } + + @Test + void tryConsumeWithValidTypeAutoboxing() { + Union union = Union.ofTypes(String.class, int.class, double.class); + + union.setValue(STRING_VALUE); + assertEquals(String.class, union.getCurrentType()); + assertTrue(union.tryConsume(value -> assertEquals(STRING_VALUE, value), String.class)); + + union.setValue(INT_VALUE); + assertEquals(int.class, union.getCurrentType()); + assertTrue(union.tryConsume(value -> assertEquals(INT_VALUE, value), int.class)); + + union.setValue(DOUBLE_VALUE); + assertEquals(double.class, union.getCurrentType()); + assertTrue(union.tryConsume(value -> assertEquals(DOUBLE_VALUE, value), double.class)); + } + + @Test + void tryConsumeWithInvalidTypeAutoboxing() { + Union union = Union.ofTypes(String.class, int.class, double.class); + union.setValue(INT_VALUE); + assertTrue(union.tryConsume(value -> assertEquals(INT_VALUE, value), int.class)); + assertFalse(union.tryConsume(value -> fail("Should not consume String"), String.class)); + assertFalse(union.tryConsume(value -> fail("Should not consume Double"), Double.class)); + assertFalse(union.tryConsume(value -> fail("Should not consume Long"), Long.class)); + assertFalse(union.tryConsume(value -> fail("Should not consume Float"), Float.class)); + } + + // Array types tests + + @Test + void createUnionWithArrayTypes() { + Union union = Union.ofTypes(String[].class, int[].class, float[].class); + assertNotNull(union); + assertEquals(3, union.getSupportedTypes().size()); + } + + @Test + void setAndGetValueWithArrayTypes() { + Union union = Union.ofTypes(String[].class, int[].class, float[].class); + + union.setValue(STRING_ARRAY_VALUE); + assertEquals(String[].class, union.getCurrentType()); + assertArrayEquals(STRING_ARRAY_VALUE, union.getValue(String[].class)); + + union.setValue(INT_ARRAY_VALUE); + assertEquals(int[].class, union.getCurrentType()); + assertArrayEquals(INT_ARRAY_VALUE, union.getValue(int[].class)); + + union.setValue(FLOAT_ARRAY_VALUE); + assertEquals(float[].class, union.getCurrentType()); + assertArrayEquals(FLOAT_ARRAY_VALUE, union.getValue(float[].class)); + } + + @Test + void setValueWithInvalidArrayType() { + Union union = Union.ofTypes(String[].class, int[].class, float[].class); + assertThrows(IllegalArgumentException.class, () -> union.setValue(LONG_ARRAY_VALUE)); + assertThrows(IllegalArgumentException.class, () -> union.setValue(DOUBLE_ARRAY_VALUE)); + } + + @Test + void tryConsumeWithValidArrayType() { + Union union = Union.ofTypes(String[].class, int[].class, float[].class); + + union.setValue(STRING_ARRAY_VALUE); + assertTrue(union.tryConsume(value -> assertArrayEquals(STRING_ARRAY_VALUE, value), String[].class)); + + union.setValue(INT_ARRAY_VALUE); + assertTrue(union.tryConsume(value -> assertArrayEquals(INT_ARRAY_VALUE, value), int[].class)); + + union.setValue(FLOAT_ARRAY_VALUE); + assertTrue(union.tryConsume(value -> assertArrayEquals(FLOAT_ARRAY_VALUE, value), float[].class)); + } + + @Test + void tryConsumeWithInvalidArrayType() { + Union union = Union.ofTypes(String[].class, int[].class, float[].class); + union.setValue(INT_ARRAY_VALUE); + + assertTrue(union.tryConsume(value -> assertArrayEquals(INT_ARRAY_VALUE, value), int[].class)); + assertFalse(union.tryConsume(value -> fail("Should not consume Double"), double[].class)); + assertFalse(union.tryConsume(value -> fail("Should not consume Long"), long[].class)); + } + + // Parameterized types tests + + @Test + void createUnionWithParameterizedTypes() { + Union union = Union.ofTypes(LIST_OF_STRING_TYPE, LIST_OF_INTEGER_TYPE, LIST_OF_DOUBLE_TYPE); + assertNotNull(union); + assertEquals(3, union.getSupportedTypes().size()); + } + + @Test + void setAndGetValueWithParameterizedTypes() { + Union union = Union.ofTypes(LIST_OF_STRING_TYPE, LIST_OF_INTEGER_TYPE, LIST_OF_DOUBLE_TYPE); + + union.setValue(LIST_OF_STRING_VALUE); + assertEquals(LIST_OF_STRING_TYPE, union.getCurrentType()); + assertEquals(LIST_OF_STRING_VALUE, union.getValue(LIST_OF_STRING_TYPE)); + + union.setValue(LIST_OF_INTEGER_VALUE); + assertEquals(LIST_OF_INTEGER_TYPE, union.getCurrentType()); + assertEquals(LIST_OF_INTEGER_VALUE, union.getValue(LIST_OF_INTEGER_TYPE)); + + union.setValue(LIST_OF_DOUBLE_VALUE); + assertEquals(LIST_OF_DOUBLE_TYPE, union.getCurrentType()); + assertEquals(LIST_OF_DOUBLE_VALUE, union.getValue(LIST_OF_DOUBLE_TYPE)); + } + + @Test + void setValueWithInvalidParameterizedType() { + Union union = Union.ofTypes(LIST_OF_STRING_TYPE, LIST_OF_INTEGER_TYPE, LIST_OF_DOUBLE_TYPE); + + assertThrows(IllegalArgumentException.class, () -> union.setValue(LIST_OF_LONG_VALUE)); + assertThrows(IllegalArgumentException.class, () -> union.setValue(LIST_OF_FLOAT_VALUE)); + assertThrows(IllegalArgumentException.class, () -> union.setValue(SET_OF_STRING_VALUE)); + } + + @Test + void tryConsumeWithValidParameterizedType() { + Union union = Union.ofTypes(LIST_OF_STRING_TYPE, LIST_OF_INTEGER_TYPE, LIST_OF_DOUBLE_TYPE); + + union.setValue(LIST_OF_STRING_VALUE); + assertTrue(union.tryConsume(value -> assertEquals(LIST_OF_STRING_VALUE, value), LIST_OF_STRING_TYPE)); + + union.setValue(LIST_OF_INTEGER_VALUE); + assertTrue(union.tryConsume(value -> assertEquals(LIST_OF_INTEGER_VALUE, value), LIST_OF_INTEGER_TYPE)); + + union.setValue(LIST_OF_DOUBLE_VALUE); + assertTrue(union.tryConsume(value -> assertEquals(LIST_OF_DOUBLE_VALUE, value), LIST_OF_DOUBLE_TYPE)); + } + + @Test + void tryConsumeWithInvalidParameterizedType() { + Union union = Union.ofTypes(LIST_OF_STRING_TYPE, LIST_OF_INTEGER_TYPE, LIST_OF_DOUBLE_TYPE, LIST_OF_FLOAT_TYPE); + + union.setValue(LIST_OF_INTEGER_VALUE); + assertTrue(union.tryConsume(value -> assertEquals(LIST_OF_INTEGER_VALUE, value), LIST_OF_INTEGER_TYPE)); + assertFalse(union.tryConsume(value -> fail("Should not consume List"), LIST_OF_STRING_TYPE)); + assertFalse(union.tryConsume(value -> fail("Should not consume List"), LIST_OF_LONG_TYPE)); + assertFalse(union.tryConsume(value -> fail("Should not consume List"), LIST_OF_FLOAT_TYPE)); + assertFalse(union.tryConsume(value -> fail("Should not consume List"), LIST_OF_DOUBLE_TYPE)); + assertFalse(union.tryConsume(value -> fail("Should not consume Set"), SET_OF_STRING_TYPE)); + } + + // Additional tests + @Test + void createUnionWithNullTypes() { + assertThrows(IllegalArgumentException.class, () -> Union.ofTypes((Type) null)); + assertThrows(IllegalArgumentException.class, () -> Union.ofTypes(String.class, null, int.class)); + } + + @Test + void setAndGetValueWithNull() { + Union union = Union.ofTypes(String.class, Integer.class, Double.class); + union.setValue(null); + assertNull(union.getValue()); + } + + @Test + void setAndGetValueWithEmptyCollection() { + Union union = Union.ofTypes(LIST_OF_STRING_TYPE); + List emptyList = Arrays.asList(); + union.setValue(emptyList); + assertEquals(LIST_OF_STRING_TYPE, union.getCurrentType()); + assertEquals(emptyList, union.getValue(LIST_OF_STRING_TYPE)); + } + + @Test + void setValueWithMixedTypeCollection() { + Union union = Union.ofTypes(LIST_OF_STRING_TYPE); + + List mixedList = Arrays.asList("Hello", 1); + assertThrows(IllegalArgumentException.class, () -> union.setValue(mixedList)); + } + + @Test + void setAndGetValueWithNestedParameterizedType() { + GenericParameterizedType listOfListOfString + = new GenericParameterizedType(List.class, new GenericParameterizedType(List.class, String.class)); + Union union = Union.ofTypes(listOfListOfString); + + List> nestedList = Arrays.asList(LIST_OF_STRING_VALUE); + union.setValue(nestedList); + assertEquals(listOfListOfString, union.getCurrentType()); + assertEquals(nestedList, union.getValue(listOfListOfString)); + } + + @Test + void setAndGetValueWithPrimitiveArray() { + Union union = Union.ofTypes(int[].class, double[].class); + + union.setValue(INT_ARRAY_VALUE); + assertEquals(int[].class, union.getCurrentType()); + assertArrayEquals(INT_ARRAY_VALUE, union.getValue(int[].class)); + + union.setValue(DOUBLE_ARRAY_VALUE); + assertEquals(double[].class, union.getCurrentType()); + assertArrayEquals(DOUBLE_ARRAY_VALUE, union.getValue(double[].class)); + } + + @Test + void setAndGetValueWithDeeplyNestedParameterizedType() { + GenericParameterizedType listOfListOfListOfString + = new GenericParameterizedType(List.class, new GenericParameterizedType(List.class, LIST_OF_STRING_TYPE)); + Union union = Union.ofTypes(listOfListOfListOfString); + + List>> deeplyNestedList = Arrays.asList(Arrays.asList(LIST_OF_STRING_VALUE)); + union.setValue(deeplyNestedList); + assertEquals(listOfListOfListOfString, union.getCurrentType()); + assertEquals(deeplyNestedList, union.getValue(listOfListOfListOfString)); + } + + @Test + void setAndGetValueWithMixedParameterizedTypesAndClass() { + Union union = Union.ofTypes(LIST_OF_STRING_TYPE, SET_OF_STRING_TYPE, String.class); + + union.setValue(LIST_OF_STRING_VALUE); + assertEquals(LIST_OF_STRING_TYPE, union.getCurrentType()); + assertEquals(LIST_OF_STRING_VALUE, union.getValue(LIST_OF_STRING_TYPE)); + + union.setValue(SET_OF_STRING_VALUE); + assertEquals(SET_OF_STRING_TYPE, union.getCurrentType()); + assertEquals(SET_OF_STRING_VALUE, union.getValue(SET_OF_STRING_TYPE)); + + union.setValue(STRING_VALUE); + assertEquals(String.class, union.getCurrentType()); + assertEquals(STRING_VALUE, union.getValue(String.class)); + } + + @Test + void unionWithMap() { + Union union = Union.ofTypes(String.class, Integer.class, Map.class); + String key = "key"; + String value = "value"; + Map map = Collections.singletonMap(key, value); + union.setValue(map); + + assertEquals(Map.class, union.getCurrentType()); + assertEquals(map, union.getValue(Map.class)); + + union.tryConsume(map1 -> assertEquals(value, map1.get(key)), Map.class); + + assertFalse(union.tryConsume(ignore -> fail("Should not consume List"), LIST_OF_FLOAT_TYPE)); + } + + @Test + void unionWithBooleanShortBytes() { + Union union = Union.ofTypes(Boolean.class, Short.class, Byte.class); + union.setValue(true); + assertEquals(Boolean.class, union.getCurrentType()); + assertEquals(true, union.getValue(boolean.class)); + + union.setValue((short) 1); + assertEquals(Short.class, union.getCurrentType()); + assertEquals((short) 1, union.getValue(Short.class)); + union.setValue((byte) 1); + assertEquals(Byte.class, union.getCurrentType()); + assertEquals((byte) 1, union.getValue(Byte.class)); + + assertFalse(union.tryConsume(ignore -> fail("Should not consume List"), LIST_OF_FLOAT_TYPE)); + } + + @Test + void unionWithNestedUnion() { + Union union = Union.ofTypes(String.class, Union.class); + Union nestedUnion = Union.ofTypes(String.class, Integer.class, Double.class); + nestedUnion = nestedUnion.setValue(STRING_VALUE); + union.setValue(nestedUnion); + + assertEquals(Union.class, union.getCurrentType()); + Union unionValue = union.getValue(Union.class); + assertNotNull(unionValue); + assertEquals(nestedUnion, unionValue); + assertEquals(STRING_VALUE, unionValue.getValue()); + } +}