From 2bc0333f855dc56eeca14da8bf78632625aa2608 Mon Sep 17 00:00:00 2001 From: JonasG Date: Thu, 7 Mar 2024 17:11:07 +0100 Subject: [PATCH] feat: fail on unknown enum value Configures the XjxSerdes to fail when an enum value cannot be mapped to an enum constant. When not set, defaults to false and will default to null when a value cannot be mapped to a name. --- .../io/jonasg/xjx/serdes/TypeMappers.java | 13 +- .../java/io/jonasg/xjx/serdes/XjxSerdes.java | 30 ++- .../deserialize/DeserializationFeature.java | 6 + .../deserialize/PathBasedSaxHandler.java | 15 +- .../deserialize/PathWriterIndexFactory.java | 42 ++-- .../deserialize/TypedValueMapSaxHandler.java | 10 +- .../deserialize/accessor/FieldAccessor.java | 7 +- .../accessor/RecordFieldAccessor.java | 8 +- .../config/ConfigurationBuilder.java | 24 ++ .../deserialize/config/XjxConfiguration.java | 14 ++ .../deserialize/EnumDeserializationTest.java | 227 ++++++++++++------ 11 files changed, 282 insertions(+), 114 deletions(-) create mode 100644 xjx-serdes/src/main/java/io/jonasg/xjx/serdes/deserialize/DeserializationFeature.java create mode 100644 xjx-serdes/src/main/java/io/jonasg/xjx/serdes/deserialize/config/ConfigurationBuilder.java create mode 100644 xjx-serdes/src/main/java/io/jonasg/xjx/serdes/deserialize/config/XjxConfiguration.java diff --git a/xjx-serdes/src/main/java/io/jonasg/xjx/serdes/TypeMappers.java b/xjx-serdes/src/main/java/io/jonasg/xjx/serdes/TypeMappers.java index aac5f4c..c6cab03 100644 --- a/xjx-serdes/src/main/java/io/jonasg/xjx/serdes/TypeMappers.java +++ b/xjx-serdes/src/main/java/io/jonasg/xjx/serdes/TypeMappers.java @@ -1,5 +1,8 @@ package io.jonasg.xjx.serdes; +import io.jonasg.xjx.serdes.deserialize.XjxDeserializationException; +import io.jonasg.xjx.serdes.deserialize.config.XjxConfiguration; + import java.math.BigDecimal; import java.time.LocalDate; import java.time.LocalDateTime; @@ -30,7 +33,7 @@ public final class TypeMappers { TYPES.add(LocalDate.class); } - public static Function forType(Class type) { + public static Function forType(Class type, XjxConfiguration configuration) { Function mapper = Function.identity(); if (type.equals(String.class)) { mapper = String::valueOf; @@ -69,7 +72,13 @@ public static Function forType(Class type) { mapper = value -> ZonedDateTime.parse(String.valueOf(value)); } if (type.isEnum()) { - mapper = value -> toEnum(type, String.valueOf(value)); + mapper = value -> { + Object enumValue = toEnum(type, String.valueOf(value)); + if (enumValue == null && configuration.failOnUnknownEnumValue()) { + throw new XjxDeserializationException("Cannot map value '" + value + "' to enum " + type.getSimpleName()); + } + return enumValue; + }; } return mapper; } diff --git a/xjx-serdes/src/main/java/io/jonasg/xjx/serdes/XjxSerdes.java b/xjx-serdes/src/main/java/io/jonasg/xjx/serdes/XjxSerdes.java index 7f255ff..2722009 100644 --- a/xjx-serdes/src/main/java/io/jonasg/xjx/serdes/XjxSerdes.java +++ b/xjx-serdes/src/main/java/io/jonasg/xjx/serdes/XjxSerdes.java @@ -4,13 +4,16 @@ import java.io.StringReader; import java.util.HashMap; import java.util.Map; +import java.util.function.Consumer; import io.jonasg.xjx.sax.SaxParser; +import io.jonasg.xjx.serdes.deserialize.config.ConfigurationBuilder; import io.jonasg.xjx.serdes.deserialize.MapOf; import io.jonasg.xjx.serdes.deserialize.MapRootSaxHandler; import io.jonasg.xjx.serdes.deserialize.PathBasedSaxHandler; import io.jonasg.xjx.serdes.deserialize.PathWriterIndexFactory; import io.jonasg.xjx.serdes.deserialize.XjxDeserializationException; +import io.jonasg.xjx.serdes.deserialize.config.XjxConfiguration; import io.jonasg.xjx.serdes.serialize.XmlNodeStructureFactory; import io.jonasg.xjx.serdes.serialize.XmlStringBuilder; @@ -24,22 +27,37 @@ public class XjxSerdes { private final PathWriterIndexFactory pathWriterIndexFactory; private final XmlNodeStructureFactory xmlNodeStructureFactory = new XmlNodeStructureFactory(); + private final XmlStringBuilder xmlStringBuilder; - private XjxSerdes(SaxParser saxParser, PathWriterIndexFactory pathWriterIndexFactory, XmlStringBuilder xmlStringBuilder) { + private final XjxConfiguration configuration; + + private XjxSerdes(SaxParser saxParser, + XmlStringBuilder xmlStringBuilder, + Consumer configurationBuilder) { + this.configuration = new XjxConfiguration(); + configurationBuilder.accept(new ConfigurationBuilder(configuration)); this.saxParser = saxParser; - this.pathWriterIndexFactory = pathWriterIndexFactory; + this.pathWriterIndexFactory = new PathWriterIndexFactory(configuration); this.xmlStringBuilder = xmlStringBuilder; - } + } /** * Constructs an XjxSerdes instance with default configurations. */ public XjxSerdes() { - this(new SaxParser(), new PathWriterIndexFactory(), new XmlStringBuilder()); + this(new SaxParser(), new XmlStringBuilder(), (builder) -> {}); } - /** + /** + * Constructs an XjxSerdes instance with custom configurations. + * @param configurationBuilder The configuration builder to configure the XjxSerdes instance. + */ + public XjxSerdes(Consumer configurationBuilder) { + this(new SaxParser(), new XmlStringBuilder(), configurationBuilder); + } + + /** * Reads XML data and deserializes it into an object of the specified class. * * @param data The XML data to read. @@ -60,7 +78,7 @@ public T read(String data, Class clazz) { * @return The deserialized object. */ public T read(Reader data, Class clazz) { - PathBasedSaxHandler saxHandler = new PathBasedSaxHandler<>((rootTag) -> pathWriterIndexFactory.createIndexForType(clazz, rootTag)); + PathBasedSaxHandler saxHandler = new PathBasedSaxHandler<>((rootTag) -> pathWriterIndexFactory.createIndexForType(clazz, rootTag), this.configuration); saxParser.parse(data, saxHandler); return saxHandler.instance(); } diff --git a/xjx-serdes/src/main/java/io/jonasg/xjx/serdes/deserialize/DeserializationFeature.java b/xjx-serdes/src/main/java/io/jonasg/xjx/serdes/deserialize/DeserializationFeature.java new file mode 100644 index 0000000..28c4e6e --- /dev/null +++ b/xjx-serdes/src/main/java/io/jonasg/xjx/serdes/deserialize/DeserializationFeature.java @@ -0,0 +1,6 @@ +package io.jonasg.xjx.serdes.deserialize; + +public enum DeserializationFeature { + FAIL_ON_UNMAPPABLE_ENUM_VALUE + +} diff --git a/xjx-serdes/src/main/java/io/jonasg/xjx/serdes/deserialize/PathBasedSaxHandler.java b/xjx-serdes/src/main/java/io/jonasg/xjx/serdes/deserialize/PathBasedSaxHandler.java index 2deff16..6909396 100644 --- a/xjx-serdes/src/main/java/io/jonasg/xjx/serdes/deserialize/PathBasedSaxHandler.java +++ b/xjx-serdes/src/main/java/io/jonasg/xjx/serdes/deserialize/PathBasedSaxHandler.java @@ -9,12 +9,15 @@ import io.jonasg.xjx.sax.Attribute; import io.jonasg.xjx.sax.SaxHandler; import io.jonasg.xjx.serdes.Path; +import io.jonasg.xjx.serdes.deserialize.config.XjxConfiguration; public class PathBasedSaxHandler implements SaxHandler { private final Function> indexSupplier; - private final LinkedList objectInstances = new LinkedList<>(); + private final XjxConfiguration configuration; + + private final LinkedList objectInstances = new LinkedList<>(); private String rootTag; @@ -28,13 +31,15 @@ public class PathBasedSaxHandler implements SaxHandler { private String mapStartTag; - public PathBasedSaxHandler(Function> indexSupplier) { + public PathBasedSaxHandler(Function> indexSupplier, XjxConfiguration configuration) { this.indexSupplier = indexSupplier; - } + this.configuration = configuration; + } - public PathBasedSaxHandler(Function> indexSupplier, String rootTag) { + public PathBasedSaxHandler(Function> indexSupplier, String rootTag, XjxConfiguration configuration) { this.indexSupplier = indexSupplier; this.rootTag = rootTag; + this.configuration = configuration; handleRootTag(rootTag); } @@ -60,7 +65,7 @@ public void startTag(String namespace, String name, List attributes) this.mapRootSaxHandlerDelegate = new MapRootSaxHandler((HashMap) object); this.mapStartTag = name; } else if (object instanceof MapWithTypeInfo mapWithTypeInfo) { - this.mapRootSaxHandlerDelegate = new TypedValueMapSaxHandler(mapWithTypeInfo); + this.mapRootSaxHandlerDelegate = new TypedValueMapSaxHandler(mapWithTypeInfo, configuration); this.mapStartTag = name; } this.objectInstances.push(object); diff --git a/xjx-serdes/src/main/java/io/jonasg/xjx/serdes/deserialize/PathWriterIndexFactory.java b/xjx-serdes/src/main/java/io/jonasg/xjx/serdes/deserialize/PathWriterIndexFactory.java index b71cc8e..bd1bf23 100644 --- a/xjx-serdes/src/main/java/io/jonasg/xjx/serdes/deserialize/PathWriterIndexFactory.java +++ b/xjx-serdes/src/main/java/io/jonasg/xjx/serdes/deserialize/PathWriterIndexFactory.java @@ -21,6 +21,7 @@ import io.jonasg.xjx.serdes.Tag; import io.jonasg.xjx.serdes.TypeMappers; import io.jonasg.xjx.serdes.deserialize.accessor.FieldAccessor; +import io.jonasg.xjx.serdes.deserialize.config.XjxConfiguration; import io.jonasg.xjx.serdes.reflector.FieldReflector; import io.jonasg.xjx.serdes.reflector.TypeReflector; @@ -29,9 +30,16 @@ public class PathWriterIndexFactory { public static final List> BASIC_TYPES = List.of( String.class, Integer.class, int.class, Boolean.class, boolean.class, Long.class, long.class, BigDecimal.class, Double.class, double.class, char.class, Character.class, LocalDate.class, LocalDateTime.class, ZonedDateTime.class); - private final Map, Object> collectionCacheType = new HashMap<>(); - public Map createIndexForType(Class type, String rootTag) { + private final XjxConfiguration configuration; + + private final Map, Object> collectionCacheType = new HashMap<>(); + + public PathWriterIndexFactory(XjxConfiguration xjxConfiguration) { + this.configuration = xjxConfiguration; + } + + public Map createIndexForType(Class type, String rootTag) { Path path = Path.of(rootTag); return buildIndex(type, path); } @@ -82,7 +90,7 @@ private void indexRecordType(FieldReflector field, Map index, return recordWrapper; }).setValueInitializer((value) -> { if (value instanceof RecordWrapper recordWrapperValue) { - FieldAccessor.of(field, parent.get()).set(recordWrapperValue.record()); + FieldAccessor.of(field, parent.get(), configuration).set(recordWrapperValue.record()); } })); doBuildIndex(field.type(), getPathForField(field, path), index, () -> recordWrapper); @@ -97,14 +105,14 @@ private void indexMapType(FieldReflector field, Map index, Pat } } - private static void doIndexMapType(FieldReflector field, + private void doIndexMapType(FieldReflector field, Map index, Supplier parent, Path pathForField) { index.put(pathForField, PathWriter.objectInitializer(() -> { Map map = new HashMap<>(); Class valueType = (Class) ((ParameterizedType) field.genericType()).getActualTypeArguments()[1]; - FieldAccessor.of(field, parent.get()).set(map); + FieldAccessor.of(field, parent.get(), configuration).set(map); if (valueType.equals(Object.class)) { return map; } else { @@ -113,13 +121,13 @@ private static void doIndexMapType(FieldReflector field, })); } - private static void indexMapAsRootType(FieldReflector field, + private void indexMapAsRootType(FieldReflector field, Map index, Supplier parent, Path pathForField) { index.put(pathForField, PathWriter.rootInitializer(() -> { Map map = new HashMap<>(); - FieldAccessor.of(field, parent.get()).set(map); + FieldAccessor.of(field, parent.get(), configuration).set(map); return new MapAsRoot(parent.get(), map); })); } @@ -149,7 +157,7 @@ private void doIndexComplexType(FieldReflector field, Map inde index.put(getPathForField(field, path), PathWriter.valueInitializer((value) -> { value = ValueDeserializationHandler.getInstance().handle(field.rawField(), (String) value) .orElse(value); - FieldAccessor.of(field, parent.get()).set(value); + FieldAccessor.of(field, parent.get(), configuration).set(value); })); } else { Supplier complexTypeSupplier = () -> { @@ -158,7 +166,7 @@ private void doIndexComplexType(FieldReflector field, Map inde } Object complexType = TypeReflector.reflect(field.type()).instanceReflector().instance(); collectionCacheType.put(field.type(), complexType); - FieldAccessor.of(field, parent.get()).set(complexType); + FieldAccessor.of(field, parent.get(), configuration).set(complexType); return complexType; }; index.putAll(doBuildIndex(field.type(), getPathForField(field, path), index, complexTypeSupplier)); @@ -192,7 +200,7 @@ private void indexEnumType(FieldReflector field, Map index, Pa value = ValueDeserializationHandler.getInstance().handle(field.rawField(), (String) value) .orElse(value); } - FieldAccessor.of(field, parent.get()).set(value); + FieldAccessor.of(field, parent.get(), configuration).set(value); })); } @@ -203,7 +211,7 @@ private void indexSimpleType(FieldReflector field, Map index, value = ValueDeserializationHandler.getInstance().handle(field.rawField(), (String) value) .orElse(value); } - FieldAccessor.of(field, parent.get()).set(value); + FieldAccessor.of(field, parent.get(), configuration).set(value); })); } } @@ -212,12 +220,12 @@ private void indexSetType(FieldReflector field, Map index, Pat Collection set = new HashSet<>(); Path path = getPathForField(field, parentPath); var pathWriter = PathWriter.objectInitializer(() -> { - FieldAccessor.of(field, parent.get()).set(set); + FieldAccessor.of(field, parent.get(), configuration).set(set); return set; }); if (path.isRoot()) { pathWriter.setRootInitializer(() -> { - FieldAccessor.of(field, parent.get()).set(set); + FieldAccessor.of(field, parent.get(), configuration).set(set); return parent.get(); }); } @@ -247,12 +255,12 @@ private void indexListType(FieldReflector field, Map index, Pa List list = new ArrayList<>(); Path path = getPathForField(field, parentPath); var pathWriter = PathWriter.objectInitializer(() -> { - FieldAccessor.of(field, parent.get()).set(list); + FieldAccessor.of(field, parent.get(), configuration).set(list); return list; }); if (path.isRoot()) { pathWriter.setRootInitializer(() -> { - FieldAccessor.of(field, parent.get()).set(list); + FieldAccessor.of(field, parent.get(), configuration).set(list); return parent.get(); }); } @@ -271,10 +279,10 @@ private void indexListTypeArgument(Path path, FieldReflector field, Map index, Collection list, FieldReflector field, Class typeArgument) { + private void indexSimpleTypeListTypeArgument(Path path, Map index, Collection list, FieldReflector field, Class typeArgument) { Tag tag = field.getAnnotation(Tag.class); index.put(path.append(Path.parse(tag.items())), - PathWriter.valueInitializer((o) -> list.add(TypeMappers.forType(typeArgument).apply(o)))); + PathWriter.valueInitializer((o) -> list.add(TypeMappers.forType(typeArgument, configuration).apply(o)))); } private void indexComplexListTypeArgument(Map index, Collection list, Class typeArgument, FieldReflector field) { diff --git a/xjx-serdes/src/main/java/io/jonasg/xjx/serdes/deserialize/TypedValueMapSaxHandler.java b/xjx-serdes/src/main/java/io/jonasg/xjx/serdes/deserialize/TypedValueMapSaxHandler.java index 2acb286..4e2ff0f 100644 --- a/xjx-serdes/src/main/java/io/jonasg/xjx/serdes/deserialize/TypedValueMapSaxHandler.java +++ b/xjx-serdes/src/main/java/io/jonasg/xjx/serdes/deserialize/TypedValueMapSaxHandler.java @@ -6,21 +6,25 @@ import io.jonasg.xjx.sax.Attribute; import io.jonasg.xjx.sax.SaxHandler; +import io.jonasg.xjx.serdes.deserialize.config.XjxConfiguration; public class TypedValueMapSaxHandler implements SaxHandler { private final Class valueType; + private final XjxConfiguration configuration; + private final Map instance; private PathBasedSaxHandler objectPathBasedSaxHandler; private String activeKey; - public TypedValueMapSaxHandler(MapWithTypeInfo instance) { + public TypedValueMapSaxHandler(MapWithTypeInfo instance, XjxConfiguration configuration) { this.instance = instance.map(); this.valueType = instance.valueType(); - } + this.configuration = configuration; + } @Override public void startDocument() { @@ -31,7 +35,7 @@ public void startTag(String namespace, String name, List attributes) if (this.activeKey == null) { this.activeKey = name; objectPathBasedSaxHandler = new PathBasedSaxHandler<>(rootTag -> - new PathWriterIndexFactory().createIndexForType(valueType, this.activeKey), this.activeKey); + new PathWriterIndexFactory(configuration).createIndexForType(valueType, this.activeKey), this.activeKey, configuration); } else { objectPathBasedSaxHandler.startTag(namespace, name, attributes); } diff --git a/xjx-serdes/src/main/java/io/jonasg/xjx/serdes/deserialize/accessor/FieldAccessor.java b/xjx-serdes/src/main/java/io/jonasg/xjx/serdes/deserialize/accessor/FieldAccessor.java index 429c318..815d5bd 100644 --- a/xjx-serdes/src/main/java/io/jonasg/xjx/serdes/deserialize/accessor/FieldAccessor.java +++ b/xjx-serdes/src/main/java/io/jonasg/xjx/serdes/deserialize/accessor/FieldAccessor.java @@ -2,19 +2,20 @@ import io.jonasg.xjx.serdes.TypeMappers; import io.jonasg.xjx.serdes.deserialize.RecordWrapper; +import io.jonasg.xjx.serdes.deserialize.config.XjxConfiguration; import io.jonasg.xjx.serdes.reflector.FieldReflector; public interface FieldAccessor { - static FieldAccessor of(FieldReflector field, Object instance) { + static FieldAccessor of(FieldReflector field, Object instance, XjxConfiguration configuration) { if (instance instanceof RecordWrapper recordWrapper) { - return new RecordFieldAccessor(field, recordWrapper); + return new RecordFieldAccessor(field, recordWrapper, configuration); } else { var setterFieldAccessor = new SetterFieldAccessor(field, instance); if (setterFieldAccessor.hasSetterForField()) { return new SetterFieldAccessor(field, instance); } - var mapper = TypeMappers.forType(field.type()); + var mapper = TypeMappers.forType(field.type(), configuration); return new ReflectiveFieldAccessor(field, instance, mapper); } } diff --git a/xjx-serdes/src/main/java/io/jonasg/xjx/serdes/deserialize/accessor/RecordFieldAccessor.java b/xjx-serdes/src/main/java/io/jonasg/xjx/serdes/deserialize/accessor/RecordFieldAccessor.java index 29d9ac7..9176c2a 100644 --- a/xjx-serdes/src/main/java/io/jonasg/xjx/serdes/deserialize/accessor/RecordFieldAccessor.java +++ b/xjx-serdes/src/main/java/io/jonasg/xjx/serdes/deserialize/accessor/RecordFieldAccessor.java @@ -2,6 +2,7 @@ import io.jonasg.xjx.serdes.TypeMappers; import io.jonasg.xjx.serdes.deserialize.RecordWrapper; +import io.jonasg.xjx.serdes.deserialize.config.XjxConfiguration; import io.jonasg.xjx.serdes.reflector.FieldReflector; public class RecordFieldAccessor implements FieldAccessor { @@ -10,14 +11,17 @@ public class RecordFieldAccessor implements FieldAccessor { private final RecordWrapper recordWrapper; - public RecordFieldAccessor(FieldReflector field, RecordWrapper recordWrapper) { + private final XjxConfiguration configuration; + + public RecordFieldAccessor(FieldReflector field, RecordWrapper recordWrapper, XjxConfiguration configuration) { this.field = field; this.recordWrapper = recordWrapper; + this.configuration = configuration; } @Override public void set(Object value) { - Object mappedValue = TypeMappers.forType(field.type()).apply(value); + Object mappedValue = TypeMappers.forType(field.type(), configuration).apply(value); recordWrapper.set(field.rawField().getName(), mappedValue); } } diff --git a/xjx-serdes/src/main/java/io/jonasg/xjx/serdes/deserialize/config/ConfigurationBuilder.java b/xjx-serdes/src/main/java/io/jonasg/xjx/serdes/deserialize/config/ConfigurationBuilder.java new file mode 100644 index 0000000..bd6598c --- /dev/null +++ b/xjx-serdes/src/main/java/io/jonasg/xjx/serdes/deserialize/config/ConfigurationBuilder.java @@ -0,0 +1,24 @@ +package io.jonasg.xjx.serdes.deserialize.config; + +import io.jonasg.xjx.serdes.XjxSerdes; + +@SuppressWarnings("UnusedReturnValue") +public class ConfigurationBuilder { + + private final XjxConfiguration xjxConfiguration; + + public ConfigurationBuilder(XjxConfiguration xjxConfiguration) { + this.xjxConfiguration = xjxConfiguration; + } + + /** + * Configures the {@link XjxSerdes} to fail when an enum value cannot be mapped to an enum constant. + * When not set, defaults to false and will default to null when a value cannot be mapped to a name. + * @param failOnUnmappableEnumValue Whether to fail when an enum value cannot be mapped to an enum constant + * @return The ConfigurationBuilder + */ + public ConfigurationBuilder failOnUnknownEnumValue(boolean failOnUnmappableEnumValue) { + this.xjxConfiguration.failOnUnknownEnumValue = failOnUnmappableEnumValue; + return this; + } +} diff --git a/xjx-serdes/src/main/java/io/jonasg/xjx/serdes/deserialize/config/XjxConfiguration.java b/xjx-serdes/src/main/java/io/jonasg/xjx/serdes/deserialize/config/XjxConfiguration.java new file mode 100644 index 0000000..0e58c60 --- /dev/null +++ b/xjx-serdes/src/main/java/io/jonasg/xjx/serdes/deserialize/config/XjxConfiguration.java @@ -0,0 +1,14 @@ +package io.jonasg.xjx.serdes.deserialize.config; + +public class XjxConfiguration { + + /** + * Whether to fail when an enum value cannot be mapped to an enum constant + * Defaults to false and will default to null when a value cannot be mapped to a name. + */ + boolean failOnUnknownEnumValue = false; + + public boolean failOnUnknownEnumValue() { + return this.failOnUnknownEnumValue; + } +} diff --git a/xjx-serdes/src/test/java/io/jonasg/xjx/serdes/deserialize/EnumDeserializationTest.java b/xjx-serdes/src/test/java/io/jonasg/xjx/serdes/deserialize/EnumDeserializationTest.java index 4886239..aba4463 100644 --- a/xjx-serdes/src/test/java/io/jonasg/xjx/serdes/deserialize/EnumDeserializationTest.java +++ b/xjx-serdes/src/test/java/io/jonasg/xjx/serdes/deserialize/EnumDeserializationTest.java @@ -1,87 +1,162 @@ package io.jonasg.xjx.serdes.deserialize; -import org.assertj.core.api.Assertions; +import org.assertj.core.api.ThrowableAssert.ThrowingCallable; +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import io.jonasg.xjx.serdes.Tag; import io.jonasg.xjx.serdes.XjxSerdes; +import io.jonasg.xjx.serdes.deserialize.CustomValueDeserializationTest.EnumHolder; +import io.jonasg.xjx.serdes.deserialize.CustomValueDeserializationTest.Unit; +import static org.assertj.core.api.Assertions.*; public class EnumDeserializationTest { - @Test - void mapToFieldOfTypeEnum_whenCharacterDataMatchesEnumConstantName() { - // given - String data = """ - - - - - 78 - FAHRENHEIT - - - 62 - FAHRENHEIT - - - 10 - PERCENTAGE - - Partly Cloudy - - - """; - - // when - EnumHolder enumHolder = new XjxSerdes().read(data, EnumHolder.class); - - // then - Assertions.assertThat(enumHolder.dayHighUnit).isEqualTo(Unit.FAHRENHEIT); - Assertions.assertThat(enumHolder.precipitationUnit).isEqualTo(Unit.PERCENTAGE); - } - - @Test - void defaultToNullWhenValueCannotBeMappedToName() { - // given - String data = """ - - - - - 78 - fahrenheit - - - 62 - fahrenheit - - - 10 - percentage - - Partly Cloudy - - - """; - - // when - EnumHolder enumHolder = new XjxSerdes().read(data, EnumHolder.class); - - // then - Assertions.assertThat(enumHolder.dayHighUnit).isNull(); - Assertions.assertThat(enumHolder.precipitationUnit).isNull(); - } - - static class EnumHolder { - @Tag(path = "/WeatherData/Day/High/Unit") - Unit dayHighUnit; - - @Tag(path = "/WeatherData/Day/Precipitation/Unit") - Unit precipitationUnit; - } - - enum Unit { - FAHRENHEIT, PERCENTAGE - } + @Test + void mapToFieldOfTypeEnum_whenCharacterDataMatchesEnumConstantName() { + // given + String data = """ + + + + + 78 + FAHRENHEIT + + + 62 + FAHRENHEIT + + + 10 + PERCENTAGE + + Partly Cloudy + + + """; + + // when + EnumHolder enumHolder = new XjxSerdes().read(data, EnumHolder.class); + + // then + assertThat(enumHolder.dayHighUnit).isEqualTo(Unit.FAHRENHEIT); + assertThat(enumHolder.precipitationUnit).isEqualTo(Unit.PERCENTAGE); + } + + @Test + void defaultToNullWhenValueCannotBeMappedToName() { + // given + String data = """ + + + + + 78 + fahrenheit + + + 62 + fahrenheit + + + 10 + percentage + + Partly Cloudy + + + """; + + // when + EnumHolder enumHolder = new XjxSerdes().read(data, EnumHolder.class); + + // then + assertThat(enumHolder.dayHighUnit).isNull(); + assertThat(enumHolder.precipitationUnit).isNull(); + } + + @Nested + class ConfigurationTest { + + @Test + void defaultToNullWhenValueCannotBeMappedToName() { + // given + String data = """ + + + + + 78 + fahrenheit + + + 62 + fahrenheit + + + 10 + percentage + + Partly Cloudy + + + """; + + // when + EnumHolder enumHolder = new XjxSerdes(c -> c.failOnUnknownEnumValue(false)) + .read(data, EnumHolder.class); + + // then + assertThat(enumHolder.dayHighUnit).isNull(); + assertThat(enumHolder.precipitationUnit).isNull(); + } + + @Test + void failOnUnmappableEnumValue() { + // given + String data = """ + + + + + 78 + unmappable + + + 62 + unmappable + + + 10 + fahrenheit/Unit> + + Partly Cloudy + + + """; + + // when + ThrowingCallable throwingCallable = () -> new XjxSerdes(c -> c.failOnUnknownEnumValue(true)) + .read(data, EnumHolder.class); + + // then + assertThatThrownBy(throwingCallable) + .isInstanceOf(XjxDeserializationException.class) + .hasMessageContaining("Cannot map value 'unmappable' to enum Unit"); + } + } + + static class EnumHolder { + @Tag(path = "/WeatherData/Day/High/Unit") + Unit dayHighUnit; + + @Tag(path = "/WeatherData/Day/Precipitation/Unit") + Unit precipitationUnit; + } + + enum Unit { + FAHRENHEIT, PERCENTAGE + } }