diff --git a/core/pom.xml b/core/pom.xml
index f3c0f8e2..c29417c8 100644
--- a/core/pom.xml
+++ b/core/pom.xml
@@ -64,6 +64,9 @@
false
+
+ 5.11.4
+
0.9.0
2.29
@@ -73,9 +76,23 @@
3.2.8
3.1.4
3.21.0
+ 0.8.14
+
+
+ org.junit.jupiter
+ junit-jupiter-api
+ ${junit-jupiter.version}
+ test
+
+
+ org.junit.jupiter
+ junit-jupiter-engine
+ ${junit-jupiter.version}
+ test
+
@@ -199,6 +216,37 @@
+
+ metrics
+
+
+
+ org.jacoco
+ jacoco-maven-plugin
+ ${plugin.jacoco.version}
+
+
+ prepare-agent
+
+ prepare-agent
+
+
+
+ report
+ verify
+
+ report
+
+
+
+
+ ${project.build.directory}/jacoco.exec
+ ${project.reporting.outputDirectory}/jacoco
+
+
+
+
+
\ No newline at end of file
diff --git a/core/src/main/java/org/javahelpers/simple/builders/core/builders/ArrayListBuilder.java b/core/src/main/java/org/javahelpers/simple/builders/core/builders/ArrayListBuilder.java
index 6b5d13d6..8d94a172 100644
--- a/core/src/main/java/org/javahelpers/simple/builders/core/builders/ArrayListBuilder.java
+++ b/core/src/main/java/org/javahelpers/simple/builders/core/builders/ArrayListBuilder.java
@@ -36,6 +36,20 @@
public class ArrayListBuilder {
private final List mList = new ArrayList<>();
+ /** Creates an empty ArrayListBuilder. */
+ public ArrayListBuilder() {}
+
+ /**
+ * Creates an ArrayListBuilder initialized with the elements from the given list.
+ *
+ * @param initialList list to initialize from
+ */
+ public ArrayListBuilder(List initialList) {
+ if (initialList != null) {
+ mList.addAll(initialList);
+ }
+ }
+
/**
* Appends the element to the end of the internal list. Calling add-function on inner ArrayList
* field.
diff --git a/core/src/main/java/org/javahelpers/simple/builders/core/builders/HashMapBuilder.java b/core/src/main/java/org/javahelpers/simple/builders/core/builders/HashMapBuilder.java
index f3dfc190..e66e41ad 100644
--- a/core/src/main/java/org/javahelpers/simple/builders/core/builders/HashMapBuilder.java
+++ b/core/src/main/java/org/javahelpers/simple/builders/core/builders/HashMapBuilder.java
@@ -37,6 +37,20 @@
public class HashMapBuilder {
private final Map mMap = new HashMap<>();
+ /** Creates an empty HashMapBuilder. */
+ public HashMapBuilder() {}
+
+ /**
+ * Creates a HashMapBuilder initialized with the mappings from the given map.
+ *
+ * @param initialMap map to initialize from
+ */
+ public HashMapBuilder(Map initialMap) {
+ if (initialMap != null) {
+ mMap.putAll(initialMap);
+ }
+ }
+
/**
* Associates the specified value with the specified key in this map. Using an inner HashMap
* implementation. If the key already exists in the map, the old value is replaced.
diff --git a/core/src/main/java/org/javahelpers/simple/builders/core/builders/HashSetBuilder.java b/core/src/main/java/org/javahelpers/simple/builders/core/builders/HashSetBuilder.java
index 57448b23..d49dcb4f 100644
--- a/core/src/main/java/org/javahelpers/simple/builders/core/builders/HashSetBuilder.java
+++ b/core/src/main/java/org/javahelpers/simple/builders/core/builders/HashSetBuilder.java
@@ -36,9 +36,22 @@
public class HashSetBuilder {
private final Set mSet = new HashSet<>();
+ /** Creates an empty HashSetBuilder. */
+ public HashSetBuilder() {}
+
+ /**
+ * Creates a HashSetBuilder initialized with the elements from the given set.
+ *
+ * @param initialSet set to initialize from
+ */
+ public HashSetBuilder(Set initialSet) {
+ if (initialSet != null) {
+ mSet.addAll(initialSet);
+ }
+ }
+
/**
- * Adds the specified element to this set if it is not already present. Calling add-function on
- * inner HashSet field.
+ * Appends the element to the internal set. Calling add-function on inner HashSet field.
*
* @param element element to be appended
* @return current instance of ArrayListBuilder for using in stream-notation
diff --git a/core/src/main/java/org/javahelpers/simple/builders/core/util/TrackedValue.java b/core/src/main/java/org/javahelpers/simple/builders/core/util/TrackedValue.java
index 8d4f6854..8cc2a919 100644
--- a/core/src/main/java/org/javahelpers/simple/builders/core/util/TrackedValue.java
+++ b/core/src/main/java/org/javahelpers/simple/builders/core/util/TrackedValue.java
@@ -43,17 +43,27 @@
* @param the value type
* @param value the underlying value (may be {@code null})
* @param isChanged whether the value was explicitly changed by the builder API
+ * @param isInitial whether the value is an initial value from an existing instance
*/
-public record TrackedValue(T value, boolean isChanged) {
+public record TrackedValue(T value, boolean isChanged, boolean isInitial) {
/**
- * Executes the provided {@link Consumer} only if this value has been explicitly changed.
+ * Checks if this value has been set (either as initial value or changed).
*
- * @param consumer action to perform with the current {@link #value()} when {@link #isChanged()}
- * is {@code true}
+ * @return true if the value is set (initial or changed), false if unset
*/
- public void ifChanged(Consumer consumer) {
- if (isChanged) {
+ public boolean isSet() {
+ return isChanged || isInitial;
+ }
+
+ /**
+ * Executes the provided {@link Consumer} only if this value has been set (initial value or
+ * changed).
+ *
+ * @param consumer action to perform with the current {@link #value()}
+ */
+ public void ifSet(Consumer consumer) {
+ if (isSet()) {
consumer.accept(value);
}
}
@@ -62,10 +72,11 @@ public void ifChanged(Consumer consumer) {
* Creates a tracked value representing an unset state (no change made).
*
* @param the value type
- * @return an instance with {@code value == null} and {@code isChanged == false}
+ * @return an instance with {@code value == null}, {@code isChanged == false}, and {@code
+ * isInitial == false}
*/
public static TrackedValue unsetValue() {
- return new TrackedValue<>(null, false);
+ return new TrackedValue<>(null, false, false);
}
/**
@@ -74,10 +85,11 @@ public static TrackedValue unsetValue() {
*
* @param the value type
* @param value the initial value (may be {@code null})
- * @return an instance with the given value and {@code isChanged == false}
+ * @return an instance with the given value, {@code isChanged == false}, and {@code isInitial ==
+ * true}
*/
public static TrackedValue initialValue(T value) {
- return new TrackedValue<>(value, false);
+ return new TrackedValue<>(value, false, true);
}
/**
@@ -88,6 +100,6 @@ public static TrackedValue initialValue(T value) {
* @return an instance with the given value and {@code isChanged == true}
*/
public static TrackedValue changedValue(T value) {
- return new TrackedValue<>(value, true);
+ return new TrackedValue<>(value, true, false);
}
}
diff --git a/core/src/test/java/.gitkeep b/core/src/test/java/.gitkeep
deleted file mode 100644
index e69de29b..00000000
diff --git a/core/src/test/java/org/javahelpers/simple/builders/core/builders/ArrayListBuilderTest.java b/core/src/test/java/org/javahelpers/simple/builders/core/builders/ArrayListBuilderTest.java
new file mode 100644
index 00000000..47603b17
--- /dev/null
+++ b/core/src/test/java/org/javahelpers/simple/builders/core/builders/ArrayListBuilderTest.java
@@ -0,0 +1,139 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2025 Andreas Igel
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package org.javahelpers.simple.builders.core.builders;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.util.Arrays;
+import java.util.List;
+import org.junit.jupiter.api.Test;
+
+class ArrayListBuilderTest {
+
+ @Test
+ void shouldCreateEmptyBuilder() {
+ ArrayListBuilder builder = new ArrayListBuilder<>();
+ List result = builder.build();
+
+ assertNotNull(result);
+ assertTrue(result.isEmpty());
+ }
+
+ @Test
+ void shouldCreateBuilderWithInitialList() {
+ List initial = Arrays.asList("item1", "item2", "item3");
+
+ ArrayListBuilder builder = new ArrayListBuilder<>(initial);
+ List result = builder.build();
+
+ assertNotNull(result);
+ assertEquals(3, result.size());
+ assertEquals("item1", result.get(0));
+ assertEquals("item2", result.get(1));
+ assertEquals("item3", result.get(2));
+ }
+
+ @Test
+ void shouldHandleNullInitialList() {
+ ArrayListBuilder builder = new ArrayListBuilder<>(null);
+ List result = builder.build();
+
+ assertNotNull(result);
+ assertTrue(result.isEmpty());
+ }
+
+ @Test
+ void shouldAllowAddingToInitializedList() {
+ List initial = Arrays.asList("item1", "item2");
+
+ ArrayListBuilder builder = new ArrayListBuilder<>(initial);
+ builder.add("item3").add("item4");
+ List result = builder.build();
+
+ assertNotNull(result);
+ assertEquals(4, result.size());
+ assertEquals("item1", result.get(0));
+ assertEquals("item2", result.get(1));
+ assertEquals("item3", result.get(2));
+ assertEquals("item4", result.get(3));
+ }
+
+ @Test
+ void shouldAddSingleElement() {
+ ArrayListBuilder builder = new ArrayListBuilder<>();
+ builder.add("test");
+ List result = builder.build();
+
+ assertEquals(1, result.size());
+ assertEquals("test", result.get(0));
+ }
+
+ @Test
+ void shouldAddMultipleElements() {
+ ArrayListBuilder builder = new ArrayListBuilder<>();
+ builder.add("first").add("second").add("third");
+ List result = builder.build();
+
+ assertEquals(3, result.size());
+ assertEquals("first", result.get(0));
+ assertEquals("second", result.get(1));
+ assertEquals("third", result.get(2));
+ }
+
+ @Test
+ void shouldAddAllFromCollection() {
+ ArrayListBuilder builder = new ArrayListBuilder<>();
+ builder.addAll(Arrays.asList(1, 2, 3));
+ List result = builder.build();
+
+ assertEquals(3, result.size());
+ assertEquals(1, result.get(0));
+ assertEquals(2, result.get(1));
+ assertEquals(3, result.get(2));
+ }
+
+ @Test
+ void shouldChainAddAndAddAll() {
+ ArrayListBuilder builder = new ArrayListBuilder<>();
+ builder.add("first").addAll(Arrays.asList("second", "third")).add("fourth");
+ List result = builder.build();
+
+ assertEquals(4, result.size());
+ assertEquals("first", result.get(0));
+ assertEquals("second", result.get(1));
+ assertEquals("third", result.get(2));
+ assertEquals("fourth", result.get(3));
+ }
+
+ @Test
+ void shouldReturnThisForFluentInterface() {
+ ArrayListBuilder builder = new ArrayListBuilder<>();
+ ArrayListBuilder result = builder.add("test");
+
+ assertEquals(builder, result);
+ }
+}
diff --git a/core/src/test/java/org/javahelpers/simple/builders/core/builders/HashMapBuilderTest.java b/core/src/test/java/org/javahelpers/simple/builders/core/builders/HashMapBuilderTest.java
new file mode 100644
index 00000000..9b47c95f
--- /dev/null
+++ b/core/src/test/java/org/javahelpers/simple/builders/core/builders/HashMapBuilderTest.java
@@ -0,0 +1,172 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2025 Andreas Igel
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package org.javahelpers.simple.builders.core.builders;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.util.HashMap;
+import java.util.Map;
+import org.junit.jupiter.api.Test;
+
+class HashMapBuilderTest {
+
+ @Test
+ void shouldCreateEmptyBuilder() {
+ HashMapBuilder builder = new HashMapBuilder<>();
+ Map result = builder.build();
+
+ assertNotNull(result);
+ assertTrue(result.isEmpty());
+ }
+
+ @Test
+ void shouldCreateBuilderWithInitialMap() {
+ Map initial = new HashMap<>();
+ initial.put("one", 1);
+ initial.put("two", 2);
+ initial.put("three", 3);
+
+ HashMapBuilder builder = new HashMapBuilder<>(initial);
+ Map result = builder.build();
+
+ assertNotNull(result);
+ assertEquals(3, result.size());
+ assertEquals(1, result.get("one"));
+ assertEquals(2, result.get("two"));
+ assertEquals(3, result.get("three"));
+ }
+
+ @Test
+ void shouldHandleNullInitialMap() {
+ HashMapBuilder builder = new HashMapBuilder<>(null);
+ Map result = builder.build();
+
+ assertNotNull(result);
+ assertTrue(result.isEmpty());
+ }
+
+ @Test
+ void shouldAllowAddingToInitializedMap() {
+ Map initial = new HashMap<>();
+ initial.put("one", 1);
+ initial.put("two", 2);
+
+ HashMapBuilder builder = new HashMapBuilder<>(initial);
+ builder.put("three", 3).put("four", 4);
+ Map result = builder.build();
+
+ assertNotNull(result);
+ assertEquals(4, result.size());
+ assertEquals(1, result.get("one"));
+ assertEquals(2, result.get("two"));
+ assertEquals(3, result.get("three"));
+ assertEquals(4, result.get("four"));
+ }
+
+ @Test
+ void shouldPutSingleEntry() {
+ HashMapBuilder builder = new HashMapBuilder<>();
+ builder.put("key", "value");
+ Map result = builder.build();
+
+ assertEquals(1, result.size());
+ assertEquals("value", result.get("key"));
+ }
+
+ @Test
+ void shouldPutMultipleEntries() {
+ HashMapBuilder builder = new HashMapBuilder<>();
+ builder.put("first", 1).put("second", 2).put("third", 3);
+ Map result = builder.build();
+
+ assertEquals(3, result.size());
+ assertEquals(1, result.get("first"));
+ assertEquals(2, result.get("second"));
+ assertEquals(3, result.get("third"));
+ }
+
+ @Test
+ void shouldOverwriteExistingKey() {
+ HashMapBuilder builder = new HashMapBuilder<>();
+ builder.put("key", 1).put("key", 2);
+ Map result = builder.build();
+
+ assertEquals(1, result.size());
+ assertEquals(2, result.get("key"));
+ }
+
+ @Test
+ void shouldPutAllFromMap() {
+ HashMapBuilder builder = new HashMapBuilder<>();
+ Map additional = new HashMap<>();
+ additional.put("a", 1);
+ additional.put("b", 2);
+
+ builder.putAll(additional);
+ Map result = builder.build();
+
+ assertEquals(2, result.size());
+ assertEquals(1, result.get("a"));
+ assertEquals(2, result.get("b"));
+ }
+
+ @Test
+ void shouldChainPutAndPutAll() {
+ HashMapBuilder builder = new HashMapBuilder<>();
+ Map additional = new HashMap<>();
+ additional.put("two", 2);
+ additional.put("three", 3);
+
+ builder.put("one", 1).putAll(additional).put("four", 4);
+ Map result = builder.build();
+
+ assertEquals(4, result.size());
+ assertEquals(1, result.get("one"));
+ assertEquals(2, result.get("two"));
+ assertEquals(3, result.get("three"));
+ assertEquals(4, result.get("four"));
+ }
+
+ @Test
+ void shouldHandleNullValues() {
+ HashMapBuilder builder = new HashMapBuilder<>();
+ builder.put("key", null);
+ Map result = builder.build();
+
+ assertEquals(1, result.size());
+ assertTrue(result.containsKey("key"));
+ assertEquals(null, result.get("key"));
+ }
+
+ @Test
+ void shouldReturnThisForFluentInterface() {
+ HashMapBuilder builder = new HashMapBuilder<>();
+ HashMapBuilder result = builder.put("key", 1);
+
+ assertEquals(builder, result);
+ }
+}
diff --git a/core/src/test/java/org/javahelpers/simple/builders/core/builders/HashSetBuilderTest.java b/core/src/test/java/org/javahelpers/simple/builders/core/builders/HashSetBuilderTest.java
new file mode 100644
index 00000000..883f3d24
--- /dev/null
+++ b/core/src/test/java/org/javahelpers/simple/builders/core/builders/HashSetBuilderTest.java
@@ -0,0 +1,151 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2025 Andreas Igel
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package org.javahelpers.simple.builders.core.builders;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Set;
+import org.junit.jupiter.api.Test;
+
+class HashSetBuilderTest {
+
+ @Test
+ void shouldCreateEmptyBuilder() {
+ HashSetBuilder builder = new HashSetBuilder<>();
+ Set result = builder.build();
+
+ assertNotNull(result);
+ assertTrue(result.isEmpty());
+ }
+
+ @Test
+ void shouldCreateBuilderWithInitialSet() {
+ Set initial = new HashSet<>(Arrays.asList("item1", "item2", "item3"));
+
+ HashSetBuilder builder = new HashSetBuilder<>(initial);
+ Set result = builder.build();
+
+ assertNotNull(result);
+ assertEquals(3, result.size());
+ assertTrue(result.contains("item1"));
+ assertTrue(result.contains("item2"));
+ assertTrue(result.contains("item3"));
+ }
+
+ @Test
+ void shouldHandleNullInitialSet() {
+ HashSetBuilder builder = new HashSetBuilder<>(null);
+ Set result = builder.build();
+
+ assertNotNull(result);
+ assertTrue(result.isEmpty());
+ }
+
+ @Test
+ void shouldAllowAddingToInitializedSet() {
+ Set initial = new HashSet<>(Arrays.asList("item1", "item2"));
+
+ HashSetBuilder builder = new HashSetBuilder<>(initial);
+ builder.add("item3").add("item4");
+ Set result = builder.build();
+
+ assertNotNull(result);
+ assertEquals(4, result.size());
+ assertTrue(result.contains("item1"));
+ assertTrue(result.contains("item2"));
+ assertTrue(result.contains("item3"));
+ assertTrue(result.contains("item4"));
+ }
+
+ @Test
+ void shouldAddSingleElement() {
+ HashSetBuilder builder = new HashSetBuilder<>();
+ builder.add("test");
+ Set result = builder.build();
+
+ assertEquals(1, result.size());
+ assertTrue(result.contains("test"));
+ }
+
+ @Test
+ void shouldAddMultipleElements() {
+ HashSetBuilder builder = new HashSetBuilder<>();
+ builder.add("first").add("second").add("third");
+ Set result = builder.build();
+
+ assertEquals(3, result.size());
+ assertTrue(result.contains("first"));
+ assertTrue(result.contains("second"));
+ assertTrue(result.contains("third"));
+ }
+
+ @Test
+ void shouldHandleDuplicateElements() {
+ HashSetBuilder builder = new HashSetBuilder<>();
+ builder.add("duplicate").add("duplicate").add("unique");
+ Set result = builder.build();
+
+ assertEquals(2, result.size());
+ assertTrue(result.contains("duplicate"));
+ assertTrue(result.contains("unique"));
+ }
+
+ @Test
+ void shouldAddAllFromSet() {
+ HashSetBuilder builder = new HashSetBuilder<>();
+ builder.addAll(new HashSet<>(Arrays.asList(1, 2, 3)));
+ Set result = builder.build();
+
+ assertEquals(3, result.size());
+ assertTrue(result.contains(1));
+ assertTrue(result.contains(2));
+ assertTrue(result.contains(3));
+ }
+
+ @Test
+ void shouldChainAddAndAddAll() {
+ HashSetBuilder builder = new HashSetBuilder<>();
+ builder.add("first").addAll(new HashSet<>(Arrays.asList("second", "third"))).add("fourth");
+ Set result = builder.build();
+
+ assertEquals(4, result.size());
+ assertTrue(result.contains("first"));
+ assertTrue(result.contains("second"));
+ assertTrue(result.contains("third"));
+ assertTrue(result.contains("fourth"));
+ }
+
+ @Test
+ void shouldReturnThisForFluentInterface() {
+ HashSetBuilder builder = new HashSetBuilder<>();
+ HashSetBuilder result = builder.add("test");
+
+ assertEquals(builder, result);
+ }
+}
diff --git a/core/src/test/java/org/javahelpers/simple/builders/core/util/TrackedValueTest.java b/core/src/test/java/org/javahelpers/simple/builders/core/util/TrackedValueTest.java
new file mode 100644
index 00000000..46aab1cf
--- /dev/null
+++ b/core/src/test/java/org/javahelpers/simple/builders/core/util/TrackedValueTest.java
@@ -0,0 +1,210 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2025 Andreas Igel
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package org.javahelpers.simple.builders.core.util;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicReference;
+import org.junit.jupiter.api.Test;
+
+class TrackedValueTest {
+
+ @Test
+ void shouldCreateUnsetValue() {
+ TrackedValue tracked = TrackedValue.unsetValue();
+
+ assertNull(tracked.value());
+ assertFalse(tracked.isChanged());
+ assertFalse(tracked.isInitial());
+ assertFalse(tracked.isSet());
+ }
+
+ @Test
+ void shouldCreateInitialValue() {
+ TrackedValue tracked = TrackedValue.initialValue("test");
+
+ assertEquals("test", tracked.value());
+ assertFalse(tracked.isChanged());
+ assertTrue(tracked.isInitial());
+ assertTrue(tracked.isSet());
+ }
+
+ @Test
+ void shouldCreateInitialValueWithNull() {
+ TrackedValue tracked = TrackedValue.initialValue(null);
+
+ assertNull(tracked.value());
+ assertFalse(tracked.isChanged());
+ assertTrue(tracked.isInitial());
+ assertTrue(tracked.isSet());
+ }
+
+ @Test
+ void shouldCreateChangedValue() {
+ TrackedValue tracked = TrackedValue.changedValue("modified");
+
+ assertEquals("modified", tracked.value());
+ assertTrue(tracked.isChanged());
+ assertFalse(tracked.isInitial());
+ assertTrue(tracked.isSet());
+ }
+
+ @Test
+ void shouldCreateChangedValueWithNull() {
+ TrackedValue tracked = TrackedValue.changedValue(null);
+
+ assertNull(tracked.value());
+ assertTrue(tracked.isChanged());
+ assertFalse(tracked.isInitial());
+ assertTrue(tracked.isSet());
+ }
+
+ @Test
+ void shouldReturnTrueForIsSetWhenChanged() {
+ TrackedValue tracked = TrackedValue.changedValue("value");
+
+ assertTrue(tracked.isSet());
+ }
+
+ @Test
+ void shouldReturnTrueForIsSetWhenInitial() {
+ TrackedValue tracked = TrackedValue.initialValue("value");
+
+ assertTrue(tracked.isSet());
+ }
+
+ @Test
+ void shouldReturnFalseForIsSetWhenUnset() {
+ TrackedValue tracked = TrackedValue.unsetValue();
+
+ assertFalse(tracked.isSet());
+ }
+
+ @Test
+ void shouldExecuteConsumerWhenChanged() {
+ TrackedValue tracked = TrackedValue.changedValue("test");
+ AtomicBoolean executed = new AtomicBoolean(false);
+ AtomicReference capturedValue = new AtomicReference<>();
+
+ tracked.ifSet(
+ value -> {
+ executed.set(true);
+ capturedValue.set(value);
+ });
+
+ assertTrue(executed.get());
+ assertEquals("test", capturedValue.get());
+ }
+
+ @Test
+ void shouldExecuteConsumerWhenInitial() {
+ TrackedValue tracked = TrackedValue.initialValue("initial");
+ AtomicBoolean executed = new AtomicBoolean(false);
+ AtomicReference capturedValue = new AtomicReference<>();
+
+ tracked.ifSet(
+ value -> {
+ executed.set(true);
+ capturedValue.set(value);
+ });
+
+ assertTrue(executed.get());
+ assertEquals("initial", capturedValue.get());
+ }
+
+ @Test
+ void shouldNotExecuteConsumerWhenUnset() {
+ TrackedValue tracked = TrackedValue.unsetValue();
+ AtomicBoolean executed = new AtomicBoolean(false);
+
+ tracked.ifSet(value -> executed.set(true));
+
+ assertFalse(executed.get());
+ }
+
+ @Test
+ void shouldPassNullToConsumerWhenValueIsNull() {
+ TrackedValue tracked = TrackedValue.changedValue(null);
+ AtomicBoolean executed = new AtomicBoolean(false);
+ AtomicReference capturedValue = new AtomicReference<>("not-null");
+
+ tracked.ifSet(
+ value -> {
+ executed.set(true);
+ capturedValue.set(value);
+ });
+
+ assertTrue(executed.get());
+ assertNull(capturedValue.get());
+ }
+
+ @Test
+ void shouldSupportRecordEquality() {
+ TrackedValue tracked1 = new TrackedValue<>("test", true, false);
+ TrackedValue tracked2 = new TrackedValue<>("test", true, false);
+
+ assertEquals(tracked1, tracked2);
+ }
+
+ @Test
+ void shouldSupportRecordHashCode() {
+ TrackedValue tracked1 = new TrackedValue<>("test", true, false);
+ TrackedValue tracked2 = new TrackedValue<>("test", true, false);
+
+ assertEquals(tracked1.hashCode(), tracked2.hashCode());
+ }
+
+ @Test
+ void shouldDistinguishDifferentStates() {
+ TrackedValue unset = TrackedValue.unsetValue();
+ TrackedValue initial = TrackedValue.initialValue("test");
+ TrackedValue changed = TrackedValue.changedValue("test");
+
+ // Unset vs Initial
+ assertFalse(unset.isSet());
+ assertTrue(initial.isSet());
+
+ // Initial vs Changed
+ assertTrue(initial.isInitial());
+ assertFalse(changed.isInitial());
+ assertFalse(initial.isChanged());
+ assertTrue(changed.isChanged());
+ }
+
+ @Test
+ void shouldWorkWithComplexTypes() {
+ TrackedValue intTracked = TrackedValue.changedValue(42);
+ TrackedValue doubleTracked = TrackedValue.initialValue(3.14);
+
+ assertEquals(42, intTracked.value());
+ assertEquals(3.14, doubleTracked.value());
+ assertTrue(intTracked.isSet());
+ assertTrue(doubleTracked.isSet());
+ }
+}
diff --git a/example/src/test/java/org/javahelpers/simple/builders/example/PersonDtoBuilderTest.java b/example/src/test/java/org/javahelpers/simple/builders/example/PersonDtoBuilderTest.java
index ba234ce5..75e9ab50 100644
--- a/example/src/test/java/org/javahelpers/simple/builders/example/PersonDtoBuilderTest.java
+++ b/example/src/test/java/org/javahelpers/simple/builders/example/PersonDtoBuilderTest.java
@@ -38,18 +38,22 @@ void testBuilder() {
PersonDtoBuilder.create()
.birthdate(LocalDate.now())
.nickNames(b -> b.add("Test1").add("Test2"))
+ .nickNames(b -> b.add("Test3").add("Test4"))
.mannschaft(
mb ->
mb.name("Testmannschaft")
.sponsoren(
sb -> sb.add(SponsorDtoBuilder.create().name("TestSponsor").build())))
.name(this::nameSupplier)
+ .mannschaft(mb -> mb.sponsoren(sb -> sb.add(SponsorDtoBuilder.create().name("TestSponsor2").build())))
.build();
assertNotNull(personDto);
assertNotNull(personDto.getBirthdate());
assertEquals("Testname", personDto.getName());
assertNotNull(personDto.getMannschaft());
assertEquals("Testmannschaft", personDto.getMannschaft().getName());
+ assertEquals(4, personDto.getNickNames().size());
+ assertEquals(2, personDto.getMannschaft().getSponsoren().size());
}
private String nameSupplier() {
diff --git a/pom.xml b/pom.xml
index e1ba8ab2..09fa2537 100644
--- a/pom.xml
+++ b/pom.xml
@@ -73,7 +73,7 @@
Simple Builders
https://sonarcloud.io
${project.basedir}/core/target/classes,${project.basedir}/processor/target/classes
- ${project.basedir}/processor/target/site/jacoco/jacoco.xml
+ ${project.basedir}/core/target/site/jacoco/jacoco.xml,${project.basedir}/processor/target/site/jacoco/jacoco.xml
**/example/**
**/*Mapper*.java,**/processor/dtos/**,**/generated/**
diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/BuilderDefinitionCreator.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/BuilderDefinitionCreator.java
index 0cbf7220..1499b520 100644
--- a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/BuilderDefinitionCreator.java
+++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/BuilderDefinitionCreator.java
@@ -489,7 +489,7 @@ private static MethodDto createFieldConsumer(String fieldName, TypeName fieldTyp
methodDto.setMethodType(MethodTypes.CONSUMER);
methodDto.setCode(
"""
- $helperType:T consumer = new $helperType:T();
+ $helperType:T consumer = this.$fieldName:N.isSet() ? this.$fieldName:N.value() : new $helperType:T();
$dtoMethodParam:N.accept(consumer);
this.$fieldName:N = $builderFieldWrapper:T.changedValue(consumer);
return this;
@@ -545,7 +545,7 @@ private static MethodDto createFieldConsumerWithBuilder(String fieldName, TypeNa
methodDto.setMethodType(MethodTypes.CONSUMER_BY_BUILDER);
methodDto.setCode(
"""
- $helperType:T builder = new $helperType:T();
+ $helperType:T builder = this.$fieldName:N.isSet() ? new $helperType:T(this.$fieldName:N.value()) : new $helperType:T();
$dtoMethodParam:N.accept(builder);
this.$fieldName:N = $builderFieldWrapper:T.changedValue(builder.build());
return this;
diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/JavaCodeGenerator.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/JavaCodeGenerator.java
index 2a99207b..5d261417 100644
--- a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/JavaCodeGenerator.java
+++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/JavaCodeGenerator.java
@@ -281,9 +281,9 @@ private MethodSpec createMethodBuild(
mb.addStatement("$1T result = new $2T<>($3L)", returnType, dtoBaseClass, ctorArgs);
}
- // Apply setter-based fields only when changed
+ // Apply setter-based fields only when set
for (FieldDto f : setterFields) {
- mb.addStatement("this.$N.ifChanged(result::$N)", f.getFieldName(), f.getSetterName());
+ mb.addStatement("this.$N.ifSet(result::$N)", f.getFieldName(), f.getSetterName());
}
mb.addStatement("return result");
return mb.build();
diff --git a/processor/src/test/java/org/javahelpers/simple/builders/processor/BuilderProcessorTest.java b/processor/src/test/java/org/javahelpers/simple/builders/processor/BuilderProcessorTest.java
index 6db98c43..c86be7db 100644
--- a/processor/src/test/java/org/javahelpers/simple/builders/processor/BuilderProcessorTest.java
+++ b/processor/src/test/java/org/javahelpers/simple/builders/processor/BuilderProcessorTest.java
@@ -201,7 +201,7 @@ void shouldHandleFieldsOfConstructorAndSetters() {
"public CtorAndSetterBuilder a(int a)", // helpermethod for constructor param
"public CtorAndSetterBuilder a(Supplier aSupplier)", // supplier for constructor
"CtorAndSetter result = new CtorAndSetter(this.a.value());",
- "this.name.ifChanged(result::setName);");
+ "this.name.ifSet(result::setName);");
}
@Test
@@ -247,7 +247,7 @@ void shouldPreferConstructorOverSetterWhenBothExist() {
"public CtorAndSetterSameFieldBuilder name(String name)",
// Expectations on build function
"CtorAndSetterSameField result = new CtorAndSetterSameField(this.a.value());",
- "this.name.ifChanged(result::setName);");
+ "this.name.ifSet(result::setName);");
// expect no setter application for 'a' on build-function
ProcessorAsserts.assertNotContaining(generatedCode, "result.setA(this.a);");
}
@@ -454,7 +454,7 @@ public Address() {}
"public RequiredCustomTypeViaConstructorBuilder address(Supplier addressSupplier)",
// Consumer method for constructor field (the key coverage addition!)
"public RequiredCustomTypeViaConstructorBuilder address(Consumer addressConsumer)",
- "Address consumer = new Address();",
+ "Address consumer = this.address.isSet() ? this.address.value() : new Address();",
"addressConsumer.accept(consumer);");
}
@@ -882,7 +882,7 @@ void shouldGenerateBuildOverrideAndReturnAnInstanceWithCalledSetters() {
@Override
public BuildDoc build() {
BuildDoc result = new BuildDoc();
- this.x.ifChanged(result::setX);
+ this.x.ifSet(result::setX);
return result;
}
""");
@@ -1206,7 +1206,7 @@ public HelperAnno() {}
generatedCode,
"private TrackedValue helper = unsetValue();",
"helperBuilderConsumer",
- "HelperAnnoBuilder builder = new HelperAnnoBuilder();",
+ "HelperAnnoBuilder builder = this.helper.isSet() ? new HelperAnnoBuilder(this.helper.value()) : new HelperAnnoBuilder();",
"helperBuilderConsumer.accept(builder);",
"this.helper = changedValue(builder.build());");
}
@@ -1248,7 +1248,7 @@ public HelperPlain() {}
// Expect consumer method with local var named 'consumer' of HelperPlain
ProcessorAsserts.assertContaining(
generatedCode,
- "HelperPlain consumer = new HelperPlain();",
+ "HelperPlain consumer = this.helperPlain.isSet() ? this.helperPlain.value() : new HelperPlain();",
"helperPlainConsumer.accept(consumer);",
"private TrackedValue helperPlain = unsetValue();");
}