Repository: decorators-squad/eo-yaml
Pull request #592 (2024-01-19 13:59:58)

#591 [at]YamlComment Annotation for Reflected YamlNode
PR for #591 



Patch:
diff --git a/src/main/java/com/amihaiemil/eoyaml/JsonYamlDump.java b/src/main/java/com/amihaiemil/eoyaml/JsonYamlDump.java
index 43bd19d..6cfd3a3 100644
--- a/src/main/java/com/amihaiemil/eoyaml/JsonYamlDump.java
+++ b/src/main/java/com/amihaiemil/eoyaml/JsonYamlDump.java
@@ -54,7 +54,7 @@ final class JsonYamlDump implements YamlDump {
     }
 
     @Override
-    public YamlNode dump() {
+    public YamlNode dump(final String comment) {
         final YamlNode node;
         final JsonValue safeValue;
         if (this.value == null) {
diff --git a/src/main/java/com/amihaiemil/eoyaml/ReflectedYamlDump.java b/src/main/java/com/amihaiemil/eoyaml/ReflectedYamlDump.java
index fc099d3..b5a239f 100644
--- a/src/main/java/com/amihaiemil/eoyaml/ReflectedYamlDump.java
+++ b/src/main/java/com/amihaiemil/eoyaml/ReflectedYamlDump.java
@@ -62,14 +62,14 @@ final class ReflectedYamlDump implements YamlDump {
     }
 
     @Override
-    public YamlNode dump() {
+    public YamlNode dump(final String comment) {
         final YamlNode node;
         if(this.object == null || SCALAR_TYPES.contains(this.object.getClass())) {
-            node = new ReflectedYamlScalar(this.object);
+            node = new ReflectedYamlScalar(this.object, comment);
         } else if(this.object instanceof Collection || this.object.getClass().isArray()){
-            node = new ReflectedYamlSequence(this.object);
+            node = new ReflectedYamlSequence(this.object, comment);
         } else {
-            node = new ReflectedYamlMapping(this.object);
+            node = new ReflectedYamlMapping(this.object, comment);
         }
         return node;
     }
diff --git a/src/main/java/com/amihaiemil/eoyaml/ReflectedYamlMapping.java b/src/main/java/com/amihaiemil/eoyaml/ReflectedYamlMapping.java
index eb6fd61..91fbe63 100644
--- a/src/main/java/com/amihaiemil/eoyaml/ReflectedYamlMapping.java
+++ b/src/main/java/com/amihaiemil/eoyaml/ReflectedYamlMapping.java
@@ -46,11 +46,25 @@ final class ReflectedYamlMapping extends BaseYamlMapping {
      */
     private final Object bean;
 
+    /**
+     * Comment of this mapping.
+     */
+    private final String comment;
+
     /**
      * Constructor.
      * @param bean Serializable get/set Java Bean.
      */
     ReflectedYamlMapping(final Object bean) {
+        this(bean, "");
+    }
+
+    /**
+     * Constructor.
+     * @param bean Serializable get/set Java Bean.
+     * @param comment Comment of this mapping.
+     */
+    ReflectedYamlMapping(final Object bean, final String comment) {
         if(bean instanceof Collection || bean.getClass().isArray()) {
             throw new IllegalArgumentException(
                 "YamlMapping can only be reflected "
@@ -58,6 +72,7 @@ final class ReflectedYamlMapping extends BaseYamlMapping {
             );
         }
         this.bean = bean;
+        this.comment = comment;
     }
 
     @Override
@@ -93,9 +108,17 @@ final class ReflectedYamlMapping extends BaseYamlMapping {
             }
         } else {
             if (key instanceof Scalar) {
-                node = this.objectToYamlNode(
-                    this.invokeMethod(((Scalar) key).value())
-                );
+                final YamlNode reflectedKey = this.keys().stream().filter(
+                    k -> k.asScalar().value().equals(((Scalar) key).value())
+                ).findFirst().orElse(null);
+                if(reflectedKey == null) {
+                    node = null;
+                } else {
+                    node = this.objectToYamlNode(
+                        this.invokeMethod(reflectedKey.asScalar().value()),
+                        reflectedKey.asScalar().comment().value()
+                    );
+                }
             } else {
                 throw new IllegalArgumentException(
                     "Reflected YamlMapping can only have string keys "
@@ -116,7 +139,7 @@ final class ReflectedYamlMapping extends BaseYamlMapping {
 
             @Override
             public String value() {
-                return "";
+                return ReflectedYamlMapping.this.comment;
             }
         };
     }
@@ -155,7 +178,17 @@ final class ReflectedYamlMapping extends BaseYamlMapping {
      * @return YamlNode.
      */
     private YamlNode objectToYamlNode(final Object value) {
-        return Yaml.createYamlDump(value).dump();
+        return this.objectToYamlNode(value, "");
+    }
+
+    /**
+     * Turn a Java Object to an appropriate YAML Node, adding a comment.
+     * @param value Object value.
+     * @param comm String comment.
+     * @return YamlNode.
+     */
+    private YamlNode objectToYamlNode(final Object value, final String comm) {
+        return Yaml.createYamlDump(value).dump(comm);
     }
 
     /**
@@ -207,7 +240,15 @@ final class ReflectedYamlMapping extends BaseYamlMapping {
 
                 @Override
                 public String value() {
-                    return "";
+                    final YamlComment yamlComment = MethodKey.this.method
+                        .getAnnotation(YamlComment.class);
+                    final String value;
+                    if(yamlComment == null) {
+                        value = "";
+                    } else {
+                        value = yamlComment.value();
+                    }
+                    return value;
                 }
             };
         }
diff --git a/src/main/java/com/amihaiemil/eoyaml/ReflectedYamlScalar.java b/src/main/java/com/amihaiemil/eoyaml/ReflectedYamlScalar.java
index 4c76c0f..c8d5105 100644
--- a/src/main/java/com/amihaiemil/eoyaml/ReflectedYamlScalar.java
+++ b/src/main/java/com/amihaiemil/eoyaml/ReflectedYamlScalar.java
@@ -40,12 +40,27 @@ final class ReflectedYamlScalar extends BaseScalar {
      */
     private final Object scalar;
 
+    /**
+     * Comment referring to this scalar.
+     */
+    private final String comment;
+
     /**
      * Constructor.
      * @param scalar Scalar Object
      */
     ReflectedYamlScalar(final Object scalar) {
+        this(scalar, "");
+    }
+
+    /**
+     * Constructor.
+     * @param scalar Scalar Object
+     * @param comment String comment referring to this Scalar.
+     */
+    ReflectedYamlScalar(final Object scalar, final String comment) {
         this.scalar = scalar;
+        this.comment = comment;
     }
 
     @Override
@@ -69,7 +84,7 @@ final class ReflectedYamlScalar extends BaseScalar {
 
             @Override
             public String value() {
-                return "";
+                return ReflectedYamlScalar.this.comment;
             }
         };
     }
diff --git a/src/main/java/com/amihaiemil/eoyaml/ReflectedYamlSequence.java b/src/main/java/com/amihaiemil/eoyaml/ReflectedYamlSequence.java
index d121b83..3ccfd16 100644
--- a/src/main/java/com/amihaiemil/eoyaml/ReflectedYamlSequence.java
+++ b/src/main/java/com/amihaiemil/eoyaml/ReflectedYamlSequence.java
@@ -45,11 +45,25 @@ final class ReflectedYamlSequence extends BaseYamlSequence {
      */
     private final Collection<Object> sequence;
 
+    /**
+     * Comment referring to this sequence.
+     */
+    private final String comment;
+
     /**
      * Constructor.
      * @param sequence Collection or array of Object.
      */
     ReflectedYamlSequence(final Object sequence) {
+        this(sequence, "");
+    }
+
+    /**
+     * Constructor.
+     * @param sequence Collection or array of Object.
+     * @param comment String comment referring to this sequence.
+     */
+    ReflectedYamlSequence(final Object sequence, final String comment) {
         if(sequence instanceof Collection) {
             this.sequence = (Collection<Object>) sequence;
         } else if(sequence.getClass().isArray()) {
@@ -58,9 +72,10 @@ final class ReflectedYamlSequence extends BaseYamlSequence {
         } else {
             throw new IllegalArgumentException(
                 "YamlSequence can only be reflected "
-              + "from a Collection or from an array."
+                    + "from a Collection or from an array."
             );
         }
+        this.comment = comment;
     }
 
     @Override
@@ -82,7 +97,7 @@ final class ReflectedYamlSequence extends BaseYamlSequence {
 
             @Override
             public String value() {
-                return "";
+                return ReflectedYamlSequence.this.comment;
             }
         };
     }
diff --git a/src/main/java/com/amihaiemil/eoyaml/YamlComment.java b/src/main/java/com/amihaiemil/eoyaml/YamlComment.java
new file mode 100644
index 0000000..6f14129
--- /dev/null
+++ b/src/main/java/com/amihaiemil/eoyaml/YamlComment.java
@@ -0,0 +1,25 @@
+package com.amihaiemil.eoyaml;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Annotation destined for the accessor methods (public non-void methods)
+ * of Java Beans to be converted to YAML. The specified value will be the
+ * comment of the corresponding YAML Node.
+ * @author Mihai Andronache (amihaiemil@gmail.com)
+ * @version $Id$
+ * @since 7.1.0
+ */
+@Target(ElementType.METHOD)
+@Retention(RetentionPolicy.RUNTIME)
+public @interface YamlComment {
+
+    /**
+     * String value of the comment.
+     * @return String, never null.
+     */
+    String value();
+}
diff --git a/src/main/java/com/amihaiemil/eoyaml/YamlDump.java b/src/main/java/com/amihaiemil/eoyaml/YamlDump.java
index b6f07da..47e2252 100644
--- a/src/main/java/com/amihaiemil/eoyaml/YamlDump.java
+++ b/src/main/java/com/amihaiemil/eoyaml/YamlDump.java
@@ -36,7 +36,6 @@ package com.amihaiemil.eoyaml;
 public interface YamlDump {
 
     /**
-     *
      * Dump an Object, represent it as YAML.
      * Generally, if the Object is a Collection or on Array, the resulting
      * YamlNode will be a YamlSequence.<br><br>
@@ -44,9 +43,24 @@ public interface YamlDump {
      * YamlNode will be a YamlSequence.<br><br>
      * If the Object is a String, LocalDate, LocaDateTime or a primitive,
      * the resulting YamlNode will be a plain Scalar.
+     * @param comment String comment.
      * @return YAML Representation.
      */
-    YamlNode dump();
+    YamlNode dump(final String comment);
+
+    /**
+     * Dump an Object, represent it as YAML.
+     * Generally, if the Object is a Collection or on Array, the resulting
+     * YamlNode will be a YamlSequence.<br><br>
+     * If the Object is a Map or other kind of Object, the resulting
+     * YamlNode will be a YamlSequence.<br><br>
+     * If the Object is a String, LocalDate, LocaDateTime or a primitive,
+     * the resulting YamlNode will be a plain Scalar.
+     * @return YAML Representation.
+     */
+    default YamlNode dump() {
+        return this.dump("");
+    }
 
     /**
      * Convenience method, equivalent to calling the
@@ -54,7 +68,7 @@ public interface YamlDump {
      * @return YamlMapping.
      */
     default YamlMapping dumpMapping() {
-        return (YamlMapping) this.dump();
+        return this.dumpMapping("");
     }
 
     /**
@@ -63,7 +77,7 @@ public interface YamlDump {
      * @return YamlSequence.
      */
     default YamlSequence dumpSequence() {
-        return (YamlSequence) this.dump();
+        return this.dumpSequence("");
     }
 
     /**
@@ -72,7 +86,37 @@ public interface YamlDump {
      * @return Scalar.
      */
     default Scalar dumpScalar() {
-        return (Scalar) this.dump();
+        return this.dumpScalar("");
+    }
+
+    /**
+     * Convenience method, equivalent to calling the
+     * dump(...) method and casting the YamlNode to YamlMapping.
+     * @param comment String comment referring to the mapping.
+     * @return YamlMapping.
+     */
+    default YamlMapping dumpMapping(final String comment) {
+        return (YamlMapping) this.dump(comment);
+    }
+
+    /**
+     * Convenience method, equivalent to calling the
+     * dump(...) method and casting the YamlNode to YamlSequence.
+     * @param comment String comment referring to the sequence.
+     * @return YamlSequence.
+     */
+    default YamlSequence dumpSequence(final String comment) {
+        return (YamlSequence) this.dump(comment);
+    }
+
+    /**
+     * Convenience method, equivalent to calling the
+     * dump(...) method and casting the YamlNode to Scalar.
+     * @param comment String comment referring to the scalar.
+     * @return Scalar.
+     */
+    default Scalar dumpScalar(final String comment) {
+        return (Scalar) this.dump(comment);
     }
 
 }


Test patch:
diff --git a/src/test/java/com/amihaiemil/eoyaml/ReflectedYamlMappingTest.java b/src/test/java/com/amihaiemil/eoyaml/ReflectedYamlMappingTest.java
index b6d314a..d3fc0cf 100644
--- a/src/test/java/com/amihaiemil/eoyaml/ReflectedYamlMappingTest.java
+++ b/src/test/java/com/amihaiemil/eoyaml/ReflectedYamlMappingTest.java
@@ -64,7 +64,8 @@ public final class ReflectedYamlMappingTest {
     @Test
     public void reflectsKeys() {
         final YamlMapping mapping = new ReflectedYamlMapping(
-            new Student("Mihai", "Test", 20, 3.5, Arrays.asList("Math, CS"))
+            new Student("Mihai", "Test", 20, 3.5, Arrays.asList("Math", "CS")),
+            "Information about a student"
         );
         final List<String> keys = mapping.keys().stream().map(
             key -> ((ReflectedYamlMapping.MethodKey) key).value()
@@ -76,7 +77,44 @@ public final class ReflectedYamlMappingTest {
         MatcherAssert.assertThat(keys, Matchers.hasItem("gpa"));
         MatcherAssert.assertThat(keys, Matchers.hasItem("grades"));
         MatcherAssert.assertThat(keys, Matchers.hasItem("classes"));
+    }
+
+    /**
+     * Comments in a reflected YamlMapping are accessible.
+     */
+    @Test
+    public void reflectsComments() {
+        final YamlMapping mapping = new ReflectedYamlMapping(
+            new Student(
+                "Mihai", "Test", 20, 3.5,
+                Arrays.asList("Math", "CS")
+            ),
+            "Information about a student"
+        );
+        final List<String> keys = mapping.keys().stream().map(
+            key -> ((ReflectedYamlMapping.MethodKey) key).value()
+        ).collect(Collectors.toList());
+        MatcherAssert.assertThat(keys.size(), Matchers.equalTo(6));
         System.out.println(mapping);
+        MatcherAssert.assertThat(
+            mapping.comment().value(),
+            Matchers.equalTo("Information about a student")
+        );
+        mapping.keys().forEach(
+            k -> System.out.println(k.comment().value())
+        );
+        MatcherAssert.assertThat(
+            mapping.value("classes").comment().value(),
+            Matchers.equalTo("Classes the student is enrolled to.")
+        );
+        MatcherAssert.assertThat(
+            mapping.value("grades").comment().value(),
+            Matchers.equalTo("Some grades of the student.")
+        );
+        MatcherAssert.assertThat(
+            mapping.value("firstName").comment().value(),
+            Matchers.equalTo("First name of the Student.")
+        );
     }
 
     /**
@@ -188,6 +226,7 @@ public final class ReflectedYamlMappingTest {
             this.classes = classes;
         }
 
+        @YamlComment("First name of the Student.")
         public String getFirstName() {
             return this.firstName;
         }
@@ -213,6 +252,7 @@ public final class ReflectedYamlMappingTest {
             this.gpa = gpa;
         }
 
+        @YamlComment("Some grades of the student.")
         public Map<String, Integer> getGrades() {
             return this.grades;
         }
@@ -221,6 +261,7 @@ public final class ReflectedYamlMappingTest {
             this.grades = grades;
         }
 
+        @YamlComment("Classes the student is enrolled to.")
         public List<String> getClasses() {
             return this.classes;
         }
diff --git a/src/test/java/com/amihaiemil/eoyaml/ReflectedYamlSequenceTest.java b/src/test/java/com/amihaiemil/eoyaml/ReflectedYamlSequenceTest.java
index 285b14f..12118c9 100644
--- a/src/test/java/com/amihaiemil/eoyaml/ReflectedYamlSequenceTest.java
+++ b/src/test/java/com/amihaiemil/eoyaml/ReflectedYamlSequenceTest.java
@@ -33,6 +33,7 @@ import org.junit.Test;
 import org.mockito.Mockito;
 
 import java.util.*;
+import java.util.stream.Collectors;
 
 /**
  * Unit tests for {@link ReflectedYamlSequence}.
@@ -179,6 +180,49 @@ public final class ReflectedYamlSequenceTest {
         );
     }
 
+    /**
+     * A collection of complex objects can be reflected into a YamlSequence.
+     */
+    @Test
+    public void reflectsObjectCollection() {
+        final YamlSequence sequence = new ReflectedYamlSequence(
+            Arrays.asList(
+                new Student(
+                    "Mihai",
+                    "Test",
+                    20,
+                    3.5,
+                    Arrays.asList("Math", "CS")
+                )
+            )
+        );
+        final YamlMapping firstMapping = sequence.yamlNode(0).asMapping();
+        final List<String> keys = firstMapping.keys().stream().map(
+            key -> ((ReflectedYamlMapping.MethodKey) key).value()
+        ).collect(Collectors.toList());
+        MatcherAssert.assertThat(keys.size(), Matchers.equalTo(6));
+        MatcherAssert.assertThat(keys, Matchers.hasItem("firstName"));
+        MatcherAssert.assertThat(keys, Matchers.hasItem("lastName"));
+        MatcherAssert.assertThat(keys, Matchers.hasItem("age"));
+        MatcherAssert.assertThat(keys, Matchers.hasItem("gpa"));
+        MatcherAssert.assertThat(keys, Matchers.hasItem("grades"));
+        MatcherAssert.assertThat(keys, Matchers.hasItem("classes"));
+
+        MatcherAssert.assertThat(
+            firstMapping.value("classes").comment().value(),
+            Matchers.equalTo("Classes the student is enrolled to.")
+        );
+        MatcherAssert.assertThat(
+            firstMapping.value("grades").comment().value(),
+            Matchers.equalTo("Some grades of the student.")
+        );
+        MatcherAssert.assertThat(
+            firstMapping.value("firstName").comment().value(),
+            Matchers.equalTo("First name of the Student.")
+        );
+        System.out.println(sequence);
+    }
+
     /**
      * Prints a reflected sequence of strings properly.
      */
@@ -204,4 +248,80 @@ public final class ReflectedYamlSequenceTest {
     public void throwsExceptionWhenWrongObject() {
         new ReflectedYamlSequence("wrong");
     }
+
+    /**
+     * Simple student pojo for test.
+     * @checkstyle JavadocVariable (100 lines)
+     * @checkstyle JavadocMethod (100 lines)
+     * @checkstyle HiddenField (100 lines)
+     * @checkstyle ParameterNumber (100 lines)
+     * @checkstyle FinalParameters (100 lines)
+     */
+    static final class Student {
+
+        private String firstName;
+        private String lastName;
+        private int age;
+        private double gpa;
+        private Map<String, Integer> grades = new LinkedHashMap<>();
+
+        private List<String> classes;
+
+        Student(
+            String firstName, String lastName,
+            int age, double gpa, List<String> classes
+        ) {
+            this.firstName = firstName;
+            this.lastName = lastName;
+            this.age = age;
+            this.gpa = gpa;
+            this.grades.put("Math", 9);
+            this.grades.put("CS", 10);
+            this.classes = classes;
+        }
+
+        @YamlComment("First name of the Student.")
+        public String getFirstName() {
+            return this.firstName;
+        }
+        public void setFirstName(String firstName) {
+            this.firstName = firstName;
+        }
+        public String getLastName() {
+            return this.lastName;
+        }
+        public void setLastName(String lastName) {
+            this.lastName = lastName;
+        }
+        public int getAge() {
+            return this.age;
+        }
+        public void setAge(int age) {
+            this.age = age;
+        }
+        public double getGpa() {
+            return this.gpa;
+        }
+        public void setGpa(double gpa) {
+            this.gpa = gpa;
+        }
+
+        @YamlComment("Some grades of the student.")
+        public Map<String, Integer> getGrades() {
+            return this.grades;
+        }
+
+        public void setGrades(Map<String, Integer> grades) {
+            this.grades = grades;
+        }
+
+        @YamlComment("Classes the student is enrolled to.")
+        public List<String> getClasses() {
+            return this.classes;
+        }
+        public void setClasses(List<String> classes) {
+            this.classes = classes;
+        }
+
+    }
 }