diff --git a/src/main/java/ch/jalu/configme/beanmapper/propertydescription/BeanDescriptionFactoryImpl.java b/src/main/java/ch/jalu/configme/beanmapper/propertydescription/BeanDescriptionFactoryImpl.java index 975182ab..c44a20c7 100644 --- a/src/main/java/ch/jalu/configme/beanmapper/propertydescription/BeanDescriptionFactoryImpl.java +++ b/src/main/java/ch/jalu/configme/beanmapper/propertydescription/BeanDescriptionFactoryImpl.java @@ -86,6 +86,13 @@ public class BeanDescriptionFactoryImpl implements BeanDescriptionFactory { comments); } + /** + * Returns the comments that are defined on the property. Comments are found by looking for an @{@link Comment} + * annotation on a field with the same name as the property. + * + * @param descriptor the property descriptor + * @return comments for the property (never null) + */ protected @NotNull List getComments(@NotNull PropertyDescriptor descriptor) { try { Field field = descriptor.getWriteMethod().getDeclaringClass().getDeclaredField(descriptor.getName()); diff --git a/src/main/java/ch/jalu/configme/beanmapper/propertydescription/BeanPropertyDescription.java b/src/main/java/ch/jalu/configme/beanmapper/propertydescription/BeanPropertyDescription.java index 233f71d6..ef876ef6 100644 --- a/src/main/java/ch/jalu/configme/beanmapper/propertydescription/BeanPropertyDescription.java +++ b/src/main/java/ch/jalu/configme/beanmapper/propertydescription/BeanPropertyDescription.java @@ -42,6 +42,9 @@ public interface BeanPropertyDescription { */ @Nullable Object getValue(@NotNull Object bean); + /** + * @return the comments to add when this property is exported + */ @NotNull List getComments(); } diff --git a/src/main/java/ch/jalu/configme/resource/yaml/SnakeYamlNodeBuilder.java b/src/main/java/ch/jalu/configme/resource/yaml/SnakeYamlNodeBuilder.java index bf2d1fd8..fed4de9b 100644 --- a/src/main/java/ch/jalu/configme/resource/yaml/SnakeYamlNodeBuilder.java +++ b/src/main/java/ch/jalu/configme/resource/yaml/SnakeYamlNodeBuilder.java @@ -44,6 +44,9 @@ public interface SnakeYamlNodeBuilder { * but we do not want the comments to appear between the key and the value in the YAML. Therefore, this method is * called before producing YAML as to move the comments from the value to the key node. * + * @implNote Only considers {@link Node#getBlockComments() block comments} on the nodes because it's the only type + * of comment that this builder sets. + * * @param valueNode the value node to remove the comments from * @param keyNode the key node to set the comments to */ diff --git a/src/main/java/ch/jalu/configme/resource/yaml/SnakeYamlNodeBuilderImpl.java b/src/main/java/ch/jalu/configme/resource/yaml/SnakeYamlNodeBuilderImpl.java index ffecfebe..3c72fe25 100644 --- a/src/main/java/ch/jalu/configme/resource/yaml/SnakeYamlNodeBuilderImpl.java +++ b/src/main/java/ch/jalu/configme/resource/yaml/SnakeYamlNodeBuilderImpl.java @@ -13,6 +13,7 @@ import org.yaml.snakeyaml.nodes.SequenceNode; import org.yaml.snakeyaml.nodes.Tag; +import java.math.BigDecimal; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -55,7 +56,7 @@ public class SnakeYamlNodeBuilderImpl implements SnakeYamlNodeBuilder { node = createSequenceNode(stream, path, configurationData); } else { throw new IllegalArgumentException("Unsupported value of type: " - + (value == null ? null : value.getClass())); + + (value == null ? null : value.getClass().getName())); } List commentLines = collectComments(obj, path, configurationData, numberOfNewLines); @@ -78,7 +79,7 @@ public class SnakeYamlNodeBuilderImpl implements SnakeYamlNodeBuilder { @Override public void transferComments(@NotNull Node valueNode, @NotNull Node keyNode) { - if (!valueNode.getBlockComments().isEmpty()) { + if (valueNode.getBlockComments() != null && !valueNode.getBlockComments().isEmpty()) { keyNode.setBlockComments(valueNode.getBlockComments()); valueNode.setBlockComments(Collections.emptyList()); } @@ -89,7 +90,9 @@ public void transferComments(@NotNull Node valueNode, @NotNull Node keyNode) { } protected @NotNull Node createNumberNode(@NotNull Number value) { - Tag tag = (value instanceof Double || value instanceof Float) ? Tag.FLOAT : Tag.INT; + Tag tag = (value instanceof Double || value instanceof Float || value instanceof BigDecimal) + ? Tag.FLOAT + : Tag.INT; return new ScalarNode(tag, value.toString(), null, null, DumperOptions.ScalarStyle.PLAIN); } @@ -128,6 +131,16 @@ public void transferComments(@NotNull Node valueNode, @NotNull Node keyNode) { return new MappingNode(Tag.MAP, nodeEntries, DumperOptions.FlowStyle.BLOCK); } + /** + * Creates comments based on all possible sources (number of empty lines, configuration data, + * {@link ValueWithComments}) and returns them as SnakeYAML comment lines. + * + * @param value the export value + * @param path the path the value is located at + * @param configurationData the configuration data instance + * @param numberOfNewLines number of new lines to add to the beginning of the comments + * @return comment lines representing all defined comments + */ protected @NotNull List collectComments(@NotNull Object value, @NotNull String path, @NotNull ConfigurationData configurationData, int numberOfNewLines) { diff --git a/src/main/java/ch/jalu/configme/resource/yaml/SnakeYamlNodeContainerImpl.java b/src/main/java/ch/jalu/configme/resource/yaml/SnakeYamlNodeContainerImpl.java index cb8c18d8..89dcda6a 100644 --- a/src/main/java/ch/jalu/configme/resource/yaml/SnakeYamlNodeContainerImpl.java +++ b/src/main/java/ch/jalu/configme/resource/yaml/SnakeYamlNodeContainerImpl.java @@ -32,7 +32,7 @@ public SnakeYamlNodeContainerImpl(@NotNull List comments) { @NotNull Supplier> commentsSupplier) { Object value = values.computeIfAbsent(name, k -> new SnakeYamlNodeContainerImpl(commentsSupplier.get())); if (!(value instanceof SnakeYamlNodeContainer)) { - throw new IllegalStateException("Unexpectedly found " + value.getClass() + " in '" + name + "'"); + throw new IllegalStateException("Unexpectedly found " + value.getClass().getName() + " in '" + name + "'"); } return (SnakeYamlNodeContainer) value; } diff --git a/src/test/java/ch/jalu/configme/TestUtils.java b/src/test/java/ch/jalu/configme/TestUtils.java index 451d0977..622a4da9 100644 --- a/src/test/java/ch/jalu/configme/TestUtils.java +++ b/src/test/java/ch/jalu/configme/TestUtils.java @@ -119,16 +119,6 @@ public static Matcher> containsAll(Iterable element // Exception verification // ------------- - /** - * Verifies that the provided executable throws an exception of the given type. - * - * @param executable the executable to check - * @param exceptionType the expected type of the exception - */ - public static void verifyException(Executable executable, Class exceptionType) { - verifyException(executable, exceptionType, ""); - } - /** * Verifies that the provided executable throws an exception of the given type whose message contains * the provided message excerpt. diff --git a/src/test/java/ch/jalu/configme/beanmapper/MapperExportValueTest.java b/src/test/java/ch/jalu/configme/beanmapper/MapperExportValueTest.java index 1a22fec4..35997a07 100644 --- a/src/test/java/ch/jalu/configme/beanmapper/MapperExportValueTest.java +++ b/src/test/java/ch/jalu/configme/beanmapper/MapperExportValueTest.java @@ -3,7 +3,6 @@ import ch.jalu.configme.beanmapper.command.Command; import ch.jalu.configme.beanmapper.command.CommandConfig; import ch.jalu.configme.beanmapper.command.ExecutionDetails; -import ch.jalu.configme.beanmapper.command.Executor; import ch.jalu.configme.properties.convertresult.ValueWithComments; import org.junit.jupiter.api.Test; @@ -11,7 +10,6 @@ import java.util.Collection; import java.util.HashMap; import java.util.LinkedHashMap; -import java.util.LinkedHashSet; import java.util.Map; import static ch.jalu.configme.beanmapper.command.Executor.CONSOLE; @@ -30,11 +28,11 @@ class MapperExportValueTest { @Test void shouldCreatePropertyEntriesForCommandConfig() { // given - ExecutionDetails kickExecution = createExecution(CONSOLE, 0.4, true, "player.kick", "is.admin"); + ExecutionDetails kickExecution = new ExecutionDetails(CONSOLE, 0.4, true, "player.kick", "is.admin"); Command kickCommand = createCommand("kick", kickExecution, "name"); - ExecutionDetails msgExecution = createExecution(USER, 1.0, false, "player.msg"); + ExecutionDetails msgExecution = new ExecutionDetails(USER, 1.0, false, "player.msg"); Command msgCommand = createCommand("msg", msgExecution, "name", "message"); - ExecutionDetails vanishExecution = createExecution(USER, 0.1, true, "player.vanish"); + ExecutionDetails vanishExecution = new ExecutionDetails(USER, 0.1, true, "player.vanish"); Command vanishCommand = createCommand("vanish", vanishExecution); CommandConfig config = new CommandConfig(); @@ -131,14 +129,4 @@ private static Command createCommand(String name, ExecutionDetails executionDeta command.setArguments(Arrays.asList(arguments)); return command; } - - private static ExecutionDetails createExecution(Executor executor, double importance, boolean isOptional, - String... privileges) { - ExecutionDetails execution = new ExecutionDetails(); - execution.setImportance(importance); - execution.setOptional(isOptional); - execution.setExecutor(executor); - execution.setPrivileges(new LinkedHashSet<>(Arrays.asList(privileges))); - return execution; - } } diff --git a/src/test/java/ch/jalu/configme/beanmapper/command/ExecutionDetails.java b/src/test/java/ch/jalu/configme/beanmapper/command/ExecutionDetails.java index d3ee4aaa..0ef25d4a 100644 --- a/src/test/java/ch/jalu/configme/beanmapper/command/ExecutionDetails.java +++ b/src/test/java/ch/jalu/configme/beanmapper/command/ExecutionDetails.java @@ -2,6 +2,8 @@ import ch.jalu.configme.Comment; +import java.util.Arrays; +import java.util.LinkedHashSet; import java.util.Set; /** @@ -15,6 +17,16 @@ public class ExecutionDetails { private Double importance; private Set privileges; + public ExecutionDetails() { + } + + public ExecutionDetails(Executor executor, double importance, boolean isOptional, String... privileges) { + this.executor = executor; + this.optional = isOptional; + this.importance = importance; + this.privileges = new LinkedHashSet<>(Arrays.asList(privileges)); + } + public Executor getExecutor() { return executor; } diff --git a/src/test/java/ch/jalu/configme/beanmapper/propertydescription/BeanDescriptionFactoryImplTest.java b/src/test/java/ch/jalu/configme/beanmapper/propertydescription/BeanDescriptionFactoryImplTest.java index 81d79acc..798a1b2e 100644 --- a/src/test/java/ch/jalu/configme/beanmapper/propertydescription/BeanDescriptionFactoryImplTest.java +++ b/src/test/java/ch/jalu/configme/beanmapper/propertydescription/BeanDescriptionFactoryImplTest.java @@ -1,6 +1,7 @@ package ch.jalu.configme.beanmapper.propertydescription; +import ch.jalu.configme.Comment; import ch.jalu.configme.beanmapper.ConfigMeMapperException; import ch.jalu.configme.samples.beanannotations.AnnotatedEntry; import ch.jalu.configme.samples.beanannotations.BeanWithEmptyName; @@ -40,8 +41,14 @@ void shouldReturnWritableProperties() { // then assertThat(descriptions, hasSize(2)); - assertThat(getDescription("size", descriptions).getTypeInformation(), equalTo(new TypeInformation(int.class))); - assertThat(getDescription("name", descriptions).getTypeInformation(), equalTo(new TypeInformation(String.class))); + + BeanPropertyDescription sizeProperty = getDescription("size", descriptions); + assertThat(sizeProperty.getTypeInformation(), equalTo(new TypeInformation(int.class))); + assertThat(sizeProperty.getComments(), contains("Size of this entry (cm)")); + + BeanPropertyDescription nameProperty = getDescription("name", descriptions); + assertThat(nameProperty.getTypeInformation(), equalTo(new TypeInformation(String.class))); + assertThat(nameProperty.getComments(), empty()); } @Test @@ -162,6 +169,7 @@ private static BeanPropertyDescription getDescription(String name, private static final class SampleBean { private String name; + @Comment("Size of this entry (cm)") private int size; private long longField; // static "getter" method private UUID uuid = UUID.randomUUID(); // no setter diff --git a/src/test/java/ch/jalu/configme/beanmapper/propertydescription/BeanPropertyDescriptionImplTest.java b/src/test/java/ch/jalu/configme/beanmapper/propertydescription/BeanPropertyDescriptionImplTest.java index dd114a6f..bf13e061 100644 --- a/src/test/java/ch/jalu/configme/beanmapper/propertydescription/BeanPropertyDescriptionImplTest.java +++ b/src/test/java/ch/jalu/configme/beanmapper/propertydescription/BeanPropertyDescriptionImplTest.java @@ -2,11 +2,14 @@ import ch.jalu.configme.beanmapper.ConfigMeMapperException; import ch.jalu.configme.samples.beanannotations.AnnotatedEntry; +import ch.jalu.configme.utils.TypeInformation; import org.junit.jupiter.api.Test; +import java.lang.reflect.Method; import java.util.Collection; import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.empty; import static org.hamcrest.Matchers.equalTo; import static org.junit.jupiter.api.Assertions.assertThrows; @@ -71,6 +74,22 @@ void shouldHaveAppropriateStringRepresentation() { + "'public boolean ch.jalu.configme.samples.beanannotations.AnnotatedEntry.getHasId()'")); } + @Test + void shouldCreateValuesWithLegacyConstructor() throws NoSuchMethodException { + // given + Method sizeGetter = SampleBean.class.getDeclaredMethod("getSize"); + Method sizeSetter = SampleBean.class.getDeclaredMethod("setSize", int.class); + + // when + BeanPropertyDescriptionImpl property = + new BeanPropertyDescriptionImpl("name", new TypeInformation(String.class), sizeGetter, sizeSetter); + + // then + assertThat(property.getName(), equalTo("name")); + assertThat(property.getTypeInformation(), equalTo(new TypeInformation(String.class))); + assertThat(property.getComments(), empty()); + } + private static BeanPropertyDescription getDescriptor(String name, Class clazz) { return new BeanDescriptionFactoryImpl().collectAllProperties(clazz) .stream() diff --git a/src/test/java/ch/jalu/configme/configurationdata/ConfigurationDataImplTest.java b/src/test/java/ch/jalu/configme/configurationdata/ConfigurationDataImplTest.java index 34445e1c..b85cb49c 100644 --- a/src/test/java/ch/jalu/configme/configurationdata/ConfigurationDataImplTest.java +++ b/src/test/java/ch/jalu/configme/configurationdata/ConfigurationDataImplTest.java @@ -20,6 +20,7 @@ import static org.hamcrest.Matchers.contains; import static org.hamcrest.Matchers.empty; import static org.hamcrest.Matchers.equalTo; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.ArgumentMatchers.anyDouble; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.BDDMockito.given; @@ -53,7 +54,8 @@ void shouldHaveImmutablePropertyList() { ConfigurationData configData = new ConfigurationDataImpl(properties, Collections.emptyMap()); // when / then - verifyException(() -> configData.getProperties().remove(0), UnsupportedOperationException.class); + assertThrows(UnsupportedOperationException.class, + () -> configData.getProperties().remove(0)); } @Test diff --git a/src/test/java/ch/jalu/configme/properties/convertresult/ValueWithCommentsTest.java b/src/test/java/ch/jalu/configme/properties/convertresult/ValueWithCommentsTest.java new file mode 100644 index 00000000..bb86616e --- /dev/null +++ b/src/test/java/ch/jalu/configme/properties/convertresult/ValueWithCommentsTest.java @@ -0,0 +1,28 @@ +package ch.jalu.configme.properties.convertresult; + +import org.junit.jupiter.api.Test; + +import java.util.Arrays; +import java.util.concurrent.TimeUnit; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.nullValue; + +/** + * Test for {@link ValueWithComments}. + */ +class ValueWithCommentsTest { + + @Test + void shouldUnwrapValue() { + // given + Object object1 = new ValueWithComments("test", Arrays.asList("Explanatory", "comments")); + Object object2 = TimeUnit.SECONDS; + + // when / then + assertThat(ValueWithComments.unwrapValue(object1), equalTo("test")); + assertThat(ValueWithComments.unwrapValue(object2), equalTo(TimeUnit.SECONDS)); + assertThat(ValueWithComments.unwrapValue(null), nullValue()); + } +} diff --git a/src/test/java/ch/jalu/configme/resource/YamlFileResourceCommentsExportTest.java b/src/test/java/ch/jalu/configme/resource/YamlFileResourceCommentsExportTest.java new file mode 100644 index 00000000..3122247b --- /dev/null +++ b/src/test/java/ch/jalu/configme/resource/YamlFileResourceCommentsExportTest.java @@ -0,0 +1,142 @@ +package ch.jalu.configme.resource; + +import ch.jalu.configme.Comment; +import ch.jalu.configme.SettingsHolder; +import ch.jalu.configme.TestUtils; +import ch.jalu.configme.beanmapper.command.Command; +import ch.jalu.configme.beanmapper.command.ExecutionDetails; +import ch.jalu.configme.configurationdata.CommentsConfiguration; +import ch.jalu.configme.configurationdata.ConfigurationData; +import ch.jalu.configme.configurationdata.ConfigurationDataBuilder; +import ch.jalu.configme.properties.BeanProperty; +import ch.jalu.configme.properties.Property; +import ch.jalu.configme.properties.convertresult.ValueWithComments; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Collections; + +import static ch.jalu.configme.beanmapper.command.Executor.CONSOLE; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.contains; + +/** + * Test cases for {@link YamlFileResource} and the handling of comments from different sources. + */ +class YamlFileResourceCommentsExportTest { + + @TempDir + public Path tempFolder; + + @Test + void shouldCombineConfiguredCommentWithExportValueComment() throws IOException { + // given + Path file = TestUtils.createTemporaryFile(tempFolder); + + Command command = new Command(); + command.setCommand("help"); + command.setExecution(new ExecutionDetails(CONSOLE, 0.8, false, "op")); + + YamlFileResource yamlResource = new YamlFileResource(file); + ConfigurationData configurationData = ConfigurationDataBuilder.createConfiguration(RootPropertyHolder.class); + configurationData.setValue(RootPropertyHolder.COMMAND, command); + + // when + yamlResource.exportProperties(configurationData); + + // then + assertThat(Files.readAllLines(file), contains( + "# Define the command here.", + "", + "# Fill out all values.", + "# By default, help is run", + "command: help", + "arguments: []", + "execution:", + " executor: CONSOLE", + " optional: false", + " # The higher the number, the more important", + " importance: 0.8", + " privileges:", + " - op" + )); + } + + @Test + void shouldCombineConfiguredCommentWithExportValueComment2() throws IOException { + // given + Path file = TestUtils.createTemporaryFile(tempFolder); + + Command command = new Command(); + command.setCommand("help"); + command.setExecution(new ExecutionDetails(CONSOLE, 0.8, false, "op")); + + YamlFileResource yamlResource = new YamlFileResource(file); + ConfigurationData configurationData = ConfigurationDataBuilder.createConfiguration(RootPropertyHolder2.class); + configurationData.setValue(RootPropertyHolder2.COMMAND2, command); + + // when + yamlResource.exportProperties(configurationData); + + // then + assertThat(Files.readAllLines(file), contains( + "# Command to run", + "", + "# Don't forget to save!", + "# This command is run on startup", + "command: help", + "arguments: []", + "execution:", + " executor: CONSOLE", + " optional: false", + " # The higher the number, the more important", + " importance: 0.8", + " privileges:", + " - op" + )); + } + + public static final class RootPropertyHolder implements SettingsHolder { + + public static final Property COMMAND = new BeanWithExportCommentProperty<>( + Command.class, "", new Command(), "By default, help is run"); + + @Override + public void registerComments(@NotNull CommentsConfiguration conf) { + conf.setComment("", "Define the command here.", "\n", "Fill out all values."); + } + } + + public static final class RootPropertyHolder2 implements SettingsHolder { + + @Comment({"Command to run", "\n", "Don't forget to save!"}) + public static final Property COMMAND2 = new BeanWithExportCommentProperty<>( + Command.class, "", new Command(), "This command is run on startup"); + + } + + public static final class BeanWithExportCommentProperty extends BeanProperty { + + private final String comment; + + public BeanWithExportCommentProperty(@NotNull Class beanType, @NotNull String path, @NotNull T defaultValue, + @NotNull String comment) { + super(beanType, path, defaultValue); + this.comment = comment; + } + + @Override + public @Nullable Object toExportValue(@NotNull T value) { + Object exportValue = super.toExportValue(value); + if (exportValue == null) { + return null; + } + return new ValueWithComments(exportValue, Collections.singletonList(comment)); + } + } +} diff --git a/src/test/java/ch/jalu/configme/resource/yaml/SnakeYamlNodeBuilderImplTest.java b/src/test/java/ch/jalu/configme/resource/yaml/SnakeYamlNodeBuilderImplTest.java new file mode 100644 index 00000000..540c0079 --- /dev/null +++ b/src/test/java/ch/jalu/configme/resource/yaml/SnakeYamlNodeBuilderImplTest.java @@ -0,0 +1,520 @@ +package ch.jalu.configme.resource.yaml; + +import ch.jalu.configme.TestUtils; +import ch.jalu.configme.configurationdata.ConfigurationData; +import ch.jalu.configme.properties.convertresult.ValueWithComments; +import org.hamcrest.Description; +import org.hamcrest.Matcher; +import org.hamcrest.TypeSafeMatcher; +import org.junit.jupiter.api.Test; +import org.yaml.snakeyaml.DumperOptions; +import org.yaml.snakeyaml.comments.CommentLine; +import org.yaml.snakeyaml.comments.CommentType; +import org.yaml.snakeyaml.nodes.MappingNode; +import org.yaml.snakeyaml.nodes.Node; +import org.yaml.snakeyaml.nodes.NodeTuple; +import org.yaml.snakeyaml.nodes.ScalarNode; +import org.yaml.snakeyaml.nodes.SequenceNode; +import org.yaml.snakeyaml.nodes.Tag; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.TimeUnit; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.empty; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.instanceOf; +import static org.hamcrest.Matchers.nullValue; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.only; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; + +/** + * Test for {@link SnakeYamlNodeBuilderImpl}. + */ +class SnakeYamlNodeBuilderImplTest { + + private final SnakeYamlNodeBuilderImpl nodeBuilder = new SnakeYamlNodeBuilderImpl(); + + @Test + void shouldCreateNodeForString() { + // given + String value = "Test"; + ConfigurationData configurationData = mock(ConfigurationData.class); + String path = "title.txt"; + given(configurationData.getCommentsForSection(path)).willReturn(Collections.singletonList("Title text")); + + // when + Node node = nodeBuilder.toYamlNode(value, path, configurationData, 2); + + // then + assertThat(node, instanceOf(ScalarNode.class)); + ScalarNode scalarNode = (ScalarNode) node; + assertThat(scalarNode.getTag(), equalTo(Tag.STR)); + assertThat(scalarNode.getValue(), equalTo(value)); + + assertThat(scalarNode.getInLineComments(), nullValue()); + assertThat(scalarNode.getEndComments(), nullValue()); + assertThat(scalarNode.getBlockComments(), hasSize(3)); + assertThat(scalarNode.getBlockComments().get(0), isBlankComment()); + assertThat(scalarNode.getBlockComments().get(1), isBlankComment()); + assertThat(scalarNode.getBlockComments().get(2), isBlockComment(" Title text")); + } + + @Test + void shouldCreateNodeForEnum() { + // given + TimeUnit value = TimeUnit.DAYS; + ConfigurationData configurationData = mock(ConfigurationData.class); + String path = "duration.unit"; + given(configurationData.getCommentsForSection(path)).willReturn(Arrays.asList("comment 1", "comment 2")); + + // when + Node node = nodeBuilder.toYamlNode(value, path, configurationData, 0); + + // then + assertThat(node, instanceOf(ScalarNode.class)); + ScalarNode scalarNode = (ScalarNode) node; + assertThat(scalarNode.getTag(), equalTo(Tag.STR)); + assertThat(scalarNode.getValue(), equalTo("DAYS")); + + assertThat(scalarNode.getInLineComments(), nullValue()); + assertThat(scalarNode.getEndComments(), nullValue()); + assertThat(scalarNode.getBlockComments(), hasSize(2)); + assertThat(scalarNode.getBlockComments().get(0), isBlockComment(" comment 1")); + assertThat(scalarNode.getBlockComments().get(1), isBlockComment(" comment 2")); + } + + @Test + void shouldCreateNodeForInt() { + // given + int value = 330; + ConfigurationData configurationData = mock(ConfigurationData.class); + String path = "title.size"; + given(configurationData.getCommentsForSection(path)).willReturn(Collections.emptyList()); + + // when + Node node = nodeBuilder.toYamlNode(value, path, configurationData, 0); + + // then + assertThat(node, instanceOf(ScalarNode.class)); + ScalarNode scalarNode = (ScalarNode) node; + assertThat(scalarNode.getTag(), equalTo(Tag.INT)); + assertThat(scalarNode.getValue(), equalTo(Integer.toString(value))); + + assertThat(scalarNode.getInLineComments(), nullValue()); + assertThat(scalarNode.getEndComments(), nullValue()); + assertThat(scalarNode.getBlockComments(), empty()); + } + + @Test + void shouldCreateNodeForDouble() { + // given + double value = 3.14159; + ConfigurationData configurationData = mock(ConfigurationData.class); + String path = "constants.pi"; + given(configurationData.getCommentsForSection(path)).willReturn(Collections.emptyList()); + + // when + Node node = nodeBuilder.toYamlNode(value, path, configurationData, 1); + + // then + assertThat(node, instanceOf(ScalarNode.class)); + ScalarNode scalarNode = (ScalarNode) node; + assertThat(scalarNode.getTag(), equalTo(Tag.FLOAT)); + assertThat(scalarNode.getValue(), equalTo(Double.toString(value))); + + assertThat(scalarNode.getInLineComments(), nullValue()); + assertThat(scalarNode.getEndComments(), nullValue()); + assertThat(scalarNode.getBlockComments(), hasSize(1)); + assertThat(scalarNode.getBlockComments().get(0), isBlankComment()); + } + + @Test + void shouldCreateNodeForBigDecimal() { + // given + BigDecimal value = new BigDecimal("3.141592653598"); + ConfigurationData configurationData = mock(ConfigurationData.class); + String path = "constants.piPrecise"; + given(configurationData.getCommentsForSection(path)).willReturn(Collections.singletonList("Pi up to 12 digits")); + + // when + Node node = nodeBuilder.toYamlNode(value, path, configurationData, 1); + + // then + assertThat(node, instanceOf(ScalarNode.class)); + ScalarNode scalarNode = (ScalarNode) node; + assertThat(scalarNode.getTag(), equalTo(Tag.FLOAT)); + assertThat(scalarNode.getValue(), equalTo("3.141592653598")); + + assertThat(scalarNode.getInLineComments(), nullValue()); + assertThat(scalarNode.getEndComments(), nullValue()); + assertThat(scalarNode.getBlockComments(), hasSize(2)); + assertThat(scalarNode.getBlockComments().get(0), isBlankComment()); + assertThat(scalarNode.getBlockComments().get(1), isBlockComment(" Pi up to 12 digits")); + } + + @Test + void shouldCreateNodeForBoolean() { + // given + Object value = true; + ConfigurationData configurationData = mock(ConfigurationData.class); + String path = "output.debug"; + given(configurationData.getCommentsForSection(path)).willReturn(Collections.emptyList()); + + // when + Node node = nodeBuilder.toYamlNode(value, path, configurationData, 0); + + // then + assertThat(node, instanceOf(ScalarNode.class)); + ScalarNode scalarNode = (ScalarNode) node; + assertThat(scalarNode.getTag(), equalTo(Tag.BOOL)); + assertThat(scalarNode.getValue(), equalTo("true")); + + assertThat(scalarNode.getInLineComments(), nullValue()); + assertThat(scalarNode.getEndComments(), nullValue()); + assertThat(scalarNode.getBlockComments(), empty()); + } + + @Test + void shouldCreateNodeForList() { + // given + Object value = Arrays.asList(3, 4.5); + ConfigurationData configurationData = mock(ConfigurationData.class); + String path = "calc.coefficients"; + given(configurationData.getCommentsForSection(path)).willReturn(Collections.singletonList("Coefficients")); + given(configurationData.getCommentsForSection(path + ".0")).willReturn(Collections.emptyList()); + given(configurationData.getCommentsForSection(path + ".1")).willReturn(Collections.singletonList("\n")); + + // when + Node node = nodeBuilder.toYamlNode(value, path, configurationData, 1); + + // then + verify(configurationData).getCommentsForSection(path); + verify(configurationData).getCommentsForSection(path + ".0"); + verify(configurationData).getCommentsForSection(path + ".1"); + verifyNoMoreInteractions(configurationData); + + assertThat(node, instanceOf(SequenceNode.class)); + SequenceNode sequenceNode = (SequenceNode) node; + assertThat(sequenceNode.getTag(), equalTo(Tag.SEQ)); + + assertThat(sequenceNode.getInLineComments(), nullValue()); + assertThat(sequenceNode.getEndComments(), nullValue()); + assertThat(sequenceNode.getBlockComments(), hasSize(2)); + assertThat(sequenceNode.getBlockComments().get(0), isBlankComment()); + assertThat(sequenceNode.getBlockComments().get(1), isBlockComment(" Coefficients")); + + List values = sequenceNode.getValue(); + assertThat(values, hasSize(2)); + + assertThat(values.get(0), instanceOf(ScalarNode.class)); + assertThat(values.get(0).getTag(), equalTo(Tag.INT)); + assertThat(((ScalarNode) values.get(0)).getValue(), equalTo("3")); + assertThat(values.get(0).getBlockComments(), empty()); + + assertThat(values.get(1), instanceOf(ScalarNode.class)); + assertThat(values.get(1).getTag(), equalTo(Tag.FLOAT)); + assertThat(((ScalarNode) values.get(1)).getValue(), equalTo("4.5")); + assertThat(values.get(1).getBlockComments(), hasSize(1)); + assertThat(values.get(1).getBlockComments().get(0), isBlankComment()); + } + + @Test + void shouldCreateNodeForArray() { + // given + Boolean[] value = {true, false, true}; + ConfigurationData configurationData = mock(ConfigurationData.class); + String path = "calc.flags"; + + // when + Node result = nodeBuilder.toYamlNode(value, path, configurationData, 0); + + // then + verify(configurationData).getCommentsForSection(path); + verify(configurationData).getCommentsForSection(path + ".0"); + verify(configurationData).getCommentsForSection(path + ".1"); + verify(configurationData).getCommentsForSection(path + ".2"); + verifyNoMoreInteractions(configurationData); + + assertThat(result, instanceOf(SequenceNode.class)); + SequenceNode sequenceNode = (SequenceNode) result; + assertThat(sequenceNode.getTag(), equalTo(Tag.SEQ)); + + assertThat(sequenceNode.getInLineComments(), nullValue()); + assertThat(sequenceNode.getEndComments(), nullValue()); + assertThat(sequenceNode.getBlockComments(), empty()); + + List nodes = sequenceNode.getValue(); + assertThat(nodes, hasSize(3)); + nodes.forEach(node -> { + assertThat(node.getBlockComments(), empty()); + }); + + assertThat(nodes.get(0), isScalarNode(Tag.BOOL, "true")); + assertThat(nodes.get(1), isScalarNode(Tag.BOOL, "false")); + assertThat(nodes.get(2), isScalarNode(Tag.BOOL, "true")); + } + + @Test + void shouldCreateNodeForMap() { + // given + Map factors = new LinkedHashMap<>(); + factors.put("S", 6); + factors.put("A", 3); + factors.put("C", 2); + ConfigurationData configurationData = mock(ConfigurationData.class); + String path = "calc.factors"; + + given(configurationData.getCommentsForSection(path)).willReturn(Collections.singletonList("\n")); + given(configurationData.getCommentsForSection(path + ".A")).willReturn(Collections.singletonList("Alpha comp.")); + + // when + Node result = nodeBuilder.toYamlNode(factors, path, configurationData, 0); + + // then + verify(configurationData).getCommentsForSection(path); + verify(configurationData).getCommentsForSection(path + ".S"); + verify(configurationData).getCommentsForSection(path + ".A"); + verify(configurationData).getCommentsForSection(path + ".C"); + verifyNoMoreInteractions(configurationData); + + assertThat(result, instanceOf(MappingNode.class)); + MappingNode mapNode = (MappingNode) result; + assertThat(mapNode.getTag(), equalTo(Tag.MAP)); + + assertThat(mapNode.getInLineComments(), nullValue()); + assertThat(mapNode.getEndComments(), nullValue()); + assertThat(mapNode.getBlockComments(), hasSize(1)); + assertThat(mapNode.getBlockComments().get(0), isBlankComment()); + + List nodeTuples = mapNode.getValue(); + assertThat(nodeTuples, hasSize(3)); + + assertThat(nodeTuples.get(0).getKeyNode(), isScalarNode(Tag.STR, "S")); + assertThat(nodeTuples.get(0).getValueNode(), isScalarNode(Tag.INT, "6")); + assertThat(nodeTuples.get(1).getKeyNode(), isScalarNode(Tag.STR, "A")); + assertThat(nodeTuples.get(1).getValueNode(), isScalarNode(Tag.INT, "3")); + assertThat(nodeTuples.get(2).getKeyNode(), isScalarNode(Tag.STR, "C")); + assertThat(nodeTuples.get(2).getValueNode(), isScalarNode(Tag.INT, "2")); + + assertThat(nodeTuples.get(0).getKeyNode().getBlockComments(), nullValue()); + assertThat(nodeTuples.get(0).getValueNode().getBlockComments(), empty()); + assertThat(nodeTuples.get(1).getKeyNode().getBlockComments(), hasSize(1)); + assertThat(nodeTuples.get(1).getKeyNode().getBlockComments().get(0), isBlockComment(" Alpha comp.")); + assertThat(nodeTuples.get(1).getValueNode().getBlockComments(), empty()); + assertThat(nodeTuples.get(2).getKeyNode().getBlockComments(), nullValue()); + assertThat(nodeTuples.get(2).getValueNode().getBlockComments(), empty()); + } + + @Test + void shouldHandleEmptyMap() { + // given + Map factors = new LinkedHashMap<>(); + ConfigurationData configurationData = mock(ConfigurationData.class); + String path = "calc.factors"; + + given(configurationData.getCommentsForSection(path)).willReturn(Collections.singletonList("Overridden factors")); + + // when + Node result = nodeBuilder.toYamlNode(factors, path, configurationData, 0); + + // then + verify(configurationData, only()).getCommentsForSection(path); + + assertThat(result, instanceOf(MappingNode.class)); + MappingNode mapNode = (MappingNode) result; + assertThat(mapNode.getTag(), equalTo(Tag.MAP)); + assertThat(mapNode.getValue(), empty()); + + assertThat(mapNode.getInLineComments(), nullValue()); + assertThat(mapNode.getEndComments(), nullValue()); + assertThat(mapNode.getBlockComments(), hasSize(1)); + assertThat(mapNode.getBlockComments().get(0), isBlockComment(" Overridden factors")); + } + + @Test + void shouldHandleEmptyArray() { + // given + Object value = new Object[0]; + ConfigurationData configurationData = mock(ConfigurationData.class); + String path = "calc.specialRules"; + + given(configurationData.getCommentsForSection(path)).willReturn(Collections.singletonList("Overridden factors")); + + // when + Node result = nodeBuilder.toYamlNode(new ValueWithComments(value, Arrays.asList("R1", "R2")), path, + configurationData, 1); + + // then + verify(configurationData, only()).getCommentsForSection(path); + + assertThat(result, instanceOf(SequenceNode.class)); + SequenceNode sequenceNode = (SequenceNode) result; + assertThat(sequenceNode.getTag(), equalTo(Tag.SEQ)); + assertThat(sequenceNode.getValue(), empty()); + + assertThat(sequenceNode.getInLineComments(), nullValue()); + assertThat(sequenceNode.getEndComments(), nullValue()); + assertThat(sequenceNode.getBlockComments(), hasSize(4)); + assertThat(sequenceNode.getBlockComments().get(0), isBlankComment()); + assertThat(sequenceNode.getBlockComments().get(1), isBlockComment(" Overridden factors")); + assertThat(sequenceNode.getBlockComments().get(2), isBlockComment(" R1")); + assertThat(sequenceNode.getBlockComments().get(3), isBlockComment(" R2")); + } + + @Test + void shouldThrowForUnknownValueTypes() { + // given + ConfigurationData configurationData = mock(ConfigurationData.class); + + // when + IllegalArgumentException ex1 = assertThrows(IllegalArgumentException.class, + () -> nodeBuilder.toYamlNode(Optional.of(3), "", configurationData, 3)); + IllegalArgumentException ex2 = assertThrows(IllegalArgumentException.class, + () -> nodeBuilder.toYamlNode(null, "", configurationData, 3)); + + // then + assertThat(ex1.getMessage(), equalTo("Unsupported value of type: java.util.Optional")); + if (!TestUtils.hasBytecodeCheckForNotNullAnnotation()) { + assertThat(ex2.getMessage(), equalTo("Unsupported value of type: null")); + } + } + + @Test + void shouldCreateCommentNodes() { + // given / when + CommentLine nodeForEmptyString = nodeBuilder.createCommentLine(""); + CommentLine nodeForNewLine = nodeBuilder.createCommentLine("\n"); + CommentLine nodeForText = nodeBuilder.createCommentLine("Text"); + + // then + assertThat(nodeForEmptyString, isBlockComment(" ")); + assertThat(nodeForNewLine, isBlankComment()); + assertThat(nodeForText, isBlockComment(" Text")); + } + + @Test + void shouldTransferCommentsFromValueToKey() { + // given + Node keyNode = new ScalarNode(Tag.STR, "key", null, null, DumperOptions.ScalarStyle.PLAIN); + Node valueNode = new ScalarNode(Tag.INT, "34", null, null, DumperOptions.ScalarStyle.PLAIN); + valueNode.setBlockComments(new ArrayList<>()); + CommentLine commentLine = new CommentLine(null, null, "Test", CommentType.BLOCK); + valueNode.getBlockComments().add(commentLine); + + // when + nodeBuilder.transferComments(valueNode, keyNode); + + // then + assertThat(keyNode.getBlockComments(), contains(commentLine)); + assertThat(valueNode.getBlockComments(), empty()); + } + + @Test + void shouldCombineComments() { + // given + ValueWithComments valueWithComments = new ValueWithComments("3", Arrays.asList("VWC1", "VWC2")); + String path = "some.path"; + ConfigurationData configurationData = mock(ConfigurationData.class); + given(configurationData.getCommentsForSection(path)).willReturn(Arrays.asList("CD1", "CD2")); + + // when + List comments = nodeBuilder.collectComments(valueWithComments, path, configurationData, 1); + + // then + assertThat(comments, hasSize(5)); + assertThat(comments.get(0), isBlankComment()); + assertThat(comments.get(1), isBlockComment(" CD1")); + assertThat(comments.get(2), isBlockComment(" CD2")); + assertThat(comments.get(3), isBlockComment(" VWC1")); + assertThat(comments.get(4), isBlockComment(" VWC2")); + } + + @Test + void shouldCombineCommentsWherePresent() { + // given + Object value = true; + String path = "some.path"; + ConfigurationData configurationData = mock(ConfigurationData.class); + given(configurationData.getCommentsForSection(path)).willReturn(Arrays.asList("CD1", "\n", "CD2")); + + // when + List comments = nodeBuilder.collectComments(value, path, configurationData, 1); + + // then + assertThat(comments, hasSize(4)); + assertThat(comments.get(0), isBlankComment()); + assertThat(comments.get(1), isBlockComment(" CD1")); + assertThat(comments.get(2), isBlankComment()); + assertThat(comments.get(3), isBlockComment(" CD2")); + } + + static Matcher isScalarNode(Tag expectedTag, String expectedValue) { + return new TypeSafeMatcher() { + @Override + protected boolean matchesSafely(Node node) { + return node instanceof ScalarNode + && node.getTag() == expectedTag + && expectedValue.equals(((ScalarNode) node).getValue()); + } + + @Override + public void describeTo(Description description) { + description.appendText("ScalarNode with Tag." + expectedTag + " and value '" + expectedValue + "'"); + } + + @Override + protected void describeMismatchSafely(Node item, Description mismatchDescription) { + if (item instanceof ScalarNode) { + mismatchDescription.appendText("ScalarNode with Tag." + item.getTag() + " and value '" + + ((ScalarNode) item).getValue() + "'"); + } else { + mismatchDescription.appendText("Node of type '" + item.getClass() + "'"); + } + } + }; + } + + static Matcher isBlockComment(String expectedComment) { + return new TypeSafeMatcher() { + @Override + protected boolean matchesSafely(CommentLine commentLine) { + return commentLine.getCommentType() == CommentType.BLOCK + && expectedComment.equals(commentLine.getValue()); + } + + @Override + public void describeTo(Description description) { + description.appendText("Comment line type=BLOCK with value=" + expectedComment); + } + }; + } + + static Matcher isBlankComment() { + return new TypeSafeMatcher() { + @Override + protected boolean matchesSafely(CommentLine commentLine) { + return commentLine.getCommentType() == CommentType.BLANK_LINE + && "".equals(commentLine.getValue()); + } + + @Override + public void describeTo(Description description) { + description.appendText("Comment line type=BLOCK with value=''"); + } + }; + } +} diff --git a/src/test/java/ch/jalu/configme/resource/yaml/SnakeYamlNodeContainerImplTest.java b/src/test/java/ch/jalu/configme/resource/yaml/SnakeYamlNodeContainerImplTest.java new file mode 100644 index 00000000..71fa55f9 --- /dev/null +++ b/src/test/java/ch/jalu/configme/resource/yaml/SnakeYamlNodeContainerImplTest.java @@ -0,0 +1,151 @@ +package ch.jalu.configme.resource.yaml; + +import org.junit.jupiter.api.Test; +import org.yaml.snakeyaml.DumperOptions; +import org.yaml.snakeyaml.comments.CommentLine; +import org.yaml.snakeyaml.comments.CommentType; +import org.yaml.snakeyaml.nodes.MappingNode; +import org.yaml.snakeyaml.nodes.Node; +import org.yaml.snakeyaml.nodes.NodeTuple; +import org.yaml.snakeyaml.nodes.ScalarNode; +import org.yaml.snakeyaml.nodes.Tag; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import static ch.jalu.configme.resource.yaml.SnakeYamlNodeBuilderImplTest.isBlankComment; +import static ch.jalu.configme.resource.yaml.SnakeYamlNodeBuilderImplTest.isBlockComment; +import static ch.jalu.configme.resource.yaml.SnakeYamlNodeBuilderImplTest.isScalarNode; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.empty; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.instanceOf; +import static org.hamcrest.Matchers.sameInstance; +import static org.junit.jupiter.api.Assertions.assertThrows; + +/** + * Test for {@link SnakeYamlNodeContainerImpl}. + */ +class SnakeYamlNodeContainerImplTest { + + @Test + void shouldStoreNodes() { + // given + SnakeYamlNodeContainerImpl rootContainer = new SnakeYamlNodeContainerImpl(Collections.emptyList()); + ScalarNode stringNode = new ScalarNode(Tag.STR, "test", null, null, DumperOptions.ScalarStyle.PLAIN); + ScalarNode boolNode = new ScalarNode(Tag.BOOL, "true", null, null, DumperOptions.ScalarStyle.PLAIN); + + // when + rootContainer.putNode("name", stringNode); + SnakeYamlNodeContainerImpl debugContainer = + (SnakeYamlNodeContainerImpl) rootContainer.getOrCreateChildContainer("debug", () -> Arrays.asList("dc1", "dc2")); + debugContainer.putNode("log", boolNode); + + // then + assertThat(rootContainer.getComments(), empty()); + assertThat(rootContainer.getValues().keySet(), contains("name", "debug")); + assertThat(rootContainer.getValues().get("name"), sameInstance(stringNode)); + assertThat(rootContainer.getValues().get("debug"), sameInstance(debugContainer)); + + assertThat(rootContainer.getOrCreateChildContainer("debug", () -> null), sameInstance(debugContainer)); + assertThat(debugContainer.getComments(), contains("dc1", "dc2")); + assertThat(debugContainer.getValues().keySet(), contains("log")); + assertThat(debugContainer.getValues().values(), contains(boolNode)); + } + + @Test + void shouldCreateMapNodeFromAllValuesAndMoveCommentsToKeyNodes() { + // given + SnakeYamlNodeContainerImpl rootContainer = new SnakeYamlNodeContainerImpl(Arrays.asList("root1", "\n", "root2")); + ScalarNode stringNode = new ScalarNode(Tag.STR, "test", null, null, DumperOptions.ScalarStyle.PLAIN); + stringNode.setBlockComments(Arrays.asList( + new CommentLine(null, null, "sc1", CommentType.BLOCK), + new CommentLine(null, null, "sc2", CommentType.BLOCK))); + + ScalarNode boolNode = new ScalarNode(Tag.BOOL, "true", null, null, DumperOptions.ScalarStyle.PLAIN); + boolNode.setBlockComments(Collections.singletonList( + new CommentLine(null, null, "bc1", CommentType.BLOCK))); + + rootContainer.putNode("name", stringNode); + SnakeYamlNodeContainerImpl debugContainer = + (SnakeYamlNodeContainerImpl) rootContainer.getOrCreateChildContainer("debug", () -> Arrays.asList("dc1", "dc2")); + debugContainer.putNode("log", boolNode); + + SnakeYamlNodeBuilderImpl nodeBuilder = new SnakeYamlNodeBuilderImpl(); + + // when + Node rootNode = rootContainer.convertToNode(nodeBuilder); + + // then + assertThat(rootNode, instanceOf(MappingNode.class)); + assertThat(rootNode.getBlockComments(), hasSize(3)); + assertThat(rootNode.getBlockComments().get(0), isBlockComment(" root1")); + assertThat(rootNode.getBlockComments().get(1), isBlankComment()); + assertThat(rootNode.getBlockComments().get(2), isBlockComment(" root2")); + + List nodeTuples = ((MappingNode) rootNode).getValue(); + assertThat(nodeTuples, hasSize(2)); + assertThat(nodeTuples.get(0).getKeyNode(), isScalarNode(Tag.STR, "name")); + assertThat(nodeTuples.get(0).getKeyNode().getBlockComments(), hasSize(2)); + assertThat(nodeTuples.get(0).getKeyNode().getBlockComments().get(0), isBlockComment("sc1")); + assertThat(nodeTuples.get(0).getKeyNode().getBlockComments().get(1), isBlockComment("sc2")); + assertThat(nodeTuples.get(0).getValueNode(), sameInstance(stringNode)); + assertThat(nodeTuples.get(0).getValueNode().getBlockComments(), empty()); + + assertThat(nodeTuples.get(1).getKeyNode(), isScalarNode(Tag.STR, "debug")); + assertThat(nodeTuples.get(1).getKeyNode().getBlockComments(), hasSize(2)); + assertThat(nodeTuples.get(1).getKeyNode().getBlockComments().get(0), isBlockComment(" dc1")); + assertThat(nodeTuples.get(1).getKeyNode().getBlockComments().get(1), isBlockComment(" dc2")); + assertThat(nodeTuples.get(1).getValueNode(), instanceOf(MappingNode.class)); + assertThat(nodeTuples.get(1).getValueNode().getBlockComments(), empty()); + List debugNodeTuples = ((MappingNode) nodeTuples.get(1).getValueNode()).getValue(); + assertThat(debugNodeTuples, hasSize(1)); + assertThat(debugNodeTuples.get(0).getKeyNode(), isScalarNode(Tag.STR, "log")); + assertThat(debugNodeTuples.get(0).getValueNode(), sameInstance(boolNode)); + } + + @Test + void shouldThrowForAlreadyExistingValue() { + // given + SnakeYamlNodeContainer container = new SnakeYamlNodeContainerImpl(Collections.emptyList()); + container.getOrCreateChildContainer("test", Collections::emptyList); + ScalarNode boolNode = new ScalarNode(Tag.BOOL, "true", null, null, DumperOptions.ScalarStyle.PLAIN); + + // when + IllegalStateException ex = assertThrows(IllegalStateException.class, () -> container.putNode("test", boolNode)); + + // then + assertThat(ex.getMessage(), equalTo("Container unexpectedly already contains entry for 'test'")); + } + + @Test + void shouldThrowIfPathDoesNotHaveContainer() { + // given + SnakeYamlNodeContainer container = new SnakeYamlNodeContainerImpl(Collections.emptyList()); + ScalarNode boolNode = new ScalarNode(Tag.BOOL, "true", null, null, DumperOptions.ScalarStyle.PLAIN); + container.putNode("toast", boolNode); + + // when + IllegalStateException ex = assertThrows(IllegalStateException.class, () -> container.getOrCreateChildContainer("toast", Collections::emptyList)); + + // then + assertThat(ex.getMessage(), equalTo("Unexpectedly found org.yaml.snakeyaml.nodes.ScalarNode in 'toast'")); + } + + @Test + void shouldReturnRootNode() { + // given + SnakeYamlNodeContainer container = new SnakeYamlNodeContainerImpl(Collections.emptyList()); + ScalarNode boolNode = new ScalarNode(Tag.BOOL, "true", null, null, DumperOptions.ScalarStyle.PLAIN); + container.putNode("", boolNode); + + // when + Node rootNode = container.getRootValueNode(); + + // then + assertThat(rootNode, sameInstance(boolNode)); + } +}