Skip to content

Commit

Permalink
feat: add state interactions
Browse files Browse the repository at this point in the history
  • Loading branch information
Citymonstret committed Jan 26, 2024
1 parent 2a457cc commit 25f3fd6
Show file tree
Hide file tree
Showing 14 changed files with 403 additions and 43 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@ indra {
checkstyle().set(libs.versions.checkstyle)

javaVersions {
minimumToolchain(17)
target(17)
testWith(17)
testWith().set(setOf(17))
}
}

Expand Down
2 changes: 1 addition & 1 deletion state-core/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
plugins {
id("state.base-conventions")
// id("state.publishing-conventions")
id("state.publishing-conventions")
}

dependencies {
Expand Down
24 changes: 14 additions & 10 deletions state-core/src/main/java/org/incendo/state/AbstractStateful.java
Original file line number Diff line number Diff line change
Expand Up @@ -26,48 +26,52 @@
import java.util.Objects;
import org.apiguardian.api.API;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.checkerframework.common.returnsreceiver.qual.This;

/**
* A thread-safe implementation of {@link MutableStateful}.
*
* @param <S> state type
* @param <U> state type
* @param <V> self-referencing type
* @since 1.0.0
*/
@API(status = API.Status.STABLE, since = "1.0.0")
public abstract class AbstractStateful<S extends State<S>> implements MutableStateful<S> {
public abstract class AbstractStateful<U extends State<U>, V extends AbstractStateful<U, V>> implements MutableStateful<U, V> {

private S state;
private U state;

/**
* Creates a new instance.
*
* @param initialState initial state
*/
protected AbstractStateful(final @NonNull S initialState) {
protected AbstractStateful(final @NonNull U initialState) {
this.state = Objects.requireNonNull(initialState, "initialState");
}

@Override
public final synchronized @NonNull S state() {
public final synchronized @NonNull U state() {
return this.state;
}

@Override
public final synchronized void transitionTo(final @NonNull S state) throws IllegalStateTransitionException {
@SuppressWarnings("unchecked")
public final synchronized @This @NonNull V transitionTo(final @NonNull U state) throws IllegalStateTransitionException {
Objects.requireNonNull(state, "state");
if (!this.canTransitionTo(state)) {
throw new IllegalStateTransitionException(this.state, state, this);
}
this.state = state;
return (V) this;
}

@Override
public final synchronized void transition(final @NonNull S currentState, final @NonNull S newState)
public final synchronized @This @NonNull V transition(final @NonNull U currentState, final @NonNull U newState)
throws UnexpectedStateException, IllegalStateTransitionException {
Objects.requireNonNull(currentState, "currentState");
if (!this.state.equals(newState)) {
throw new UnexpectedStateException(currentState, newState, this);
if (!this.state.equals(currentState)) {
throw new UnexpectedStateException(States.of(currentState), newState, this);
}
this.transitionTo(newState);
return this.transitionTo(newState);
}
}
5 changes: 5 additions & 0 deletions state-core/src/main/java/org/incendo/state/EmptyStates.java
Original file line number Diff line number Diff line change
Expand Up @@ -40,4 +40,9 @@ public boolean contains(final @NonNull S state) {
public @NonNull States<S> withState(final @NonNull S state) {
return States.of(state);
}

@Override
public String toString() {
return "()";
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ public class IllegalStateTransitionException extends IllegalArgumentException {

private final State<?> from;
private final State<?> to;
private final MutableStateful<?> stateful;
private final MutableStateful<?, ?> stateful;

/**
* Creates a new instance.
Expand All @@ -48,7 +48,7 @@ public class IllegalStateTransitionException extends IllegalArgumentException {
public IllegalStateTransitionException(
final @NonNull State<?> from,
final @NonNull State<?> to,
final @NonNull MutableStateful<?> stateful
final @NonNull MutableStateful<?, ?> stateful
) {
super(String.format("Cannot transition from state '%s' to state '%s'", from, to));
this.from = from;
Expand Down Expand Up @@ -79,7 +79,7 @@ public IllegalStateTransitionException(
*
* @return stateful instance
*/
public @NonNull MutableStateful<?> stateful() {
public @NonNull MutableStateful<?, ?> stateful() {
return this.stateful;
}
}
34 changes: 24 additions & 10 deletions state-core/src/main/java/org/incendo/state/MutableStateful.java
Original file line number Diff line number Diff line change
Expand Up @@ -25,26 +25,28 @@

import org.apiguardian.api.API;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.checkerframework.common.returnsreceiver.qual.This;

/**
* Something that holds a {@link State} that can be mutated.
*
* <p>It is recommended to extend {@link AbstractStateful} when implementing this interface.</p>
*
* @param <S> state type
* @param <U> state type
* @param <V> self-referencing type
* @since 1.0.0
*/
@API(status = API.Status.STABLE, since = "1.0.0")
public interface MutableStateful<S extends State<S>> extends Stateful<S> {
public interface MutableStateful<U extends State<U>, V extends MutableStateful<U, V>> extends Stateful<U, V> {

/**
* Creates a new mutable stateful instance with the given {@code initialState}.
*
* @param <S> state type
* @param <U> state type
* @param initialState initial state
* @return the instance
*/
static <S extends State<S>> @NonNull MutableStateful<S> of(final @NonNull S initialState) {
static <U extends State<U>> @NonNull MutableStateful<U, ?> of(final @NonNull U initialState) {
return new StatefulImpl<>(initialState);
}

Expand All @@ -53,33 +55,45 @@ public interface MutableStateful<S extends State<S>> extends Stateful<S> {
*
* @param state new state
* @throws IllegalStateTransitionException if the state transition is not possible
* @return {@code this}
*/
void transitionTo(@NonNull S state) throws IllegalStateTransitionException;
@This @NonNull V transitionTo(@NonNull U state) throws IllegalStateTransitionException;

/**
* Attempts to transition from the given {@code currentState} to the given {@code newState}.
*
* @param currentState expected current state, the state transition will fail if the current {@link #state()} is different
* @param newState new state
* @return {@code this}
* @throws UnexpectedStateException if the actual {@link #state()} is different form the {@code currentState}
* @throws IllegalStateTransitionException if the state transition is not possible
*/
void transition(@NonNull S currentState, @NonNull S newState) throws
@This @NonNull V transition(@NonNull U currentState, @NonNull U newState) throws
UnexpectedStateException, IllegalStateTransitionException;

/**
* Creates a new interaction builder.
*
* @return the builder
*/
@SuppressWarnings("unchecked")
default StateInteraction.@NonNull Builder<U, V> interact() {
return StateInteraction.on((V) this);
}

/**
* Returns an immutable view of this instance.
*
* <p>Any updates to this instance will be reflected in the returned view.</p>
*
* @return immutable view
*/
default @NonNull Stateful<S> asImmutable() {
final Stateful<S> mutable = this;
return new Stateful<S>() {
default @NonNull Stateful<U, V> asImmutable() {
final Stateful<U, V> mutable = this;
return new Stateful<U, V>() {

@Override
public @NonNull S state() {
public @NonNull U state() {
return mutable.state();
}
};
Expand Down
2 changes: 1 addition & 1 deletion state-core/src/main/java/org/incendo/state/State.java
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
* @since 1.0.0
*/
@API(status = API.Status.STABLE, since = "1.0.0")
public interface State<S extends State<S>> extends Stateful<S> {
public interface State<S extends State<S>> extends Stateful<S, S> {

/**
* Returns the transitions that are possible <i>from</i> this state.
Expand Down
144 changes: 144 additions & 0 deletions state-core/src/main/java/org/incendo/state/StateInteraction.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
//
// MIT License
//
// Copyright (c) 2024 Incendo
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
//
package org.incendo.state;

import java.util.Objects;
import java.util.function.Function;
import java.util.function.UnaryOperator;
import org.apiguardian.api.API;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.checkerframework.common.returnsreceiver.qual.This;

@API(status = API.Status.STABLE, since = "1.0.0")
public interface StateInteraction<U extends State<U>, V extends Stateful<U, V>> {

/**
* Creates a new interaction builder.
*
* @param <U> state type
* @param <V> instance type
* @param instance instance to run the interaction on
* @return interaction builder
*/
static <U extends State<U>, V extends Stateful<U, V>> @NonNull Builder<U, V> on(final @NonNull V instance) {
return new Builder<>(instance);
}

/**
* Executes the interaction.
*
* @return the result of the interaction
*/
@NonNull V execute();

final class Builder<U extends State<U>, V extends Stateful<U, V>> implements StateInteraction<U, V> {

private final V instance;

private States<U> incomingStates;
private States<U> outgoingStates;
private States<U> shortCircuitStates;
private Function<V, V> interaction;

private Builder(final @NonNull V instance) {
this.instance = Objects.requireNonNull(instance, "instance");
this.incomingStates = States.of(instance.state());
this.outgoingStates = instance.allowedTransitions();
this.shortCircuitStates = States.empty();
this.interaction = UnaryOperator.identity();
}

/**
* Sets the allowed incoming states of the interaction.
*
* @param states incoming states
* @return {@code this}
*/
public @This @NonNull Builder<U, V> incomingStates(final @NonNull States<U> states) {
this.incomingStates = Objects.requireNonNull(states, "states");
return this;
}

/**
* Sets the allowed outgoing states of the interaction.
*
* @param states outgoing states
* @return {@code this}
*/
public @This @NonNull Builder<U, V> outgoingStates(final @NonNull States<U> states) {
this.outgoingStates = Objects.requireNonNull(states, "states");
return this;
}

/**
* Sets the short-circuit states of the interaction.
*
* <p>If the instance has a short-circuit state then bot the incoming &amp; outgoing state validation will be
* bypassed, and the incoming instance will be immediately returned.</p>
*
* @param states short-circuit states
* @return {@code this}
*/
public @This @NonNull Builder<U, V> shortCircuitStates(final @NonNull States<U> states) {
this.shortCircuitStates = Objects.requireNonNull(states, "states");
return this;
}

/**
* Sets the interaction.
*
* @param interaction interaction
* @return {@code this}
*/
public @This @NonNull Builder<U, V> interaction(final @NonNull Function<V, V> interaction) {
this.interaction = Objects.requireNonNull(interaction, "interaction");
return this;
}

/**
* Builds the interaction.
*
* @return the interaction
*/
public @NonNull StateInteraction<U, V> build() {
return new StateInteractionImpl<>(
this.instance,
this.incomingStates,
this.outgoingStates,
this.shortCircuitStates,
this.interaction
);
}

/**
* {@link #build() Builds} and {@link StateInteraction#execute() executes} the interaction.
*
* @return the result of the interaction
*/
@Override
public @NonNull V execute() {
return this.build().execute();
}
}
}
Loading

0 comments on commit 25f3fd6

Please sign in to comment.