From d3312c444433f214b38fe5fc33eab645d7aeefa0 Mon Sep 17 00:00:00 2001
From: Michael Hoennig <michael@hoennig.de>
Date: Fri, 30 Sep 2022 16:27:18 +0200
Subject: [PATCH] improved Stringify

---
 build.gradle                                  |  2 +-
 .../net/hostsharing/hsadminng/Stringify.java  | 52 ++++++++++---
 .../office/partner/HsOfficePartnerEntity.java | 22 +++++-
 .../hsadminng/StringifyUnitTest.java          | 75 ++++++++++++++-----
 .../partner/HsOfficePartnerEntityTest.java    | 34 +++++++++
 ...fficePartnerRepositoryIntegrationTest.java | 16 ++--
 6 files changed, 159 insertions(+), 42 deletions(-)
 create mode 100644 src/test/java/net/hostsharing/hsadminng/hs/office/partner/HsOfficePartnerEntityTest.java

diff --git a/build.gradle b/build.gradle
index 5f6d36a3..0e573ba8 100644
--- a/build.gradle
+++ b/build.gradle
@@ -273,7 +273,7 @@ pitest {
     // As Java unit tests are pretty pointless in our case, this maybe makes not much sense.
     mutationThreshold = 71
     coverageThreshold = 57
-    testStrengthThreshold = 88
+    testStrengthThreshold = 87
 
     outputFormats = ['XML', 'HTML']
     timestampedReports = false
diff --git a/src/main/java/net/hostsharing/hsadminng/Stringify.java b/src/main/java/net/hostsharing/hsadminng/Stringify.java
index 76078329..7cde6757 100644
--- a/src/main/java/net/hostsharing/hsadminng/Stringify.java
+++ b/src/main/java/net/hostsharing/hsadminng/Stringify.java
@@ -4,22 +4,26 @@ import javax.validation.constraints.NotNull;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Objects;
+import java.util.Optional;
 import java.util.function.Function;
 import java.util.stream.Collectors;
 
-// TODO.refa: use this instead of toDisplayName everywhere and add JavaDoc
-public class Stringify<B> {
+import static java.lang.Boolean.TRUE;
+
+public final class Stringify<B> {
 
     private final Class<B> clazz;
     private final String name;
     private final List<Property<B>> props = new ArrayList<>();
+    private String separator = ", ";
+    private Boolean quotedValues = null;
 
     public static <B> Stringify<B> stringify(final Class<B> clazz, final String name) {
-        return new Stringify<B>(clazz, name);
+        return new Stringify<>(clazz, name);
     }
 
     public static <B> Stringify<B> stringify(final Class<B> clazz) {
-        return new Stringify<B>(clazz, null);
+        return new Stringify<>(clazz, null);
     }
 
     private Stringify(final Class<B> clazz, final String name) {
@@ -28,7 +32,12 @@ public class Stringify<B> {
     }
 
     public Stringify<B> withProp(final String propName, final Function<B, ?> getter) {
-        props.add(new Property<B>(propName, getter));
+        props.add(new Property<>(propName, getter));
+        return this;
+    }
+
+    public Stringify<B> withProp(final Function<B, ?> getter) {
+        props.add(new Property<>(null, getter));
         return this;
     }
 
@@ -42,20 +51,43 @@ public class Stringify<B> {
                     }
                     return propVal;
                 })
-                .map(propVal -> propVal.prop.name + "=" + optionallyQuoted(propVal))
-                .collect(Collectors.joining(", "));
+                .map(propVal -> propName(propVal, "=") + optionallyQuoted(propVal))
+                .collect(Collectors.joining(separator));
         return (name != null ? name : object.getClass().getSimpleName()) + "(" + propValues + ")";
     }
 
+    public Stringify<B> withSeparator(final String separator) {
+        this.separator = separator;
+        return this;
+    }
+
+    private String propName(final PropertyValue<B> propVal, final String delimiter) {
+        return Optional.ofNullable(propVal.prop.name).map(v -> v + delimiter).orElse("");
+    }
+
     private String optionallyQuoted(final PropertyValue<B> propVal) {
-        return (propVal.rawValue instanceof Number) || (propVal.rawValue instanceof Boolean)
-                ? propVal.value
-                : "'" + propVal.value + "'";
+        if (quotedValues == null)
+            return quotedQuotedValueType(propVal)
+                    ? ("'" + propVal.value + "'")
+                    : propVal.value;
+        return TRUE == quotedValues
+                ? ("'" + propVal.value + "'")
+                : propVal.value;
+    }
+
+    private static <B> boolean quotedQuotedValueType(final PropertyValue<B> propVal) {
+        return !(propVal.rawValue instanceof Number || propVal.rawValue instanceof Boolean);
+    }
+
+    public Stringify<B> quotedValues(final boolean quotedValues) {
+        this.quotedValues = quotedValues;
+        return this;
     }
 
     private record Property<B>(String name, Function<B, ?> getter) {}
 
     private record PropertyValue<B>(Property<B> prop, Object rawValue, String value) {
+
         static <B> PropertyValue<B> of(Property<B> prop, Object rawValue) {
             return rawValue != null ? new PropertyValue<>(prop, rawValue, rawValue.toString()) : null;
         }
diff --git a/src/main/java/net/hostsharing/hsadminng/hs/office/partner/HsOfficePartnerEntity.java b/src/main/java/net/hostsharing/hsadminng/hs/office/partner/HsOfficePartnerEntity.java
index 0a37b76f..58e7937a 100644
--- a/src/main/java/net/hostsharing/hsadminng/hs/office/partner/HsOfficePartnerEntity.java
+++ b/src/main/java/net/hostsharing/hsadminng/hs/office/partner/HsOfficePartnerEntity.java
@@ -1,6 +1,8 @@
 package net.hostsharing.hsadminng.hs.office.partner;
 
 import lombok.*;
+import net.hostsharing.hsadminng.Stringify;
+import net.hostsharing.hsadminng.Stringifyable;
 import net.hostsharing.hsadminng.hs.office.contact.HsOfficeContactEntity;
 import net.hostsharing.hsadminng.hs.office.person.HsOfficePersonEntity;
 
@@ -8,6 +10,8 @@ import javax.persistence.*;
 import java.time.LocalDate;
 import java.util.UUID;
 
+import static net.hostsharing.hsadminng.Stringify.stringify;
+
 @Entity
 @Table(name = "hs_office_partner_rv")
 @Getter
@@ -15,7 +19,13 @@ import java.util.UUID;
 @Builder
 @NoArgsConstructor
 @AllArgsConstructor
-public class HsOfficePartnerEntity {
+public class HsOfficePartnerEntity implements Stringifyable {
+
+    private static Stringify<HsOfficePartnerEntity> stringify = stringify(HsOfficePartnerEntity.class, "partner")
+            .withProp(HsOfficePartnerEntity::getPerson)
+            .withProp(HsOfficePartnerEntity::getContact)
+            .withSeparator(": ")
+            .quotedValues(false);
 
     private @Id UUID uuid;
 
@@ -33,7 +43,13 @@ public class HsOfficePartnerEntity {
     private @Column(name = "birthday") LocalDate birthday;
     private @Column(name = "dateofdeath") LocalDate dateOfDeath;
 
-    public String getDisplayName() {
-        return "partner(%s, %s)".formatted(person.getDisplayName(), contact.getLabel());
+    @Override
+    public String toString() {
+        return stringify.apply(this);
+    }
+
+    @Override
+    public String toShortString() {
+        return person.toShortString();
     }
 }
diff --git a/src/test/java/net/hostsharing/hsadminng/StringifyUnitTest.java b/src/test/java/net/hostsharing/hsadminng/StringifyUnitTest.java
index e7e22e6b..9bf1870c 100644
--- a/src/test/java/net/hostsharing/hsadminng/StringifyUnitTest.java
+++ b/src/test/java/net/hostsharing/hsadminng/StringifyUnitTest.java
@@ -2,24 +2,17 @@ package net.hostsharing.hsadminng;
 
 import lombok.*;
 import lombok.experimental.FieldNameConstants;
-import net.hostsharing.hsadminng.hs.office.contact.HsOfficeContactEntity;
 import org.junit.jupiter.api.Test;
 
-import javax.persistence.Column;
-import javax.persistence.Entity;
-import javax.persistence.Id;
-import javax.persistence.Table;
 import java.util.UUID;
 
 import static net.hostsharing.hsadminng.Stringify.stringify;
 import static org.assertj.core.api.Assertions.assertThat;
-import static org.junit.jupiter.api.Assertions.*;
 
 class StringifyUnitTest {
 
     @Getter
     @Setter
-    @Builder
     @NoArgsConstructor
     @AllArgsConstructor
     @FieldNameConstants
@@ -27,15 +20,20 @@ class StringifyUnitTest {
 
         private static Stringify<TestBean> toString = stringify(TestBean.class, "bean")
                 .withProp(TestBean.Fields.label, TestBean::getLabel)
-                .withProp(TestBean.Fields.content, TestBean::getContent)
+                .withProp(TestBean.Fields.contentA, TestBean::getContentA)
+                .withProp(TestBean.Fields.contentB, TestBean::getContentB)
+                .withProp(TestBean.Fields.value, TestBean::getValue)
                 .withProp(TestBean.Fields.active, TestBean::isActive);
 
         private UUID uuid;
 
         private String label;
 
-        private SubBean content;
+        private SubBeanWithUnquotedValues contentA;
 
+        private SubBeanWithQuotedValues contentB;
+
+        private int value;
         private boolean active;
 
         @Override
@@ -51,15 +49,41 @@ class StringifyUnitTest {
 
     @Getter
     @Setter
-    @Builder
     @NoArgsConstructor
     @AllArgsConstructor
-    @FieldNameConstants
-    public static class SubBean implements Stringifyable {
+    public static class SubBeanWithUnquotedValues implements Stringifyable {
 
-        private static Stringify<SubBean> toString = stringify(SubBean.class)
-                .withProp(SubBean.Fields.key, SubBean::getKey)
-                .withProp(Fields.value, SubBean::getValue);
+        private static Stringify<SubBeanWithUnquotedValues> toString = stringify(SubBeanWithUnquotedValues.class)
+                .withProp(SubBeanWithUnquotedValues::getKey)
+                .withProp(SubBeanWithUnquotedValues::getValue)
+                .withSeparator(": ")
+                .quotedValues(false);
+
+        private String key;
+        private String value;
+
+        @Override
+        public String toString() {
+            return toString.apply(this);
+        }
+
+        @Override
+        public String toShortString() {
+            return key + ":" + value;
+        }
+    }
+
+    @Getter
+    @Setter
+    @NoArgsConstructor
+    @AllArgsConstructor
+    public static class SubBeanWithQuotedValues implements Stringifyable {
+
+        private static Stringify<SubBeanWithQuotedValues> toString = stringify(SubBeanWithQuotedValues.class)
+                .withProp(SubBeanWithQuotedValues::getKey)
+                .withProp(SubBeanWithQuotedValues::getValue)
+                .withSeparator(": ")
+                .quotedValues(true);
 
         private String key;
         private Integer value;
@@ -78,22 +102,33 @@ class StringifyUnitTest {
     @Test
     void stringifyWhenAllPropsHaveValues() {
         final var given = new TestBean(UUID.randomUUID(), "some label",
-                new SubBean("some content", 1234), false);
+                new SubBeanWithUnquotedValues("some key", "some value"),
+                new SubBeanWithQuotedValues("some key", 1234),
+                42,
+                false);
         final var result = given.toString();
-        assertThat(result).isEqualTo("bean(label='some label', content='some content:1234', active=false)");
+        assertThat(result).isEqualTo(
+                "bean(label='some label', contentA='some key:some value', contentB='some key:1234', value=42, active=false)");
     }
 
     @Test
     void stringifyWhenAllNullablePropsHaveNulValues() {
         final var given = new TestBean();
         final var result = given.toString();
-        assertThat(result).isEqualTo("bean(active=false)");
+        assertThat(result).isEqualTo("bean(value=0, active=false)");
     }
 
     @Test
     void stringifyWithoutExplicitNameUsesSimpleClassName() {
-        final var given = new SubBean("some key", 1234);
+        final var given = new SubBeanWithUnquotedValues("some key", "some value");
         final var result = given.toString();
-        assertThat(result).isEqualTo("SubBean(key='some key', value=1234)");
+        assertThat(result).isEqualTo("SubBeanWithUnquotedValues(some key: some value)");
+    }
+
+    @Test
+    void stringifyWithQuotedValueTrueQuotesEvenIntegers() {
+        final var given = new SubBeanWithQuotedValues("some key", 1234);
+        final var result = given.toString();
+        assertThat(result).isEqualTo("SubBeanWithQuotedValues('some key': '1234')");
     }
 }
diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/partner/HsOfficePartnerEntityTest.java b/src/test/java/net/hostsharing/hsadminng/hs/office/partner/HsOfficePartnerEntityTest.java
new file mode 100644
index 00000000..c28d76d2
--- /dev/null
+++ b/src/test/java/net/hostsharing/hsadminng/hs/office/partner/HsOfficePartnerEntityTest.java
@@ -0,0 +1,34 @@
+package net.hostsharing.hsadminng.hs.office.partner;
+
+import net.hostsharing.hsadminng.hs.office.contact.HsOfficeContactEntity;
+import net.hostsharing.hsadminng.hs.office.person.HsOfficePersonEntity;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+class HsOfficePartnerEntityTest {
+
+    @Test
+    void toStringContainsPersonAndContact() {
+        final var given = HsOfficePartnerEntity.builder()
+                .person(HsOfficePersonEntity.builder().tradeName("some trade name").build())
+                .contact(HsOfficeContactEntity.builder().label("some label").build())
+                .build();
+
+        final var result = given.toString();
+
+        assertThat(result).isEqualTo("partner(some trade name: some label)");
+    }
+
+    @Test
+    void toShortStringContainsPersonAndContact() {
+        final var given = HsOfficePartnerEntity.builder()
+                .person(HsOfficePersonEntity.builder().tradeName("some trade name").build())
+                .contact(HsOfficeContactEntity.builder().label("some label").build())
+                .build();
+
+        final var result = given.toShortString();
+
+        assertThat(result).isEqualTo("some trade name");
+    }
+}
diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/partner/HsOfficePartnerRepositoryIntegrationTest.java b/src/test/java/net/hostsharing/hsadminng/hs/office/partner/HsOfficePartnerRepositoryIntegrationTest.java
index e66b05f4..0808e3e4 100644
--- a/src/test/java/net/hostsharing/hsadminng/hs/office/partner/HsOfficePartnerRepositoryIntegrationTest.java
+++ b/src/test/java/net/hostsharing/hsadminng/hs/office/partner/HsOfficePartnerRepositoryIntegrationTest.java
@@ -150,9 +150,9 @@ class HsOfficePartnerRepositoryIntegrationTest extends ContextBasedTest {
             // then
             allThesePartnersAreReturned(
                     result,
-                    "partner(Third OHG, third contact)",
-                    "partner(Second e.K., second contact)",
-                    "partner(First GmbH, first contact)");
+                    "partner(Third OHG: third contact)",
+                    "partner(Second e.K.: second contact)",
+                    "partner(First GmbH: first contact)");
         }
 
         @Test
@@ -164,7 +164,7 @@ class HsOfficePartnerRepositoryIntegrationTest extends ContextBasedTest {
             final var result = partnerRepo.findPartnerByOptionalNameLike(null);
 
             // then:
-            exactlyThesePartnersAreReturned(result, "partner(First GmbH, first contact)");
+            exactlyThesePartnersAreReturned(result, "partner(First GmbH: first contact)");
         }
     }
 
@@ -180,7 +180,7 @@ class HsOfficePartnerRepositoryIntegrationTest extends ContextBasedTest {
             final var result = partnerRepo.findPartnerByOptionalNameLike("third contact");
 
             // then
-            exactlyThesePartnersAreReturned(result, "partner(Third OHG, third contact)");
+            exactlyThesePartnersAreReturned(result, "partner(Third OHG: third contact)");
         }
     }
 
@@ -392,20 +392,20 @@ class HsOfficePartnerRepositoryIntegrationTest extends ContextBasedTest {
     void cleanup() {
         context("superuser-alex@hostsharing.net", null);
         tempPartners.forEach(tempPartner -> {
-            System.out.println("DELETING temporary partner: " + tempPartner.getDisplayName());
+            System.out.println("DELETING temporary partner: " + tempPartner.toString());
             partnerRepo.deleteByUuid(tempPartner.getUuid());
         });
     }
 
     void exactlyThesePartnersAreReturned(final List<HsOfficePartnerEntity> actualResult, final String... partnerNames) {
         assertThat(actualResult)
-                .extracting(HsOfficePartnerEntity::getDisplayName)
+                .extracting(partnerEntity -> partnerEntity.toString())
                 .containsExactlyInAnyOrder(partnerNames);
     }
 
     void allThesePartnersAreReturned(final List<HsOfficePartnerEntity> actualResult, final String... partnerNames) {
         assertThat(actualResult)
-                .extracting(HsOfficePartnerEntity::getDisplayName)
+                .extracting(partnerEntity -> partnerEntity.toString())
                 .contains(partnerNames);
     }
 }