This is an automated email from the ASF dual-hosted git repository.

paulk pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/groovy.git


The following commit(s) were added to refs/heads/master by this push:
     new 0f78bf3  documentation for records (cont'd) plus minor 
refactoring/cleanup
0f78bf3 is described below

commit 0f78bf3a30480cc76362bcd752e92b3c9d038465
Author: Paul King <[email protected]>
AuthorDate: Fri Oct 29 22:33:48 2021 +1000

    documentation for records (cont'd) plus minor refactoring/cleanup
---
 .../groovy/transform/ImmutableProperties.groovy}   | 33 +++++++++-------------
 src/main/groovy/groovy/transform/RecordType.groovy | 16 +++++------
 src/main/java/groovy/transform/RecordBase.java     | 26 ++++++++++++++++-
 .../transform/ImmutableASTTransformation.java      |  2 +-
 .../transform/RecordTypeASTTransformation.java     | 13 ++++++---
 src/spec/doc/_records.adoc                         | 29 +++++++++++++++++--
 src/spec/test/RecordSpecificationTest.groovy       | 31 ++++++++++++++++++++
 7 files changed, 112 insertions(+), 38 deletions(-)

diff --git a/src/main/java/groovy/transform/RecordBase.java 
b/src/main/groovy/groovy/transform/ImmutableProperties.groovy
similarity index 54%
copy from src/main/java/groovy/transform/RecordBase.java
copy to src/main/groovy/groovy/transform/ImmutableProperties.groovy
index 1265b99..351396c 100644
--- a/src/main/java/groovy/transform/RecordBase.java
+++ b/src/main/groovy/groovy/transform/ImmutableProperties.groovy
@@ -16,31 +16,24 @@
  *  specific language governing permissions and limitations
  *  under the License.
  */
-package groovy.transform;
+package groovy.transform
 
-import org.codehaus.groovy.transform.GroovyASTTransformationClass;
+import groovy.transform.options.ImmutablePropertyHandler
 
-import java.lang.annotation.ElementType;
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
-import java.lang.annotation.Target;
+import java.lang.annotation.ElementType
+import java.lang.annotation.Retention
+import java.lang.annotation.RetentionPolicy
+import java.lang.annotation.Target
 
 /**
- * Class annotation used to assist in the creation of record-like classes.
+ * Meta annotation used when just the immutable properties part of {@code 
@Immutable} is desired.
  *
- * @see ImmutableOptions
- * @see MapConstructor
- * @see TupleConstructor
- * @see PropertyOptions
+ * @see Immutable
  * @since 4.0.0
  */
[email protected]
+@ImmutableOptions
+@PropertyOptions(propertyHandler = ImmutablePropertyHandler)
+@AnnotationCollector(mode = AnnotationCollectorMode.PREFER_EXPLICIT_MERGED)
 @Retention(RetentionPolicy.SOURCE)
-@Target({ElementType.TYPE})
-@GroovyASTTransformationClass("org.codehaus.groovy.transform.RecordTypeASTTransformation")
-public @interface RecordBase {
-    /**
-     * Mode to use when creating record type classes.
-     */
-    RecordTypeMode mode() default RecordTypeMode.AUTO;
-}
+@Target([ElementType.TYPE])
+@interface ImmutableProperties { }
diff --git a/src/main/groovy/groovy/transform/RecordType.groovy 
b/src/main/groovy/groovy/transform/RecordType.groovy
index 044fc27..3941a7b 100644
--- a/src/main/groovy/groovy/transform/RecordType.groovy
+++ b/src/main/groovy/groovy/transform/RecordType.groovy
@@ -18,7 +18,6 @@
  */
 package groovy.transform
 
-import groovy.transform.options.ImmutablePropertyHandler
 import groovy.transform.stc.POJO
 import org.apache.groovy.lang.annotation.Incubating
 
@@ -44,13 +43,12 @@ import java.lang.annotation.Target
  * </pre>
  *
  * The {@code @RecordType} meta-annotation corresponds to adding the following 
annotations:
- * {@link RecordBase},
- * {@link ToString},
- * {@link EqualsAndHashCode},
+ * {@link RecordBase} (which internally piggybacks on {@code @ToString} and 
{@code @EqualsAndHashCode}),
  * {@link ImmutableOptions},
  * {@link PropertyOptions},
- * {@link TupleConstructor},
- * {@link MapConstructor} and
+ * {@link CompileStatic},
+ * {@link POJO},
+ * {@link TupleConstructor} and
  * {@link KnownImmutable}.
  *
  * Together these annotations instruct the compiler to execute the necessary 
transformations to add
@@ -73,13 +71,13 @@ import java.lang.annotation.Target
  * @see PropertyOptions
  * @see TupleConstructor
  * @see KnownImmutable
+ * @see CompileStatic
  * @see POJO
  * @since 4.0.0
  */
 @RecordBase
-@ImmutableOptions
-@PropertyOptions(propertyHandler = ImmutablePropertyHandler)
-@TupleConstructor(namedVariant = true)
+@TupleConstructor(namedVariant = true, force = true)
+@PropertyOptions
 @KnownImmutable
 @POJO
 @CompileStatic
diff --git a/src/main/java/groovy/transform/RecordBase.java 
b/src/main/java/groovy/transform/RecordBase.java
index 1265b99..be8cd27 100644
--- a/src/main/java/groovy/transform/RecordBase.java
+++ b/src/main/java/groovy/transform/RecordBase.java
@@ -43,4 +43,28 @@ public @interface RecordBase {
      * Mode to use when creating record type classes.
      */
     RecordTypeMode mode() default RecordTypeMode.AUTO;
-}
+
+    /**
+     * If {@code true}, this adds a method {@code copyWith} which takes a Map 
of
+     * new property values and returns a new instance of the record class with
+     * these values set.
+     * Example:
+     * <pre class="groovyTestCase">
+     * {@code @groovy.transform.RecordType}(copyWith = true)
+     * class Person {
+     *     String first, last
+     * }
+     *
+     * def tim   = new Person('tim', 'yates')
+     * def alice = tim.copyWith(first:'alice')
+     *
+     * assert tim.toString() == 'Person[first=tim, last=yates]'
+     * assert alice.toString() == 'Person[first=alice, last=yates]'
+     * </pre>
+     * Unknown keys in the map are ignored, and if the values would not change
+     * the object, then the original object is returned.
+     *
+     * If a method called {@code copyWith} that takes a single parameter 
already
+     * exists in the class, then this setting is ignored, and no method is 
generated.
+     */
+    boolean copyWith() default false;}
diff --git 
a/src/main/java/org/codehaus/groovy/transform/ImmutableASTTransformation.java 
b/src/main/java/org/codehaus/groovy/transform/ImmutableASTTransformation.java
index 8907b04..0f2f760 100644
--- 
a/src/main/java/org/codehaus/groovy/transform/ImmutableASTTransformation.java
+++ 
b/src/main/java/org/codehaus/groovy/transform/ImmutableASTTransformation.java
@@ -287,7 +287,7 @@ public class ImmutableASTTransformation extends 
AbstractASTTransformation implem
         );
     }
 
-    private static void createCopyWith(final ClassNode cNode, final 
List<PropertyNode> pList) {
+    static void createCopyWith(final ClassNode cNode, final List<PropertyNode> 
pList) {
         BlockStatement body = new BlockStatement();
         body.addStatement(ifS(
                 orX(
diff --git 
a/src/main/java/org/codehaus/groovy/transform/RecordTypeASTTransformation.java 
b/src/main/java/org/codehaus/groovy/transform/RecordTypeASTTransformation.java
index eab9fd1..bfb8f95 100644
--- 
a/src/main/java/org/codehaus/groovy/transform/RecordTypeASTTransformation.java
+++ 
b/src/main/java/org/codehaus/groovy/transform/RecordTypeASTTransformation.java
@@ -58,6 +58,7 @@ import static 
org.codehaus.groovy.ast.ClassHelper.makeWithoutCaching;
 import static org.codehaus.groovy.ast.tools.GeneralUtils.bytecodeX;
 import static org.codehaus.groovy.ast.tools.GeneralUtils.constX;
 import static org.codehaus.groovy.ast.tools.GeneralUtils.getInstanceProperties;
+import static org.codehaus.groovy.ast.tools.GeneralUtils.hasDeclaredMethod;
 import static org.codehaus.groovy.ast.tools.GeneralUtils.param;
 import static org.codehaus.groovy.ast.tools.GeneralUtils.params;
 import static org.codehaus.groovy.ast.tools.GeneralUtils.stmt;
@@ -142,7 +143,7 @@ public class RecordTypeASTTransformation extends 
AbstractASTTransformation imple
 
         final List<PropertyNode> pList = getInstanceProperties(cNode);
         for (PropertyNode pNode : pList) {
-            adjustPropertyForImmutability(cNode, pNode, handler);
+            adjustPropertyForShallowImmutability(cNode, pNode, handler);
             pNode.setModifiers(pNode.getModifiers() | ACC_FINAL);
         }
         final List<FieldNode> fList = cNode.getFields();
@@ -182,7 +183,10 @@ public class RecordTypeASTTransformation extends 
AbstractASTTransformation imple
             if (unsupportedTupleAttribute(tupleCons, "includeProperties")) 
return;
             if (unsupportedTupleAttribute(tupleCons, "includeSuperFields")) 
return;
             if (unsupportedTupleAttribute(tupleCons, "callSuper")) return;
-            unsupportedTupleAttribute(tupleCons, "force");
+        }
+
+        if (!pList.isEmpty() && memberHasValue(node, "copyWith", Boolean.TRUE) 
&& !hasDeclaredMethod(cNode, "copyWith", 1)) {
+            ImmutableASTTransformation.createCopyWith(cNode, pList);
         }
     }
 
@@ -305,16 +309,17 @@ public class RecordTypeASTTransformation extends 
AbstractASTTransformation imple
         }
     }
 
-    private static void adjustPropertyForImmutability(ClassNode cNode, 
PropertyNode pNode, PropertyHandler handler) {
+    // guarantee shallow immutability but property handler may do defensive 
copying
+    private static void adjustPropertyForShallowImmutability(ClassNode cNode, 
PropertyNode pNode, PropertyHandler handler) {
         final FieldNode fNode = pNode.getField();
         fNode.setModifiers((pNode.getModifiers() & (~ACC_PUBLIC)) | ACC_FINAL 
| ACC_PRIVATE);
         boolean isGetterDefined = 
cNode.getDeclaredMethods(pNode.getName()).stream()
                 .anyMatch(MethodNodeUtils::isGetterCandidate);
         if (!isGetterDefined) {
+            pNode.setGetterName(pNode.getName());
             Statement getter = handler.createPropGetter(pNode);
             if (getter != null) {
                 pNode.setGetterBlock(getter);
-                pNode.setGetterName(pNode.getName());
             }
         }
     }
diff --git a/src/spec/doc/_records.adoc b/src/spec/doc/_records.adoc
index 8d811af..235ce47 100644
--- a/src/spec/doc/_records.adoc
+++ b/src/spec/doc/_records.adoc
@@ -37,7 +37,7 @@ a record as follows:
 
 [source,groovy]
 ----
-include::../test/RecordSpecificationTest.groovy[tags=record_message_def,indent=0]
+include::../test/RecordSpecificationTest.groovy[tags=record_message_defn,indent=0]
 ----
 
 We'd use the record class in the same way as a normal class, as shown below:
@@ -126,6 +126,8 @@ However, if you wish to customise or configure the record 
implementation,
 you may wish to drop back to the `@RecordType` style or augment your record 
class
 with one of the constituent sub-annotations.
 
+=== ToString
+
 As an example, you can a three-dimensional point record as follows:
 
 [source,groovy]
@@ -146,7 +148,28 @@ 
include::../test/RecordSpecificationTest.groovy[tags=record_point2d_tostring_ann
 
 We can see here that without the package name it would have the same toString 
as our previous example.
 
-This example was somewhat contrived but in general illustrates the principal 
behind
+=== CopyWith
+
+For a record with many fields, it can be useful to make a copy with one or 
more fields changes.
+This can be enabled by setting the `copyWith` annotation attribute to `true`:
+
+[source,groovy]
+----
+include::../test/RecordSpecificationTest.groovy[tags=record_copywith,indent=0]
+----
+
+=== Deep immutability
+
+As with Java, records by default offer shallow immutability.
+Groovy's `@Immutable` transform performs defensive copying for a range of 
mutable
+data types. Records can make use of this defensive copying to gain deep 
immutability as follows:
+
+[source,groovy]
+----
+include::../test/RecordSpecificationTest.groovy[tags=record_immutable,indent=0]
+----
+
+These examples illustrates the principal behind
 Groovy's record feature offering three levels of convenience:
 
 * Using the `record` keyword for maximum succinctness
@@ -155,7 +178,7 @@ Groovy's record feature offering three levels of 
convenience:
 
 == Other differences to Java
 
-Groovy supports creating record-like classes as well as native records.
+Groovy supports creating _record-like_ classes as well as native records.
 Record-like classes don't extend Java's `Record` class and such classes
 won't be seen by Java as records but will otherwise have similar properties.
 
diff --git a/src/spec/test/RecordSpecificationTest.groovy 
b/src/spec/test/RecordSpecificationTest.groovy
index 85c8552..13ff0ae 100644
--- a/src/spec/test/RecordSpecificationTest.groovy
+++ b/src/spec/test/RecordSpecificationTest.groovy
@@ -96,6 +96,37 @@ assert new Point3D(10, 20, 30).toString() == 
'Point3D[coords=10,20,30]'
 '''
     }
 
+    void testCopyWith() {
+        assertScript '''
+import groovy.transform.RecordBase
+// tag::record_copywith[]
+@RecordBase(copyWith=true)
+record Fruit(String name, double price) {}
+def apple = new Fruit('Apple', 11.6)
+assert 'Apple' == apple.name()
+assert 11.6 == apple.price()
+
+def orange = apple.copyWith(name: 'Orange')
+assert orange.toString() == 'Fruit[name=Orange, price=11.6]'
+// end::record_copywith[]
+'''
+    }
+
+    void testImmutable() {
+        assertScript '''
+import groovy.transform.ImmutableProperties
+// tag::record_immutable[]
+@ImmutableProperties
+record Shopping(List items) {}
+
+def items = ['bread', 'milk']
+def shop = new Shopping(items)
+items << 'chocolate'
+assert shop.items() == ['bread', 'milk']
+// end::record_immutable[]
+'''
+    }
+
     void testRecordCompactConstructor() {
         assertScript '''
 // tag::record_compact_constructor[]

Reply via email to