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[]