Implementing Actions
Actions are stateless functions that can be used to implement different uses cases, such as:
-
a pure function
-
request conversion - you can use Actions to convert incoming data into a different format before forwarding a call to a different component.
-
publish and subscribe to events
-
schedule and cancel timers
Actions can be triggered in multiple ways. For example, by:
-
a gRPC service call
-
an HTTP service call
-
a forwarded call from another component
-
a scheduled call from a timer
-
an incoming event from within the same service or a from different service
In this first example, you will learn how to implement an Action as a pure stateless function. You will create a FibonacciAction
that takes a number and returns the
next number in the Fibonacci series.
Implementing the Action
To implement this action you need the following:
-
Extend our class from
kalix.javasdk.action.Action
. This is generic. No matter what action you want to create you always need to extend fromAction
.
-
Add the Spring annotation @RequestMapping to provide a REST endpoint for the function. Here the stateless function should be reachable via HTTP.
-
Add the Spring annotations @GetMapping and @PostMapping to provide paths for GET and POST to calculate the Fibonacci of a number. Both functions do the same thing and implementation-wise the function exposed with GET calls the function exposed with POST.
import kalix.javasdk.action.Action;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.PathVariable;
@RequestMapping("/fibonacci")
public class FibonacciAction extends Action {
private boolean isFibonacci(long num) { (1)
Predicate<Long> isPerfectSquare = (n) -> {
long square = (long) Math.sqrt(n);
return square*square == n;
};
return isPerfectSquare.test(5*num*num + 4) || isPerfectSquare.test(5*num*num - 4);
}
private long nextFib(long num) { (2)
double result = num * (1 + Math.sqrt(5)) / 2.0;
return Math.round(result);
}
@GetMapping("/{number}/next")
public Effect<Number> nextNumber(@PathVariable Long number) { (3)
return nextNumber(new Number(number));
}
@PostMapping("/next")
public Effect<Number> nextNumber(@RequestBody Number number) {
long num = number.value();
if (isFibonacci(num)) { (4)
return effects().reply(new Number(nextFib(num)));
} else {
return effects() (5)
.error("Input number is not a Fibonacci number, received '" + num + "'");
}
}
}
1 | isFibonacci checks if a number is a Fibonacci number. |
2 | nextFib calculates the next number. |
3 | This nextNumber implementation calls the nextNumber implementation below. |
4 | The nextNumber implementation first checks if the input number belongs to the Fibonacci series. If so, it calculates the next number and builds a reply using effects().reply() . |
5 | Otherwise, if the input number doesn’t belong to the Fibonacci series, it builds an Effect reply error. |
Actions return effects (i.e. Action.Effect
) and there are different types of effects: a reply, an error, a forward call to another component, and to all of those you can add side effects. Here you want only the result of the calculation or an error. Therefore you are using .reply
and .error
.
Testing the Action
Unit tests
The following snippet shows how the ActionTestkit
is used to test the FibonacciAction
implementation.
With the ActionTestkit
you can call the methods of FibonacciAction
. Each call you pass over to the test kit returns an ActionResult
that contains the effect produced by the underlying action method.
Actions are unique units of computation where no local state is shared with previous or subsequent calls. The framework does not reuse an Action instance but creates a new one for each command handled and therefore this is also how the test kit behaves.
|
- Java
-
src/test/java/com/example/actions/FibonacciActionTest.java
import kalix.javasdk.testkit.ActionResult; import kalix.javasdk.testkit.ActionTestkit; import org.junit.jupiter.api.Test; public class FibonacciActionTest { @Test public void testNextFib() { ActionTestkit<FibonacciAction> testkit = ActionTestkit.of(FibonacciAction::new); (1) ActionResult<Number> result = testkit.call(a -> a.nextNumber(3L)); (2) assertTrue(result.isReply()); assertEquals(5L, result.getReply().value()); } @Test public void testNextFibError() { ActionTestkit<FibonacciAction> testkit = ActionTestkit.of(FibonacciAction::new); (1) ActionResult<Number> result = testkit.call(a -> a.nextNumber(4L)); (2) assertTrue(result.isError()); assertTrue(result.getError().startsWith("Input number is not a Fibonacci number")); } }
1 The test kit is created to allow us to test the Action’s method. 2 Calling nextNumber
method with some value.ActionResult
Calling an action method through the test kit gives us back an
ActionResult
. This class has methods that you can use to assert your expectations, such as:
-
getReply()
returns the reply message passed toeffects().reply()
or throws an exception failing the test, if the effect returned was not a reply. -
getError()
returns the error description wheneffects().error()
was returned to signal an error. -
getForward()
returns details about what message was forwarded and where the call was forwarded (since it is a unit test the forward is not actually executed).
-
TODO: add links to before and after