Repository: decorators-squad/eo-yaml (java)

Issue:
Offer "Merged" Implementations
We should offer the following, for "updating" functionality:

```java
YamlMapping original = ...;
YamlMapping changed = ...;
YamlMapping updated = new MergedYamlMapping(original, updated);
```
Same for Sequences and Streams.

Pull request:
Introducing Extensions: MergedYamlMapping and MergedYamlSequence
fixes #262 
* Implemented and tested ``MergedYamlMapping``
* Implemented and tested ``MergedYamlSequence``
* Pulled out extensions into the ``extensions`` package

Patch:
diff --git a/src/main/java/com/amihaiemil/eoyaml/BaseScalar.java b/src/main/java/com/amihaiemil/eoyaml/BaseScalar.java
index b8fff26..afc35f8 100644
--- a/src/main/java/com/amihaiemil/eoyaml/BaseScalar.java
+++ b/src/main/java/com/amihaiemil/eoyaml/BaseScalar.java
@@ -90,7 +90,17 @@ abstract class BaseScalar extends BaseYamlNode implements Scalar {
         } else if (other == null) {
             result = 1;
         } else if (other instanceof Scalar) {
-            result = this.value().compareTo(((Scalar) other).value());
+            final String value = this.value();
+            final String otherVal = ((Scalar) other).value();
+            if(value == null && otherVal == null) {
+                result = 0;
+            } else if(value != null && otherVal == null) {
+                result = 1;
+            } else if (value == null && otherVal != null) {
+                result = -1;
+            } else {
+                result = this.value().compareTo(otherVal);
+            }
         }
         return result;
     }
diff --git a/src/main/java/com/amihaiemil/eoyaml/BaseYamlMapping.java b/src/main/java/com/amihaiemil/eoyaml/BaseYamlMapping.java
index ff7fdfd..650c04c 100644
--- a/src/main/java/com/amihaiemil/eoyaml/BaseYamlMapping.java
+++ b/src/main/java/com/amihaiemil/eoyaml/BaseYamlMapping.java
@@ -45,10 +45,10 @@ import java.util.Set;
  * @version $Id$
  * @since 4.0.0
  */
-abstract class BaseYamlMapping extends BaseYamlNode implements YamlMapping {
-
+public abstract class BaseYamlMapping
+    extends BaseYamlNode implements YamlMapping {
     @Override
-    public int hashCode() {
+    public final int hashCode() {
         int hash = 0;
         for(final YamlNode key : this.keys()) {
             hash += key.hashCode();
@@ -66,7 +66,7 @@ abstract class BaseYamlMapping extends BaseYamlNode implements YamlMapping {
      * @return True or false.
      */
     @Override
-    public boolean equals(final Object other) {
+    public final boolean equals(final Object other) {
         final boolean result;
         if (other == null || !(other instanceof YamlMapping)) {
             result = false;
@@ -98,7 +98,7 @@ abstract class BaseYamlMapping extends BaseYamlNode implements YamlMapping {
      *   a value &gt; 0 if this &gt; other
      */
     @Override
-    public int compareTo(final YamlNode other) {
+    public final int compareTo(final YamlNode other) {
         int result = 0;
         if (other == null || !(other instanceof YamlMapping)) {
             result = 1;
@@ -143,9 +143,6 @@ abstract class BaseYamlMapping extends BaseYamlNode implements YamlMapping {
      *
      * Keep this method package-protected, it should NOT be visible to users.
      *
-     * @todo #227:30min Modify this method in order to properly indent Scalar
-     *  nodes. They are of multiple types (plain, folded, literal) and their
-     *  indentation differs. Don't forget to add unit tests.
      * @param indentation Indentation to start with. Usually, it's 0, since we
      *  don't want to have spaces at the beginning. But in the case of nested
      *  YamlNodes, this value may be greater than 0.
diff --git a/src/main/java/com/amihaiemil/eoyaml/BaseYamlSequence.java b/src/main/java/com/amihaiemil/eoyaml/BaseYamlSequence.java
index adf7b1f..25116ba 100644
--- a/src/main/java/com/amihaiemil/eoyaml/BaseYamlSequence.java
+++ b/src/main/java/com/amihaiemil/eoyaml/BaseYamlSequence.java
@@ -44,10 +44,11 @@ import java.util.Iterator;
  * @version $Id$
  * @since 4.0.0
  */
-abstract class BaseYamlSequence extends BaseYamlNode implements YamlSequence {
+public abstract class BaseYamlSequence
+    extends BaseYamlNode implements YamlSequence {
 
     @Override
-    public int hashCode() {
+    public final int hashCode() {
         int hash = 0;
         for(final YamlNode node : this.values()) {
             hash += node.hashCode();
@@ -62,7 +63,7 @@ abstract class BaseYamlSequence extends BaseYamlNode implements YamlSequence {
      * @return True or false
      */
     @Override
-    public boolean equals(final Object other) {
+    public final boolean equals(final Object other) {
         final boolean result;
         if (other == null || !(other instanceof YamlSequence)) {
             result = false;
@@ -95,7 +96,7 @@ abstract class BaseYamlSequence extends BaseYamlNode implements YamlSequence {
      *   a value &gt; 0 if this &gt; other
      */
     @Override
-    public int compareTo(final YamlNode other) {
+    public final int compareTo(final YamlNode other) {
         int result = 0;
         if (other == null || other instanceof Scalar) {
             result = 1;
diff --git a/src/main/java/com/amihaiemil/eoyaml/StrictYamlMapping.java b/src/main/java/com/amihaiemil/eoyaml/StrictYamlMapping.java
index c03c5de..100161e 100644
--- a/src/main/java/com/amihaiemil/eoyaml/StrictYamlMapping.java
+++ b/src/main/java/com/amihaiemil/eoyaml/StrictYamlMapping.java
@@ -37,10 +37,14 @@ import java.util.Set;
  * to a different type of node than the demanded one.<br><br>
  * It is based on the fail-fast and null-is-bad idea <br>
  * see here: http://www.yegor256.com/2014/05/13/why-null-is-bad.html
+ * @deprecated This class will be moved to the extensions package in one
+ *  of the future releases. There will be no changes to it other than a
+ *  more suitable package.
  * @author Mihai Andronache (amihaiemil@gmail.com)
  * @version $Id$
  * @since 1.0.0
  */
+@Deprecated
 public final class StrictYamlMapping extends BaseYamlMapping {
 
     /**
diff --git a/src/main/java/com/amihaiemil/eoyaml/StrictYamlSequence.java b/src/main/java/com/amihaiemil/eoyaml/StrictYamlSequence.java
index a7d6fc2..09eebcd 100644
--- a/src/main/java/com/amihaiemil/eoyaml/StrictYamlSequence.java
+++ b/src/main/java/com/amihaiemil/eoyaml/StrictYamlSequence.java
@@ -35,10 +35,14 @@ import java.util.Iterator;
  * if any of the methods of the decorated YamlSequence returns null
  * (if the given index points to a YamlNode that is not a YamlMapping,
  * for instance).
+ * @deprecated This class will be moved to the extensions package in one
+ *  of the future releases. There will be no changes to it other than a
+ *  more suitable package.
  * @author Salavat.Yalalov (s.yalalov@gmail.com)
  * @version $Id$
  * @since 1.0.0
  */
+@Deprecated
 public final class StrictYamlSequence extends BaseYamlSequence {
 
     /**
diff --git a/src/main/java/com/amihaiemil/eoyaml/extensions/MergedYamlMapping.java b/src/main/java/com/amihaiemil/eoyaml/extensions/MergedYamlMapping.java
new file mode 100644
index 0000000..ab05bab
--- /dev/null
+++ b/src/main/java/com/amihaiemil/eoyaml/extensions/MergedYamlMapping.java
@@ -0,0 +1,205 @@
+/**
+ * Copyright (c) 2016-2020, Mihai Emil Andronache
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ *  modification, are permitted provided that the following conditions are met:
+ * Redistributions of source code must retain the above copyright notice, this
+ *  list of conditions and the following disclaimer.
+ *  Redistributions in binary form must reproduce the above copyright notice,
+ *  this list of conditions and the following disclaimer in the documentation
+ *  and/or other materials provided with the distribution.
+ * Neither the name of the copyright holder nor the names of its
+ *  contributors may be used to endorse or promote products derived from
+ *  this software without specific prior written permission.
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+ * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ * SUCH DAMAGE.
+ */
+package com.amihaiemil.eoyaml.extensions;
+
+import com.amihaiemil.eoyaml.*;
+
+import java.util.Collection;
+import java.util.Set;
+import java.util.function.Supplier;
+
+/**
+ * Merge two YAML Mappings into a single one. Very useful in implementing
+ * Updating of an existing YamlMapping. Use it like this:
+ * <pre>
+ *     final YamlMapping original = ...;
+ *     final YamlMapping changed = ...;
+ *     final YamlMapping merged = new MergedYamlMapping(
+ *         original, changed, true|false
+ *     );
+ *     //or, via the Supplier constructor:
+ *     final YamlMapping merged = new MergedYamlMapping(
+ *         original,
+ *         () -> {
+ *             //create the changed version on-the-fly, right here.
+ *         },
+ *         true|false
+ *     );
+ * </pre>
+ * @author Mihai Andronache (amihaiemil@gmail.com)
+ * @version $Id$
+ * @since 4.1.0
+ */
+public final class MergedYamlMapping extends BaseYamlMapping {
+
+    /**
+     * The merged YamlMapping.
+     */
+    private YamlMapping merged;
+
+    /**
+     * Constructor. By default, conflicting keys will not be overriden.
+     * @param original YamlMapping in which the changes will be merged.
+     * @param changed YamlMapping containing the differences.
+     */
+    public MergedYamlMapping(
+        final YamlMapping original,
+        final YamlMapping changed
+    ) {
+        this(original, changed, false);
+    }
+
+    /**
+     * Constructor. By default, conflicting keys will not be overriden.
+     * @param original YamlMapping in which the changes will be merged.
+     * @param changed Supplier of YamlMapping containing the differences.
+     */
+    public MergedYamlMapping(
+        final YamlMapping original,
+        final Supplier<YamlMapping> changed
+    ) {
+        this(original, changed, false);
+    }
+
+    /**
+     * Constructor. You can choose whether conflicting keys will be
+     * overriden or not.
+     * @param original YamlMapping in which the changes will be merged.
+     * @param changed YamlMapping containing the differences.
+     * @param overrideConflicts Override key conflicts or not?
+     */
+    public MergedYamlMapping(
+        final YamlMapping original,
+        final Supplier<YamlMapping> changed,
+        final boolean overrideConflicts
+    ) {
+        this(original, changed.get(), overrideConflicts);
+    }
+
+    /**
+     * Constructor. You can choose whether conflicting keys will be
+     * overriden or not.
+     * @param original YamlMapping in which the changes will be merged.
+     * @param changed YamlMapping containing the differences.
+     * @param overrideConflicts Override key conflicts or not?
+     */
+    public MergedYamlMapping(
+        final YamlMapping original,
+        final YamlMapping changed,
+        final boolean overrideConflicts
+    ) {
+        if(original == null && changed == null) {
+            throw new IllegalArgumentException(
+                "Both mappings cannot be null!"
+            );
+        } else {
+            this.merged = merge(original, changed, overrideConflicts);
+        }
+    }
+
+    @Override
+    public Set<YamlNode> keys() {
+        return this.merged.keys();
+    }
+
+    @Override
+    public Collection<YamlNode> values() {
+        return this.merged.values();
+    }
+
+    @Override
+    public YamlNode value(final YamlNode key) {
+        return this.merged.value(key);
+    }
+
+    @Override
+    public YamlMapping yamlMapping(final YamlNode key) {
+        return this.merged.yamlMapping(key);
+    }
+
+    @Override
+    public YamlSequence yamlSequence(final YamlNode key) {
+        return this.merged.yamlSequence(key);
+    }
+
+    @Override
+    public String string(final YamlNode key) {
+        return this.merged.string(key);
+    }
+
+    @Override
+    public String foldedBlockScalar(final YamlNode key) {
+        return this.merged.foldedBlockScalar(key);
+    }
+
+    @Override
+    public Collection<String> literalBlockScalar(final YamlNode key) {
+        return this.merged.literalBlockScalar(key);
+    }
+
+    /**
+     * Merge the two mappings.
+     * @param original Original mapping.
+     * @param changed Changed mapping.
+     * @param overrideConflicts Should conflicting keys be overriden or not?
+     * @return Merged mapping.
+     */
+    private static YamlMapping merge(
+        final YamlMapping original,
+        final YamlMapping changed,
+        final boolean overrideConflicts
+    ) {
+        final YamlMapping merged;
+        if(original == null || original.keys().isEmpty()) {
+            merged = changed;
+        } else if (changed == null || changed.keys().isEmpty()) {
+            merged = original;
+        } else {
+            YamlMappingBuilder builder = Yaml.createYamlMappingBuilder();
+            final Set<YamlNode> changedKeys = changed.keys();
+            for (final YamlNode key : original.keys()) {
+                if(changedKeys.contains(key)) {
+                    if(overrideConflicts) {
+                        builder = builder.add(key, changed.value(key));
+                    } else {
+                        builder = builder.add(key, original.value(key));
+                    }
+                    changedKeys.remove(key);
+                } else {
+                    builder = builder.add(key, original.value(key));
+                }
+            }
+            for(final YamlNode key : changedKeys) {
+                builder = builder.add(key, changed.value(key));
+            }
+            merged = builder.build();
+        }
+        return merged;
+    }
+
+}
diff --git a/src/main/java/com/amihaiemil/eoyaml/extensions/MergedYamlSequence.java b/src/main/java/com/amihaiemil/eoyaml/extensions/MergedYamlSequence.java
new file mode 100644
index 0000000..b992f74
--- /dev/null
+++ b/src/main/java/com/amihaiemil/eoyaml/extensions/MergedYamlSequence.java
@@ -0,0 +1,225 @@
+/**
+ * Copyright (c) 2016-2020, Mihai Emil Andronache
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ *  modification, are permitted provided that the following conditions are met:
+ * Redistributions of source code must retain the above copyright notice, this
+ *  list of conditions and the following disclaimer.
+ *  Redistributions in binary form must reproduce the above copyright notice,
+ *  this list of conditions and the following disclaimer in the documentation
+ *  and/or other materials provided with the distribution.
+ * Neither the name of the copyright holder nor the names of its
+ *  contributors may be used to endorse or promote products derived from
+ *  this software without specific prior written permission.
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+ * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ * SUCH DAMAGE.
+ */
+package com.amihaiemil.eoyaml.extensions;
+
+import com.amihaiemil.eoyaml.*;
+
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.function.Supplier;
+
+/**
+ * Merge two YAML Sequences into a single one. Very useful in implementing
+ * Updating of an existing YamlSequence. Use it like this:
+ * <pre>
+ *     final YamlSequence original = ...;
+ *     final YamlSequence changed = ...;
+ *     final YamlSequence merged = new MergedYamlSequence(
+ *         original, changed, true|false
+ *     );
+ *     //or, via the Supplier constructor:
+ *     final YamlSequence merged = new MergedYamlSequence(
+ *         original,
+ *         () -> {
+ *             //create the changed version on-the-fly, right here.
+ *         },
+ *         true|false
+ *     );
+ * </pre>
+ * @author Mihai Andronache (amihaiemil@gmail.com)
+ * @version $Id$
+ * @since 4.1.0
+ */
+public final class MergedYamlSequence extends BaseYamlSequence {
+    /**
+     * The merged YamlSequence.
+     */
+    private YamlSequence merged;
+
+    /**
+     * Constructor. By default, it's simply going to add the elements
+     * of changed to the ones of original.
+     * @param original YamlSequence in which the changes will be merged.
+     * @param changed YamlSequence containing the differences.
+     */
+    public MergedYamlSequence(
+        final YamlSequence original,
+        final YamlSequence changed
+    ) {
+        this(original, changed, false);
+    }
+
+    /**
+     * Constructor. By default, it's simply going to add the elements
+     * of changed to the ones of original.
+     * @param original YamlSequence in which the changes will be merged.
+     * @param changed Supplier of YamlSequence containing the differences.
+     */
+    public MergedYamlSequence(
+        final YamlSequence original,
+        final Supplier<YamlSequence> changed
+    ) {
+        this(original, changed, false);
+    }
+
+    /**
+     * Constructor. You can choose whether to override existing indices
+     * or not.
+     * @param original YamlSequence in which the changes will be merged.
+     * @param changed YamlSequence containing the differences.
+     * @param overrideIndices Override existing indices or not?
+     */
+    public MergedYamlSequence(
+        final YamlSequence original,
+        final Supplier<YamlSequence> changed,
+        final boolean overrideIndices
+    ) {
+        this(original, changed.get(), overrideIndices);
+    }
+
+    /**
+     * Constructor. You can choose whether conflicting keys will be
+     * overriden or not.
+     * @param original YamlMapping in which the changes will be merged.
+     * @param changed YamlMapping containing the differences.
+     * @param overrideIndices Override existing indices or not?
+     */
+    public MergedYamlSequence(
+        final YamlSequence original,
+        final YamlSequence changed,
+        final boolean overrideIndices
+    ) {
+        if(original == null && changed == null) {
+            throw new IllegalArgumentException(
+                "Both sequences cannot be null!"
+            );
+        } else {
+            this.merged = merge(original, changed, overrideIndices);
+        }
+    }
+
+
+    @Override
+    public int size() {
+        return this.merged.size();
+    }
+
+    @Override
+    public Collection<YamlNode> values() {
+        return this.merged.values();
+    }
+
+    @Override
+    public YamlMapping yamlMapping(final int index) {
+        return this.merged.yamlMapping(index);
+    }
+
+    @Override
+    public YamlSequence yamlSequence(final int index) {
+        return this.merged.yamlSequence(index);
+    }
+
+    @Override
+    public String string(final int index) {
+        return this.merged.string(index);
+    }
+
+    @Override
+    public String foldedBlockScalar(final int index) {
+        return this.merged.foldedBlockScalar(index);
+    }
+
+    @Override
+    public Collection<String> literalBlockScalar(final int index) {
+        return this.merged.literalBlockScalar(index);
+    }
+
+    @Override
+    public Iterator<YamlNode> iterator() {
+        return this.merged.iterator();
+    }
+
+    /**
+     * Merge the two sequences.
+     * @checkstyle CyclomaticComplexity (100 lines)
+     * @checkstyle ExecutableStatementCount (100 lines)
+     * @return Merged YamlSequence.
+     * @param original Original sequence.
+     * @param changed Changed sequence.
+     * @param overrideIndices Should the existing indices be overriden
+     *  or not?
+     */
+    private static YamlSequence merge(
+        final YamlSequence original,
+        final YamlSequence changed,
+        final boolean overrideIndices
+    ) {
+        final YamlSequence merged;
+        if(original == null || original.size() == 0) {
+            merged = changed;
+        } else if (changed == null || changed.size() == 0) {
+            merged = original;
+        } else {
+            if(overrideIndices) {
+                if (changed.size() >= original.size()) {
+                    merged = changed;
+                } else {
+                    YamlSequenceBuilder builder = Yaml
+                        .createYamlSequenceBuilder();
+                    int continueFrom = -1;
+                    for(final YamlNode node : changed) {
+                        builder = builder.add(node);
+                        continueFrom++;
+                    }
+                    final Iterator<YamlNode> originalIt = original.iterator();
+                    int originalIdx = -1;
+                    while(originalIt.hasNext()) {
+                        if(originalIdx >= continueFrom) {
+                            builder = builder.add(originalIt.next());
+                        } else {
+                            originalIt.next();
+                        }
+                        originalIdx++;
+                    }
+                    merged = builder.build();
+                }
+            } else {
+                YamlSequenceBuilder builder = Yaml
+                    .createYamlSequenceBuilder();
+                for(final YamlNode node : original) {
+                    builder = builder.add(node);
+                }
+                for(final YamlNode node : changed) {
+                    builder = builder.add(node);
+                }
+                merged = builder.build();
+            }
+        }
+        return merged;
+    }
+}
diff --git a/src/main/java/com/amihaiemil/eoyaml/extensions/package-info.java b/src/main/java/com/amihaiemil/eoyaml/extensions/package-info.java
new file mode 100644
index 0000000..8f7fad4
--- /dev/null
+++ b/src/main/java/com/amihaiemil/eoyaml/extensions/package-info.java
@@ -0,0 +1,39 @@
+/**
+ * Copyright (c) 2016-2020, Mihai Emil Andronache
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ *  modification, are permitted provided that the following conditions are met:
+ * Redistributions of source code must retain the above copyright notice, this
+ *  list of conditions and the following disclaimer.
+ *  Redistributions in binary form must reproduce the above copyright notice,
+ *  this list of conditions and the following disclaimer in the documentation
+ *  and/or other materials provided with the distribution.
+ * Neither the name of the copyright holder nor the names of its
+ *  contributors may be used to endorse or promote products derived from
+ *  this software without specific prior written permission.
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+ * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ * SUCH DAMAGE.
+ */
+/**
+ * This package contains convenient extensions for both YamlMapping and
+ * YamlSequence that provide all sorts of different functionality.
+ *
+ * Classes within this package are supposed to work only with the public
+ * API of this library.
+ *
+ * In order to create your own extension, you need to extend
+ * {@link com.amihaiemil.eoyaml.BaseYamlMapping} or
+ * {@link com.amihaiemil.eoyaml.BaseYamlSequence} respectively.
+ */
+package com.amihaiemil.eoyaml.extensions;
\ No newline at end of file


Test patch:
diff --git a/src/test/java/com/amihaiemil/eoyaml/extensions/MergedYamlMappingTest.java b/src/test/java/com/amihaiemil/eoyaml/extensions/MergedYamlMappingTest.java
new file mode 100644
index 0000000..4b9e8af
--- /dev/null
+++ b/src/test/java/com/amihaiemil/eoyaml/extensions/MergedYamlMappingTest.java
@@ -0,0 +1,232 @@
+/**
+ * Copyright (c) 2016-2020, Mihai Emil Andronache
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ *  modification, are permitted provided that the following conditions are met:
+ * Redistributions of source code must retain the above copyright notice, this
+ *  list of conditions and the following disclaimer.
+ *  Redistributions in binary form must reproduce the above copyright notice,
+ *  this list of conditions and the following disclaimer in the documentation
+ *  and/or other materials provided with the distribution.
+ * Neither the name of the copyright holder nor the names of its
+ *  contributors may be used to endorse or promote products derived from
+ *  this software without specific prior written permission.
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+ * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ * SUCH DAMAGE.
+ */
+package com.amihaiemil.eoyaml.extensions;
+
+import com.amihaiemil.eoyaml.Yaml;
+import com.amihaiemil.eoyaml.YamlMapping;
+import org.hamcrest.MatcherAssert;
+import org.hamcrest.Matchers;
+import org.junit.Test;
+
+/**
+ * Unit tests for {@link MergedYamlMapping}.
+ * @author Mihai Andronache (amihaiemil.com)
+ * @version $Id$
+ * @since 4.1.0
+ */
+public final class MergedYamlMappingTest {
+
+    /**
+     * It should complain when both arguments are null.
+     */
+    @Test(expected = IllegalArgumentException.class)
+    public void complainsOnTwoNullMappings() {
+        final YamlMapping original = null;
+        final YamlMapping changed = null;
+        final YamlMapping boom = new MergedYamlMapping(original, changed);
+    }
+
+    /**
+     * It should represent the Original mapping if the changed one
+     * is null.
+     */
+    @Test
+    public void representsOriginalIfChangedIsNullOrEmpty() {
+        final YamlMapping original = Yaml
+            .createYamlMappingBuilder().add("key", "value").build();
+        final YamlMapping changed = null;
+        MatcherAssert.assertThat(
+            new MergedYamlMapping(original, changed),
+            Matchers.equalTo(original)
+        );
+        MatcherAssert.assertThat(
+            new MergedYamlMapping(
+                original,
+                Yaml.createYamlMappingBuilder().build()
+             ),
+            Matchers.equalTo(original)
+        );
+    }
+
+    /**
+     * It should represent the Changed mapping if the original one
+     * is null.
+     */
+    @Test
+    public void representsChangeIfOriginalIsNullOrEmpty() {
+        final YamlMapping original = null;
+        final YamlMapping changed = Yaml
+            .createYamlMappingBuilder().add("key", "value").build();
+        MatcherAssert.assertThat(
+            new MergedYamlMapping(original, changed),
+            Matchers.equalTo(changed)
+        );
+        MatcherAssert.assertThat(
+            new MergedYamlMapping(
+                Yaml.createYamlMappingBuilder().build(),
+                changed
+            ),
+            Matchers.equalTo(changed)
+        );
+    }
+
+    /**
+     * It should contain all the keys regardless of the override flag,
+     * since there are no conflicts.
+     */
+    @Test
+    public void mergesWithNoConflicts() {
+        final YamlMapping expected = Yaml.createYamlMappingBuilder()
+            .add("key1", "value1")
+            .add("key2", "value2")
+            .add("key3", "value3")
+            .add("key4", "value4")
+            .build();
+        final YamlMapping original = Yaml.createYamlMappingBuilder()
+            .add("key1", "value1")
+            .add("key2", "value2")
+            .build();
+        final YamlMapping changed = Yaml.createYamlMappingBuilder()
+            .add("key3", "value3")
+            .add("key4", "value4")
+            .build();
+        MatcherAssert.assertThat(
+            new MergedYamlMapping(original, changed, false),
+            Matchers.equalTo(expected)
+        );
+        MatcherAssert.assertThat(
+            new MergedYamlMapping(original, changed, true),
+            Matchers.equalTo(expected)
+        );
+    }
+
+    /**
+     * It should merge without overriding conflicting keys.
+     */
+    @Test
+    public void mergesWithConflictsNoOverride() {
+        final YamlMapping expected = Yaml.createYamlMappingBuilder()
+            .add("key1", "value1")
+            .add("key2", "original")
+            .add("key3", "value3")
+            .build();
+        final YamlMapping original = Yaml.createYamlMappingBuilder()
+                .add("key1", "value1")
+                .add("key2", "original")
+                .build();
+        final YamlMapping changed = Yaml.createYamlMappingBuilder()
+                .add("key2", "changed")
+                .add("key3", "value3")
+                .build();
+        MatcherAssert.assertThat(
+            new MergedYamlMapping(original, changed, false),
+            Matchers.equalTo(expected)
+        );
+        MatcherAssert.assertThat(
+            new MergedYamlMapping(original, changed),
+            Matchers.equalTo(expected)
+        );
+    }
+
+    /**
+     * It should merge by overriding conflicting keys.
+     */
+    @Test
+    public void mergesWithConflictsDoOverride() {
+        final YamlMapping expected = Yaml.createYamlMappingBuilder()
+            .add("key1", "value1")
+            .add("key2", "changed!")
+            .add("key3", "value3")
+            .build();
+        final YamlMapping original = Yaml.createYamlMappingBuilder()
+            .add("key1", "value1")
+            .add("key2", "original")
+            .build();
+        final YamlMapping changed = Yaml.createYamlMappingBuilder()
+            .add("key2", "changed!")
+            .add("key3", "value3")
+            .build();
+        MatcherAssert.assertThat(
+            new MergedYamlMapping(original, changed, true),
+            Matchers.equalTo(expected)
+        );
+    }
+
+    /**
+     * It should merge the original one with the changed one provided
+     * by a Supplier.
+     */
+    @Test
+    public void mergesFromSupplier() {
+        final YamlMapping expected = Yaml.createYamlMappingBuilder()
+                .add("key1", "value1")
+                .add("key2", "value2")
+                .add("key3", "value3")
+                .add("key4", "value4")
+                .build();
+        final YamlMapping original = Yaml.createYamlMappingBuilder()
+                .add("key1", "value1")
+                .add("key2", "value2")
+                .build();
+        MatcherAssert.assertThat(
+            new MergedYamlMapping(
+                original,
+                () -> Yaml.createYamlMappingBuilder()
+                    .add("key3", "value3")
+                    .add("key4", "value4")
+                    .build()
+            ),
+            Matchers.equalTo(expected)
+        );
+    }
+
+    /**
+     * It should override a conflicting key even if the new value is null.
+     */
+    @Test
+    public void overridesWithNullValue() {
+        final String nullValue = null;
+        final YamlMapping expected = Yaml.createYamlMappingBuilder()
+            .add("key1", "value1")
+            .add("key2", nullValue)
+            .add("key3", "value3")
+            .build();
+        final YamlMapping original = Yaml.createYamlMappingBuilder()
+            .add("key1", "value1")
+            .add("key2", "original")
+            .build();
+        final YamlMapping changed = Yaml.createYamlMappingBuilder()
+            .add("key2", nullValue)
+            .add("key3", "value3")
+            .build();
+        MatcherAssert.assertThat(
+            new MergedYamlMapping(original, changed, true),
+            Matchers.equalTo(expected)
+        );
+    }
+}
diff --git a/src/test/java/com/amihaiemil/eoyaml/extensions/MergedYamlSequenceTest.java b/src/test/java/com/amihaiemil/eoyaml/extensions/MergedYamlSequenceTest.java
new file mode 100644
index 0000000..22403dd
--- /dev/null
+++ b/src/test/java/com/amihaiemil/eoyaml/extensions/MergedYamlSequenceTest.java
@@ -0,0 +1,199 @@
+/**
+ * Copyright (c) 2016-2020, Mihai Emil Andronache
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ *  modification, are permitted provided that the following conditions are met:
+ * Redistributions of source code must retain the above copyright notice, this
+ *  list of conditions and the following disclaimer.
+ *  Redistributions in binary form must reproduce the above copyright notice,
+ *  this list of conditions and the following disclaimer in the documentation
+ *  and/or other materials provided with the distribution.
+ * Neither the name of the copyright holder nor the names of its
+ *  contributors may be used to endorse or promote products derived from
+ *  this software without specific prior written permission.
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+ * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ * SUCH DAMAGE.
+ */
+package com.amihaiemil.eoyaml.extensions;
+
+import com.amihaiemil.eoyaml.Yaml;
+import com.amihaiemil.eoyaml.YamlSequence;
+import org.hamcrest.MatcherAssert;
+import org.hamcrest.Matchers;
+import org.junit.Test;
+
+/**
+ * Unit tests for {@link MergedYamlSequence}.
+ * @author Mihai Andronache (amihaiemil@gmail.com)
+ * @version $Id$
+ * @since 4.1.0
+ */
+public final class MergedYamlSequenceTest {
+
+    /**
+     * It should complain when both arguments are null.
+     */
+    @Test(expected = IllegalArgumentException.class)
+    public void complainsOnTwoNullSequences() {
+        final YamlSequence original = null;
+        final YamlSequence changed = null;
+        final YamlSequence boom = new MergedYamlSequence(original, changed);
+    }
+
+    /**
+     * Simply adds the elements in one YamlSequence.
+     */
+    @Test
+    public void addsChangedToOriginal() {
+        final YamlSequence expected = Yaml
+            .createYamlSequenceBuilder()
+            .add("first")
+            .add("second")
+            .add("third")
+            .add("fourth")
+            .add("fifth")
+            .build();
+        final YamlSequence original = Yaml
+            .createYamlSequenceBuilder()
+            .add("first")
+            .add("second")
+            .add("third")
+            .build();
+        final YamlSequence changed = Yaml
+            .createYamlSequenceBuilder()
+            .add("fourth")
+            .add("fifth")
+            .build();
+        MatcherAssert.assertThat(
+            new MergedYamlSequence(original, changed),
+            Matchers.equalTo(expected)
+        );
+        MatcherAssert.assertThat(
+            new MergedYamlSequence(original, ()->changed),
+            Matchers.equalTo(expected)
+        );
+    }
+
+    /**
+     * It can override the values at the same indices.
+     */
+    @Test
+    public void overridesIndicesInOriginal() {
+        final YamlSequence expected = Yaml
+            .createYamlSequenceBuilder()
+            .add("fourth")
+            .add("fifth")
+            .add("third")
+            .build();
+        final YamlSequence original = Yaml
+            .createYamlSequenceBuilder()
+            .add("first")
+            .add("second")
+            .add("third")
+            .build();
+        final YamlSequence changed = Yaml
+            .createYamlSequenceBuilder()
+            .add("fourth")
+            .add("fifth")
+            .build();
+        MatcherAssert.assertThat(
+            new MergedYamlSequence(original, changed, true),
+            Matchers.equalTo(expected)
+        );
+        MatcherAssert.assertThat(
+            new MergedYamlSequence(original, ()->changed, true),
+            Matchers.equalTo(expected)
+        );
+    }
+
+    /**
+     * It should represent the Original sequence if the changed one
+     * is null.
+     */
+    @Test
+    public void representsOriginalIfChangedIsNullOrEmpty() {
+        final YamlSequence original = Yaml
+            .createYamlSequenceBuilder().add("value").build();
+        final YamlSequence changed = null;
+        MatcherAssert.assertThat(
+            new MergedYamlSequence(original, changed),
+            Matchers.equalTo(original)
+        );
+        MatcherAssert.assertThat(
+            new MergedYamlSequence(
+                original,
+                Yaml.createYamlSequenceBuilder().build()
+            ),
+            Matchers.equalTo(original)
+        );
+    }
+
+    /**
+     * It should represent the Changed sequence if the original one
+     * is null.
+     */
+    @Test
+    public void representsChangeIfOriginalIsNullOrEmpty() {
+        final YamlSequence original = null;
+        final YamlSequence changed = Yaml
+            .createYamlSequenceBuilder().add("value").build();
+        MatcherAssert.assertThat(
+            new MergedYamlSequence(original, changed),
+            Matchers.equalTo(changed)
+        );
+        MatcherAssert.assertThat(
+            new MergedYamlSequence(
+                Yaml.createYamlSequenceBuilder().build(),
+                changed
+            ),
+            Matchers.equalTo(changed)
+        );
+    }
+
+    /**
+     * Null elements should also be merged.
+     */
+    @Test
+    public void addsNullElementsToOriginal() {
+        final String nullValue = null;
+        final YamlSequence expected = Yaml
+            .createYamlSequenceBuilder()
+            .add("first")
+            .add("second")
+            .add("third")
+            .add("fourth")
+            .add(nullValue)
+            .build();
+        final YamlSequence original = Yaml
+            .createYamlSequenceBuilder()
+            .add("first")
+            .add("second")
+            .add("third")
+            .build();
+        final YamlSequence changed = Yaml
+            .createYamlSequenceBuilder()
+            .add("fourth")
+            .add(nullValue)
+            .build();
+        MatcherAssert.assertThat(
+            new MergedYamlSequence(original, changed),
+            Matchers.equalTo(expected)
+        );
+        MatcherAssert.assertThat(
+            new MergedYamlSequence(original, ()->changed),
+            Matchers.equalTo(expected)
+        );
+    }
+
+}