checkingClass, M... values) {
for (var value : values) {
checkNotEmpty(checkingClass, value);
}
@@ -157,6 +159,20 @@ public final boolean equals(Object obj) {
return Objects.equals(this.values, other.values);
}
+ /**
+ * If the passed {@code value} is an {@code Optional}, tells if its value is present.
+ *
+ * Otherwise, returns {@code true}.
+ */
+ static boolean isOptionalPresent(Object value) {
+ checkNotNull(value);
+ if(!(value instanceof Optional)) {
+ return true;
+ }
+ var asOptional = (Optional>) value;
+ return asOptional.isPresent();
+ }
+
/**
* Traverses through elements obtaining a message value from them.
*/
diff --git a/server/src/main/kotlin/io/spine/server/event/Just.kt b/server/src/main/kotlin/io/spine/server/event/Just.kt
index e8ec28f731d..5a256804b2b 100644
--- a/server/src/main/kotlin/io/spine/server/event/Just.kt
+++ b/server/src/main/kotlin/io/spine/server/event/Just.kt
@@ -1,5 +1,5 @@
/*
- * Copyright 2022, TeamDev. All rights reserved.
+ * Copyright 2023, TeamDev. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -27,12 +27,14 @@
package io.spine.server.event
import io.spine.base.EventMessage
+import io.spine.server.model.Nothing
import io.spine.server.tuple.Tuple
/**
* A tuple of one event.
*
- * Used when returning an iterable from a handler method for better readability over `List`.
+ * Used when returning an `Iterable` from a handler method for better readability over
+ * `Iterable` or `List`.
*
* @param E the type of the event.
*/
@@ -40,8 +42,16 @@ public class Just(event: E) : Tuple(event) {
public companion object {
+ @Suppress("ConstPropertyName") // Following Java conventions.
private const val serialVersionUID: Long = 0L
+ /**
+ * The instance of `Just`.
+ */
+ public val nothing: Just by lazy {
+ Just(Nothing.getDefaultInstance())
+ }
+
/**
* A factory method for Java.
*
@@ -51,5 +61,13 @@ public class Just(event: E) : Tuple(event) {
*/
@JvmStatic
public fun just(event: E): Just = Just(event)
+
+ /**
+ * Obtains the instance of `Just` for Java code.
+ *
+ * Prefer the [nothing] property of the companion object in Kotlin.
+ */
+ @JvmStatic
+ public fun nothing(): Just = nothing
}
}
diff --git a/server/src/main/kotlin/io/spine/server/event/Policy.kt b/server/src/main/kotlin/io/spine/server/event/Policy.kt
index 23c28b29318..91fdd395b36 100644
--- a/server/src/main/kotlin/io/spine/server/event/Policy.kt
+++ b/server/src/main/kotlin/io/spine/server/event/Policy.kt
@@ -1,5 +1,5 @@
/*
- * Copyright 2022, TeamDev. All rights reserved.
+ * Copyright 2023, TeamDev. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -27,6 +27,7 @@
package io.spine.server.event
import com.google.common.collect.ImmutableSet
+import com.google.protobuf.Message
import io.spine.base.EventMessage
import io.spine.core.ContractFor
import io.spine.logging.WithLogging
@@ -34,35 +35,99 @@ import io.spine.server.BoundedContext
import io.spine.server.type.EventClass
/**
- * A policy converts one event into zero to many other events.
+ * A policy converts one event into zero to many other events.
*
* As a rule of thumb, a policy should read:
- * Whenever , then .
- *
- * For example:
- * Whenever a field option is discovered, a validation rule must be added.
+ * ```markdown
+ * Whenever , then .
+ * ```
+ * For example,
+ * ```markdown
+ * Whenever a field option is discovered, a validation rule must be added.
+ * ```
+ * To implement the policy, override the [whenever] method to return events produced in response
+ * to the incoming event.
*
- * To implement the policy, declare a method which reacts to an event with an event:
+ * For the policy rule in the example above, the code would look like this:
* ```kotlin
- * class MyPolicy : Policy() {
+ * class ValidationRulePolicy : Policy() {
*
* @React
- * override fun whenever(event: ProjectCreated): Just {
+ * override fun whenever(event: FieldOptionDiscovered): Just {
* // Produce the event.
* }
* }
* ```
- * @param E the type of the event handled by this policy
+ * ### Returning zero events
+ * The contract of the [whenever] method requires returning an `Iterable` of event messages.
+ * To return no events, declare the return type as `Just`, where `Nothing` is
+ * the type from the `io.spine.server.model` package. Return the value of `Just.nothing` property
+ * from your Kotlin method, or `Just.nothing()` from Java.
+ *
+ * If you need to avoid the naming collision with [kotlin.Nothing], consider using
+ * type aliases [NothingHappened][io.spine.server.model.NothingHappened] or
+ * [NoReaction][io.spine.server.model.NoReaction].
+ *
+ * ### Returning one event
+ * To return one event, declare `Just` as the return type of the [whenever] method.
+ * Use the [Just] constructor from Kotlin or [Just.just]`()` static method from Java.
+ *
+ * ### Returning more than one event
+ * To make your return type more readable, consider using the following classes from
+ * the `io.spine.server.tuple` package:
+ * [Pair][io.spine.server.tuple.Pair],
+ * [Triplet][io.spine.server.tuple.Triplet], [Quartet][io.spine.server.tuple.Quartet],
+ * [Quintet][io.spine.server.tuple.Quintet], with the corresponding number of elements declared
+ * in the return type of the [whenever] method. For example, `Pair`.
+ *
+ * For returning more than five events, please use `Iterable`, as usually.
+ *
+ * @param E the type of the event handled by this policy.
+ *
+ * @see Just
+ * @see [io.spine.server.tuple.Pair]
+ * @see [io.spine.server.tuple.Triplet]
+ * @see [io.spine.server.tuple.Quartet]
+ * @see [io.spine.server.tuple.Quintet]
+ * @see [io.spine.server.model.Nothing]
+ * @see [io.spine.server.model.NothingHappened]
+ * @see [io.spine.server.model.NoReaction]
*/
public abstract class Policy : AbstractEventReactor(), WithLogging {
protected lateinit var context: BoundedContext
+ init {
+ // This call would check that there is only one event receptor
+ // defined in the derived class.
+ // Doing it earlier, here, in the constructor without waiting until
+ // the dispatching schema is built (thus gathering the message classes),
+ // allows failing faster and avoiding delayed debugging.
+ messageClasses()
+ }
+
/**
* Handles an event and produces some number of events in response.
+ *
+ * ### API NOTE
+ *
+ * This method returns `Iterable` instead of `Iterable`,
+ * to allow implementing classes declare the return types using classes descending from
+ * [Either][io.spine.server.tuple.Either]. For example, `EitherOf2`.
+ *
+ * `Either` implements `Iterable`. Classes extending `Either` have two or
+ * more generic parameters bounded by `Message`, not `EventMessage`.
+ * Therefore, these classes will not be accepted as return types of
+ * the overridden methods because `Iterable` will not be
+ * a super type for them.
+ *
+ * Policy authors should declare return types of the overridden methods as described
+ * in the [class documentation][Policy].
+ *
+ * @see Policy
*/
@ContractFor(handler = React::class)
- protected abstract fun whenever(event: E): Iterable
+ protected abstract fun whenever(event: E): Iterable
final override fun registerWith(context: BoundedContext) {
super.registerWith(context)
diff --git a/server/src/main/java/io/spine/server/tuple/Values.java b/server/src/main/kotlin/io/spine/server/model/EventExts.kt
similarity index 61%
rename from server/src/main/java/io/spine/server/tuple/Values.java
rename to server/src/main/kotlin/io/spine/server/model/EventExts.kt
index 22150d43ea3..c81668eea02 100644
--- a/server/src/main/java/io/spine/server/tuple/Values.java
+++ b/server/src/main/kotlin/io/spine/server/model/EventExts.kt
@@ -1,5 +1,5 @@
/*
- * Copyright 2022, TeamDev. All rights reserved.
+ * Copyright 2023, TeamDev. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -24,34 +24,17 @@
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
-package io.spine.server.tuple;
-
-import java.util.Optional;
-
-import static com.google.common.base.Preconditions.checkNotNull;
+package io.spine.server.model
/**
- * A utility for working with {@link Tuple} values.
+ * The alias for cases of naming collisions between
+ * [io.spine.server.model.Nothing] and [kotlin.Nothing].
*/
-final class Values {
+public typealias NothingHappened = Nothing
- /**
- * Prevents this utility class from instantiation.
- */
- private Values() {
- }
+/**
+ * The alias for cases of naming collisions between
+ * [io.spine.server.model.Nothing] and [kotlin.Nothing].
+ */
+public typealias NoReaction = Nothing
- /**
- * If the passed {@code value} is an {@code Optional}, tells if its value is present.
- *
- * Otherwise, returns {@code true}.
- */
- static boolean isOptionalPresent(Object value) {
- checkNotNull(value);
- if(!(value instanceof Optional)) {
- return true;
- }
- var asOptional = (Optional>) value;
- return asOptional.isPresent();
- }
-}
diff --git a/server/src/test/java/io/spine/server/tuple/TupleTest.java b/server/src/test/java/io/spine/server/tuple/TupleTest.java
index 38603894128..6bf90786fcd 100644
--- a/server/src/test/java/io/spine/server/tuple/TupleTest.java
+++ b/server/src/test/java/io/spine/server/tuple/TupleTest.java
@@ -39,9 +39,6 @@
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
-import java.util.Iterator;
-
-import static io.spine.server.tuple.Values.isOptionalPresent;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertThrows;
diff --git a/server/src/test/kotlin/io/spine/server/event/PolicySpec.kt b/server/src/test/kotlin/io/spine/server/event/PolicySpec.kt
new file mode 100644
index 00000000000..2a639095c31
--- /dev/null
+++ b/server/src/test/kotlin/io/spine/server/event/PolicySpec.kt
@@ -0,0 +1,90 @@
+/*
+ * Copyright 2023, TeamDev. All rights reserved.
+ *
+ * Licensed 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
+ *
+ * Redistribution and use in source and/or binary forms, with or without
+ * modification, must retain the above copyright notice and the following
+ * disclaimer.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package io.spine.server.event
+
+import io.kotest.matchers.shouldBe
+import io.spine.core.External
+import io.spine.server.model.Nothing
+import io.spine.server.tuple.EitherOf2
+import io.spine.test.shared.event.SomethingHappened
+import org.junit.jupiter.api.DisplayName
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.assertThrows
+
+@DisplayName("`Policy` should")
+internal class PolicySpec {
+
+ @Test
+ fun `do not allow adding more react methods`() {
+ assertThrows {
+ GreedyPolicy()
+ }
+ }
+
+ @Test
+ fun `allow using 'Just' in return value`() {
+ val policy = object : Policy() {
+ @React
+ public override fun whenever(event: SomethingHappened): Just {
+ return Just.nothing
+ }
+ }
+ policy.whenever(somethingHappened) shouldBe Just.nothing
+ }
+
+ @Test
+ fun `allow using 'Either' in return value`() {
+ object : Policy() {
+ @React
+ public override fun whenever(event: SomethingHappened): EitherOf2 {
+ return EitherOf2.withA(nothing)
+ }
+ }.let {
+ it.whenever(somethingHappened) shouldBe EitherOf2.withA(nothing)
+ }
+ }
+
+ companion object {
+ val somethingHappened = SomethingHappened.getDefaultInstance()
+ val nothing = Nothing.getDefaultInstance()
+ }
+}
+
+/**
+ * The policy which attempts to define a `@React` receptor to handle more than one
+ * event type, as required by the `Policy` contract.
+ */
+private class GreedyPolicy : Policy() {
+
+ @React
+ override fun whenever(@External event: Nothing): Just =
+ Just.nothing
+
+ @React
+ fun on(@Suppress("UNUSED_PARAMETER") e: SomethingHappened): Just =
+ Just.nothing
+}
diff --git a/version.gradle.kts b/version.gradle.kts
index 3d1fb0002eb..ed1c8a3aa17 100644
--- a/version.gradle.kts
+++ b/version.gradle.kts
@@ -29,4 +29,4 @@
*
* For versions of Spine-based dependencies, please see [io.spine.internal.dependency.Spine].
*/
-val versionToPublish: String by extra("2.0.0-SNAPSHOT.170")
+val versionToPublish: String by extra("2.0.0-SNAPSHOT.171")