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