diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml index 5a399bd1a89..e5ff744791c 100644 --- a/.idea/codeStyles/Project.xml +++ b/.idea/codeStyles/Project.xml @@ -95,5 +95,17 @@ + + + + + + + + diff --git a/server/src/main/java/io/spine/server/aggregate/AggregateEndpoint.java b/server/src/main/java/io/spine/server/aggregate/AggregateEndpoint.java index 5098fa7e210..37fa23c52ef 100644 --- a/server/src/main/java/io/spine/server/aggregate/AggregateEndpoint.java +++ b/server/src/main/java/io/spine/server/aggregate/AggregateEndpoint.java @@ -90,7 +90,7 @@ public final void dispatchTo(I aggregateId) { private void storeAndPost(A aggregate, DispatchOutcome outcome) { Success success = outcome.getSuccess(); - if (success.hasProducedEvents()) { + if (success.hasEvents()) { store(aggregate); List events = success.getProducedEvents() .getEventList(); @@ -126,7 +126,7 @@ private A loadOrCreate(I aggregateId) { final DispatchOutcome handleAndApplyEvents(A aggregate) { DispatchOutcome outcome = invokeDispatcher(aggregate); Success successfulOutcome = outcome.getSuccess(); - return successfulOutcome.hasProducedEvents() + return successfulOutcome.hasEvents() ? applyProducedEvents(aggregate, outcome) : outcome; } diff --git a/server/src/main/java/io/spine/server/dispatch/SuccessMixin.java b/server/src/main/java/io/spine/server/dispatch/SuccessMixin.java index e022549fa10..268d2797875 100644 --- a/server/src/main/java/io/spine/server/dispatch/SuccessMixin.java +++ b/server/src/main/java/io/spine/server/dispatch/SuccessMixin.java @@ -49,4 +49,24 @@ default Object readValue(FieldDescriptor field) { return getField(field); } } + + /** + * Determines if the outcome has any produced events. + * + * @implNote Prefer using this method over the generated {@code hasProducedEvents} + * while the latter only checks if the message is set. + */ + default boolean hasEvents() { + return hasProducedEvents() && getProducedEvents().getEventCount() > 0; + } + + /** + * Determines if the outcome has any produced commands. + * + * @implNote Prefer using this method over the generated {@code hasProducedCommands} + * while the latter only checks if the message is set. + */ + default boolean hasCommands() { + return hasProducedCommands() && getProducedCommands().getCommandCount() > 0; + } } diff --git a/server/src/test/java/io/spine/server/aggregate/AggregateTest.java b/server/src/test/java/io/spine/server/aggregate/AggregateTest.java index fd2a40c762c..5410dc72f95 100644 --- a/server/src/test/java/io/spine/server/aggregate/AggregateTest.java +++ b/server/src/test/java/io/spine/server/aggregate/AggregateTest.java @@ -37,6 +37,7 @@ import io.spine.core.Ack; import io.spine.core.Command; import io.spine.core.Event; +import io.spine.core.EventContext; import io.spine.core.MessageId; import io.spine.core.TenantId; import io.spine.server.BoundedContext; @@ -50,6 +51,11 @@ import io.spine.server.aggregate.given.aggregate.TaskAggregateRepository; import io.spine.server.aggregate.given.aggregate.TestAggregate; import io.spine.server.aggregate.given.aggregate.TestAggregateRepository; +import io.spine.server.aggregate.given.thermometer.SafeThermometer; +import io.spine.server.aggregate.given.thermometer.SafeThermometerRepo; +import io.spine.server.aggregate.given.thermometer.Thermometer; +import io.spine.server.aggregate.given.thermometer.ThermometerId; +import io.spine.server.aggregate.given.thermometer.event.TemperatureChanged; import io.spine.server.commandbus.CommandBus; import io.spine.server.delivery.MessageEndpoint; import io.spine.server.dispatch.BatchDispatchOutcome; @@ -81,7 +87,7 @@ import io.spine.test.aggregate.rejection.Rejections.AggCannotReassignUnassignedTask; import io.spine.testing.logging.MuteLogging; import io.spine.testing.server.EventSubject; -import io.spine.testing.server.blackbox.BlackBoxContext; +import io.spine.testing.server.blackbox.ContextAwareTest; import io.spine.testing.server.model.ModelTests; import io.spine.time.testing.TimeTests; import org.junit.jupiter.api.AfterEach; @@ -317,12 +323,13 @@ void writeVersionIntoEventContext() { dispatchCommand(aggregate, command(createProject)); // Get the first event since the command handler produces only one event message. - Aggregate agg = this.aggregate; - List uncommittedEvents = agg.getUncommittedEvents().list(); + Aggregate agg = aggregate; + List uncommittedEvents = agg.getUncommittedEvents() + .list(); Event event = uncommittedEvents.get(0); - - assertEquals(this.aggregate.version(), event.context() - .getVersion()); + EventContext context = event.context(); + assertThat(aggregate.version()) + .isEqualTo(context.getVersion()); } @Test @@ -719,7 +726,8 @@ void throughNewestEventsFirst() { private ProtoSubject assertNextCommandId() { Event event = history.next(); - return assertThat(event.rootMessage().asCommandId()); + return assertThat(event.rootMessage() + .asCommandId()); } @Test @@ -818,27 +826,20 @@ void checkEventsUponHistory() { private static void dispatch(TenantId tenant, Supplier> endpoint) { with(tenant).run( - () -> endpoint.get().dispatchTo(ID) + () -> endpoint.get() + .dispatchTo(ID) ); } @Nested @DisplayName("create a single event when emitting a pair without second value") - class CreateSingleEventForPair { - - private BlackBoxContext context; - - @BeforeEach - void prepareContext() { - context = BlackBoxContext.from( - BoundedContextBuilder.assumingTests() - .add(new TaskAggregateRepository()) - ); - } + class CreateSingleEventForPair extends ContextAwareTest { - @AfterEach - void closeContext() { - context.close(); + @Override + protected BoundedContextBuilder contextBuilder() { + return BoundedContextBuilder + .assumingTests() + .add(new TaskAggregateRepository()); } /** @@ -852,10 +853,10 @@ void closeContext() { @Test @DisplayName("when dispatching a command") void fromCommandDispatch() { - context.receivesCommand(createTask()) - .assertEvents() - .withType(AggTaskCreated.class) - .isNotEmpty(); + context().receivesCommand(createTask()) + .assertEvents() + .withType(AggTaskCreated.class) + .isNotEmpty(); } /** @@ -870,8 +871,8 @@ void fromCommandDispatch() { @Test @DisplayName("when reacting on an event") void fromEventReact() { - EventSubject assertEvents = context.receivesCommand(assignTask()) - .assertEvents(); + EventSubject assertEvents = context().receivesCommand(assignTask()) + .assertEvents(); assertEvents.hasSize(2); assertEvents.withType(AggTaskAssigned.class) .hasSize(1); @@ -891,8 +892,8 @@ void fromEventReact() { @Test @DisplayName("when reacting on a rejection") void fromRejectionReact() { - EventSubject assertEvents = context.receivesCommand(reassignTask()) - .assertEvents(); + EventSubject assertEvents = context().receivesCommand(reassignTask()) + .assertEvents(); assertEvents.hasSize(2); assertEvents.withType(AggCannotReassignUnassignedTask.class) .hasSize(1); @@ -900,4 +901,46 @@ void fromRejectionReact() { .hasSize(1); } } + + @Nested + @DisplayName("allow having validation on the aggregate state and") + class AllowValidatedAggregates extends ContextAwareTest { + + private final ThermometerId thermometer = ThermometerId.generate(); + + @Override + protected BoundedContextBuilder contextBuilder() { + return BoundedContextBuilder + .assumingTests() + .add(new SafeThermometerRepo(thermometer)); + } + + @Test + @DisplayName("not change the Aggregate state when there is no reaction on the event") + void notChangeStateIfNoReaction() { + TemperatureChanged booksOnFire = + TemperatureChanged.newBuilder() + .setFahrenheit(451) + .vBuild(); + context().receivesExternalEvent(booksOnFire) + .assertEntity(thermometer, SafeThermometer.class) + .doesNotExist(); + } + + @Test + @DisplayName("save valid aggregate state on change") + void safelySaveValidState() { + TemperatureChanged gettingWarmer = + TemperatureChanged.newBuilder() + .setFahrenheit(72) + .vBuild(); + context().receivesExternalEvent(gettingWarmer); + Thermometer expected = Thermometer + .newBuilder() + .setId(thermometer) + .setFahrenheit(72) + .vBuild(); + context().assertState(thermometer, expected); + } + } } diff --git a/server/src/test/java/io/spine/server/aggregate/given/thermometer/SafeThermometer.java b/server/src/test/java/io/spine/server/aggregate/given/thermometer/SafeThermometer.java new file mode 100644 index 00000000000..dadf4fee9b6 --- /dev/null +++ b/server/src/test/java/io/spine/server/aggregate/given/thermometer/SafeThermometer.java @@ -0,0 +1,75 @@ +/* + * Copyright 2021, 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.aggregate.given.thermometer; + +import io.spine.core.External; +import io.spine.server.aggregate.Aggregate; +import io.spine.server.aggregate.Apply; +import io.spine.server.aggregate.given.thermometer.event.TemperatureChanged; +import io.spine.server.aggregate.given.thermometer.event.TermTemperatureChanged; +import io.spine.server.event.React; + +import java.util.Optional; + +/** + * Ignores temperature changes outside its own range. + */ +public final class SafeThermometer extends Aggregate { + + private static final double MIN = 0; + private static final double MAX = 120; + + @React + Optional on(@External TemperatureChanged e) { + double temperature = e.getFahrenheit(); + if (!withinBounds(temperature)) { + return Optional.empty(); + } + return Optional.of( + TermTemperatureChanged + .newBuilder() + .setThermometer(id()) + .setChange( + TemperatureChange + .newBuilder() + .setNewValue(temperature) + .setPreviousValue(state().getFahrenheit()) + ) + .vBuild() + ); + } + + private static boolean withinBounds(double temperature) { + return temperature > MIN && temperature < MAX; + } + + @Apply + private void on(TermTemperatureChanged e) { + builder().setFahrenheit(e.getChange() + .getNewValue()); + } +} diff --git a/server/src/test/java/io/spine/server/aggregate/given/thermometer/SafeThermometerRepo.java b/server/src/test/java/io/spine/server/aggregate/given/thermometer/SafeThermometerRepo.java new file mode 100644 index 00000000000..ef48add5221 --- /dev/null +++ b/server/src/test/java/io/spine/server/aggregate/given/thermometer/SafeThermometerRepo.java @@ -0,0 +1,53 @@ +/* + * Copyright 2021, 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.aggregate.given.thermometer; + +import io.spine.server.aggregate.AggregateRepository; +import io.spine.server.aggregate.given.thermometer.event.TemperatureChanged; +import io.spine.server.route.EventRouting; + +import static io.spine.util.Preconditions2.checkNotDefaultArg; + +/** + * A {@link SafeThermometer thermometer} repository. + */ +public final class SafeThermometerRepo extends AggregateRepository { + + private final ThermometerId thermometer; + + /** + * Creates a new repository for the {@code thermometer}. + */ + public SafeThermometerRepo(ThermometerId thermometer) { + this.thermometer = checkNotDefaultArg(thermometer); + } + + @Override + protected void setupEventRouting(EventRouting routing) { + routing.unicast(TemperatureChanged.class, (e) -> thermometer); + } +} diff --git a/server/src/test/java/io/spine/server/aggregate/given/thermometer/package-info.java b/server/src/test/java/io/spine/server/aggregate/given/thermometer/package-info.java new file mode 100644 index 00000000000..1001511db32 --- /dev/null +++ b/server/src/test/java/io/spine/server/aggregate/given/thermometer/package-info.java @@ -0,0 +1,38 @@ +/* + * Copyright 2021, 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. + */ + +/** + * Test environment classes for the {@code Aggregate} + * {@link io.spine.server.aggregate.AggregateTest.AllowValidatedAggregates AllowValidatedAggregates} + * test. + */ +@CheckReturnValue +@ParametersAreNonnullByDefault +package io.spine.server.aggregate.given.thermometer; + +import com.google.errorprone.annotations.CheckReturnValue; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/server/src/test/java/io/spine/server/command/model/CommandReactionMethodTest.java b/server/src/test/java/io/spine/server/command/model/CommandReactionMethodTest.java index 4e14ce88597..9a8744325f2 100644 --- a/server/src/test/java/io/spine/server/command/model/CommandReactionMethodTest.java +++ b/server/src/test/java/io/spine/server/command/model/CommandReactionMethodTest.java @@ -153,8 +153,7 @@ abstract static class EmptyReturn { void setUp() { target = supplier.get(); rawMethod = target.getMethod(); - method = signature.classify(rawMethod) - .get(); + method = signature.classify(rawMethod).get(); id = ProjectId.newBuilder() .setId(newUuid()) .build(); @@ -190,8 +189,8 @@ void returnEmpty() { DispatchOutcome outcome = method.invoke(target, envelope(message)); - assertThat(outcome.getSuccess().getProducedCommands().getCommandList()) - .isEmpty(); + assertThat(outcome.getSuccess().hasCommands()) + .isFalse(); } private static CmdProjectCreated createVoidEvent(ProjectId givenId) { @@ -256,9 +255,9 @@ private static void assertResult(DispatchOutcome outcome, ProjectId id) { .build(); assertTrue(outcome.hasSuccess()); Success success = outcome.getSuccess(); - assertTrue(success.hasProducedCommands()); + assertTrue(success.hasCommands()); List commands = success.getProducedCommands() - .getCommandList(); + .getCommandList(); List commandMessages = commands .stream() .map(Command::enclosedMessage) diff --git a/server/src/test/proto/spine/test/aggregate/fibonacci/commands.proto b/server/src/test/proto/spine/test/aggregate/fibonacci/commands.proto index 4b83564c95d..b72725f4e5b 100644 --- a/server/src/test/proto/spine/test/aggregate/fibonacci/commands.proto +++ b/server/src/test/proto/spine/test/aggregate/fibonacci/commands.proto @@ -22,7 +22,8 @@ * 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. - */syntax = "proto3"; + */ +syntax = "proto3"; package spine.test.aggregate; diff --git a/server/src/test/proto/spine/test/aggregate/thermometer/events.proto b/server/src/test/proto/spine/test/aggregate/thermometer/events.proto new file mode 100644 index 00000000000..56e57ce25f6 --- /dev/null +++ b/server/src/test/proto/spine/test/aggregate/thermometer/events.proto @@ -0,0 +1,53 @@ +/* + * Copyright 2021, 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. + */ +syntax = "proto3"; + +package spine.test.aggregate; + +import "spine/options.proto"; + +option (type_url_prefix) = "type.spine.io"; +option java_package = "io.spine.server.aggregate.given.thermometer.event"; +option java_multiple_files = true; + +import "spine/test/aggregate/thermometer/thermometer.proto"; + +// A notion of the temperature change. +message TemperatureChanged { + + // The temperature in ℉. + double fahrenheit = 2; +} + +// A temperature on a particular thermometer has changed. +message TermTemperatureChanged { + + // The ID of the thermometer on which the temperature has changed. + ThermometerId thermometer = 1 [(required) = true]; + + // The temperature change on the thermometer. + TemperatureChange change = 2 [(required) = true]; +} diff --git a/server/src/test/proto/spine/test/aggregate/thermometer/thermometer.proto b/server/src/test/proto/spine/test/aggregate/thermometer/thermometer.proto new file mode 100644 index 00000000000..3d1f39e7030 --- /dev/null +++ b/server/src/test/proto/spine/test/aggregate/thermometer/thermometer.proto @@ -0,0 +1,40 @@ +syntax = "proto3"; + +package spine.test.aggregate; + +import "spine/options.proto"; + +option (type_url_prefix) = "type.spine.io"; +option java_package = "io.spine.server.aggregate.given.thermometer"; +option java_multiple_files = true; + + +// The unique factory-provided identifier of a thermometer. +message ThermometerId { + string uuid = 1 [(required) = true]; +} + +// A US thermometer for mild-weather regions. +// +// This particular thermometer type is created for the mild-weather conditions and is not gonna +// work in cold parts of the country while not being able to determine the cold temperature +// under 0 ℉. +// +message Thermometer { + option (entity).kind = AGGREGATE; + + ThermometerId id = 1 [(required) = true]; + + // The temperature in ℉. + double fahrenheit = 2 [(min).value = "0.1", (max).value = "120"]; +} + +// A change in the temperature. +message TemperatureChange { + + // The previous temperature in ℉. + double previous_value = 1; + + // The previous temperature in ℉. + double new_value = 2; +} diff --git a/version.gradle.kts b/version.gradle.kts index 18c1112df90..7fab7d0310f 100644 --- a/version.gradle.kts +++ b/version.gradle.kts @@ -34,7 +34,7 @@ /** * Version of this library. */ -val coreJava = "1.7.1" +val coreJava = "1.7.4" /** * Versions of the Spine libraries that `core-java` depends on.