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!]'
+'''
+ }
+}