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 20c2e04  documentation for records (draft)
20c2e04 is described below

commit 20c2e04f0872670beee5f18eee1baf4bbc7b8608
Author: Paul King <[email protected]>
AuthorDate: Thu Oct 28 21:39:12 2021 +1000

    documentation for records (draft)
---
 src/spec/doc/_records.adoc                   | 170 +++++++++++++++++++++++++++
 src/spec/doc/core-object-orientation.adoc    |   2 +
 src/spec/test/RecordSpecificationTest.groovy | 156 ++++++++++++++++++++++++
 3 files changed, 328 insertions(+)

diff --git a/src/spec/doc/_records.adoc b/src/spec/doc/_records.adoc
new file mode 100644
index 0000000..c996a92
--- /dev/null
+++ b/src/spec/doc/_records.adoc
@@ -0,0 +1,170 @@
+//////////////////////////////////////////
+
+  Licensed to the Apache Software Foundation (ASF) under one
+  or more contributor license agreements.  See the NOTICE file
+  distributed with this work for additional information
+  regarding copyright ownership.  The ASF licenses this file
+  to you under the Apache License, Version 2.0 (the
+  "License"); you may not use this file except in compliance
+  with the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing,
+  software distributed under the License is distributed on an
+  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+  KIND, either express or implied.  See the License for the
+  specific language governing permissions and limitations
+  under the License.
+
+//////////////////////////////////////////
+
+= Record classes (incubating)
+
+Record classes, or _records_ for short, are a special kind of class
+useful for modelling plain data aggregates.
+They provide a compact syntax with less ceremony than normal classes.
+Groovy already has AST transforms such as `@Immutable` and `@Canonical`
+which already dramatically reduce ceremony but records have been
+introduced in Java and record classes in Groovy are designed to align
+with Java record classes.
+
+For example, suppose we want to create a `Message` record
+representing an email message. For the purposes of this example,
+let's simplify such a message to contain just a _from_ email address,
+a _to_ email address, and a message _body_. We would define such
+a record as follows:
+
+[source,groovy]
+----
+include::../test/RecordSpecificationTest.groovy[tags=record_message_def,indent=0]
+----
+
+We'd use the record class in the same way as a normal class, as shown below:
+
+[source,groovy]
+----
+include::../test/RecordSpecificationTest.groovy[tags=record_message_usage,indent=0]
+----
+
+The reduced ceremony saves us from defining explicit fields, getters and
+`toString`, `equals` and `hashCode` methods. In fact, it's a shorthand
+for the following rough equivalent:
+
+[source,groovy]
+----
+include::../test/RecordSpecificationTest.groovy[tags=record_message_equivalent,indent=0]
+----
+
+Like in Java, you can override the normally implicitly supplied methods
+by writing your own:
+
+[source,groovy]
+----
+include::../test/RecordSpecificationTest.groovy[tags=record_point3d,indent=0]
+----
+
+== Compact constructor
+
+Records have an implicit constructor. This can be overridden in the normal way
+by providing your own constructor - you need to make sure you set all of the 
fields
+if you do this.
+However, for succinctness, a compact constructor syntax can be used where
+the parameter declaration part of a normal constructor is elided.
+For this special case, the normal implicit constructor is still provided
+but is augmented by the supplied statements in the compact constructor 
definition:
+
+[source,groovy]
+----
+include::../test/RecordSpecificationTest.groovy[tags=record_compact_constructor,indent=0]
+----
+
+== Groovy enhancements
+
+Groovy supports default values for constructor arguments.
+This capability is also available for records as shown in the following record 
definition
+which has default values for `y` and `color`:
+
+[source,groovy]
+----
+include::../test/RecordSpecificationTest.groovy[tags=record_point_defn,indent=0]
+----
+
+Arguments when left off (dropping one or more arguments from the right) are 
replaced
+with their defaults values as shown in the following example:
+
+[source,groovy]
+----
+include::../test/RecordSpecificationTest.groovy[tags=record_point_defaults,indent=0]
+----
+
+Named arguments may also be used (default values also apply here):
+
+[source,groovy]
+----
+include::../test/RecordSpecificationTest.groovy[tags=record_point_named_args,indent=0]
+----
+
+== Diving deeper
+
+We previously described a `Message` record and displayed it's rough equivalent.
+Groovy in fact steps through an intermediate stage where the `record` keyword
+is replaced by the `class` keyword and an accompanying `@RecordType` 
annotation:
+
+[source,groovy]
+----
+include::../test/RecordSpecificationTest.groovy[tags=record_message_annotation_defn,indent=0]
+----
+
+Then `@RecordType` itself is processed as a _meta-annotation_ (annotation 
collector)
+and expanded into its constituent sub-annotations such as `@TupleConstructor`, 
`@POJO`,
+`@RecordBase`, and others. This is in some sense an implementation detail 
which can often be ignored.
+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.
+
+As an example, you can a three-dimensional point record as follows:
+
+[source,groovy]
+----
+include::../test/RecordSpecificationTest.groovy[tags=record_point3d_tostring_annotation,indent=0]
+----
+
+We customise the `toString` by including the package name (excluded by default 
for records)
+and by caching the `toString` value since it won't change for this immutable 
record.
+We are also ignoring null values (the default value for `z` in our definition).
+
+We can have a similar definition for a two-dimensional point:
+
+[source,groovy]
+----
+include::../test/RecordSpecificationTest.groovy[tags=record_point2d_tostring_annotation,indent=0]
+----
+
+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
+Groovy's record feature offering three levels of convenience:
+
+* Using the `record` keyword for maximum succinctness
+* Support low-ceremony customization using declarative annotations
+* Provide normal method implementations when full control is required
+
+== Other differences to Java
+
+Groovy supports creating record-like classes as well as native records.
+Record-like classes don't extend Java's `Record` class and won't be seen
+by Java as records but will otherwise have similar properties.
+
+The `@RecordBase` annotation (part of `@RecordType`) supports a `mode` 
annotation attribute
+which can take one of three values (with `AUTO` being the default):
+
+NATIVE::
+Produces a class similar to what Java would do. Produces an error when 
compiling on JDKs earlier than JDK16.
+EMULATE::
+Produces a record-like class.
+AUTO::
+Produces a native record for JDK16+ and emulates the record otherwise.
+
+Whether you use the `record` keyword or the `@RecordType` annotation
+is independent of the mode.
diff --git a/src/spec/doc/core-object-orientation.adoc 
b/src/spec/doc/core-object-orientation.adoc
index dbc214a..337bef0 100644
--- a/src/spec/doc/core-object-orientation.adoc
+++ b/src/spec/doc/core-object-orientation.adoc
@@ -1235,4 +1235,6 @@ single one corresponding to 
`@CompileStatic(TypeCheckingMode.SKIP)`.
 
 include::_traits.adoc[leveloffset=+1]
 
+include::_records.adoc[leveloffset=+1]
+
 include::_sealed.adoc[leveloffset=+1]
diff --git a/src/spec/test/RecordSpecificationTest.groovy 
b/src/spec/test/RecordSpecificationTest.groovy
new file mode 100644
index 0000000..6fbb8fb
--- /dev/null
+++ b/src/spec/test/RecordSpecificationTest.groovy
@@ -0,0 +1,156 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+
+import groovy.test.GroovyTestCase
+
+/**
+ * Specification tests for records
+ */
+class RecordSpecificationTest extends GroovyTestCase {
+
+    void testSimpleRecordKeyword() {
+        assertScript '''
+// tag::record_message_defn[]
+record Message(String from, String to, String body) { }
+// end::record_message_defn[]
+// tag::record_message_usage[]
+def msg = new Message('[email protected]', '[email protected]', 'Hello!')
+assert msg.toString() == 'Message[[email protected], [email protected], 
body=Hello!]'
+// end::record_message_usage[]
+'''
+        def equiv = '''
+// tag::record_message_equivalent[]
+final class Message extends Record {
+    private final String from
+    private final String to
+    private final String body
+    private static final long serialVersionUID = 0
+
+    /* constructor(s) */
+
+    final String toString() { /*...*/ }
+
+    final boolean equals(Object other) { /*...*/ }
+
+    final int hashCode() { /*...*/ }
+}
+// end::record_message_equivalent[]
+'''
+    }
+
+    void testRecordDefaultsAndNamedArguments() {
+        assertScript '''
+import static groovy.test.GroovyAssert.shouldFail
+import groovy.transform.*
+
+// tag::record_point_defn[]
+record ColoredPoint(int x, int y = 0, String color = 'white') {}
+// end::record_point_defn[]
+
+// tag::record_point_defaults[]
+assert new ColoredPoint(5, 5, 'black').toString() == 'ColoredPoint[x=5, y=5, 
color=black]'
+assert new ColoredPoint(5, 5).toString() == 'ColoredPoint[x=5, y=5, 
color=white]'
+assert new ColoredPoint(5).toString() == 'ColoredPoint[x=5, y=0, color=white]'
+// end::record_point_defaults[]
+
+// tag::record_point_named_args[]
+assert new ColoredPoint(x: 5).toString() == 'ColoredPoint[x=5, y=0, 
color=white]'
+assert new ColoredPoint(y: 5).toString() == 'ColoredPoint[x=0, y=5, 
color=white]'
+// end::record_point_named_args[]
+assert new ColoredPoint(y: null).toString() == 'ColoredPoint[x=0, y=0, 
color=white]'
+def ex = shouldFail { new ColoredPoint(z: 5) }
+assert ex.message.contains('Unrecognized namedArgKey: z')
+'''
+    }
+
+    void testOverrideToString() {
+        assertScript '''
+// tag::record_point3d[]
+record Point3D(int x, int y, int z) {
+    String toString() {
+        "Point3D[coords=$x,$y,$z]"
+    }
+}
+
+assert new Point3D(10, 20, 30).toString() == 'Point3D[coords=10,20,30]'
+// end::record_point3d[]
+'''
+    }
+
+    void testRecordCompactConstructor() {
+        assertScript '''
+// tag::record_compact_constructor[]
+public record Warning(String message) {
+    public Warning {
+        Objects.requireNonNull(message)
+        message = message.toUpperCase()
+    }
+}
+
+def w = new Warning('Help')
+assert w.message() == 'HELP'
+// end::record_compact_constructor[]
+'''
+    }
+
+    void testToStringAnnotation() {
+        assertScript '''
+// tag::record_point3d_tostring_annotation[]
+package threed
+
+import groovy.transform.ToString
+
+@ToString(ignoreNulls=true, cache=true, includeNames=true,
+          leftDelimiter='[', rightDelimiter=']', nameValueSeparator='=')
+record Point(Integer x, Integer y, Integer z=null) { }
+
+assert new Point(10, 20).toString() == 'threed.Point[x=10, y=20]'
+// end::record_point3d_tostring_annotation[]
+'''
+        assertScript '''
+// tag::record_point2d_tostring_annotation[]
+package twod
+
+import groovy.transform.ToString
+
+@ToString(ignoreNulls=true, cache=true, includeNames=true,
+          leftDelimiter='[', rightDelimiter=']', nameValueSeparator='=')
+record Point(Integer x, Integer y) { }
+
+assert new Point(10, 20).toString() == 'twod.Point[x=10, y=20]'
+// end::record_point2d_tostring_annotation[]
+'''
+    }
+
+    void testSimpleRecordAnnotation() {
+        assertScript '''
+import groovy.transform.RecordType
+// tag::record_message_annotation_defn[]
+@RecordType
+class Message {
+    String from
+    String to
+    String body
+}
+// end::record_message_annotation_defn[]
+def msg = new Message('[email protected]', '[email protected]', 'Hello!')
+assert msg.toString() == 'Message[[email protected], [email protected], 
body=Hello!]'
+'''
+    }
+}

Reply via email to