diff --git a/project/Dependencies.scala b/project/Dependencies.scala index 0db69a19bf..c5f0f9277f 100644 --- a/project/Dependencies.scala +++ b/project/Dependencies.scala @@ -137,9 +137,12 @@ object Dependencies { val javaSdkSpring = deps ++= sdkDeps ++ springDeps ++ Seq( "net.aichler" % "jupiter-interface" % JupiterKeys.jupiterVersion.value % IntegrationTest, + "net.aichler" % "jupiter-interface" % JupiterKeys.jupiterVersion.value % Test, "org.springframework.boot" % "spring-boot-starter-test" % SpringBootVersion % IntegrationTest, junit5 % IntegrationTest, - "org.assertj" % "assertj-core" % "3.24.0" % IntegrationTest, + junit5 % Test, + "org.assertj" % "assertj-core" % "3.24.2" % IntegrationTest, + "org.assertj" % "assertj-core" % "3.24.2" % Test, "org.awaitility" % "awaitility" % "4.2.0" % IntegrationTest) val javaSdkSpringTestKit = diff --git a/sdk/java-sdk-protobuf-testkit/src/main/java/kalix/javasdk/testkit/KalixTestKit.java b/sdk/java-sdk-protobuf-testkit/src/main/java/kalix/javasdk/testkit/KalixTestKit.java index 0f4ebbf0b0..fa190260b5 100644 --- a/sdk/java-sdk-protobuf-testkit/src/main/java/kalix/javasdk/testkit/KalixTestKit.java +++ b/sdk/java-sdk-protobuf-testkit/src/main/java/kalix/javasdk/testkit/KalixTestKit.java @@ -529,4 +529,12 @@ public static int availableLocalPort() { public MessageCodec getMessageCodec() { return kalix.getMessageCodec(); } + + /** + * INTERNAL API + */ + @InternalApi + public KalixRunner getRunner() { + return runner; + } } diff --git a/sdk/java-sdk-spring/src/it/java/com/example/wiring/SpringSdkIntegrationTest.java b/sdk/java-sdk-spring/src/it/java/com/example/wiring/SpringSdkIntegrationTest.java index 3e80d13fa4..4c0fdb9071 100644 --- a/sdk/java-sdk-spring/src/it/java/com/example/wiring/SpringSdkIntegrationTest.java +++ b/sdk/java-sdk-spring/src/it/java/com/example/wiring/SpringSdkIntegrationTest.java @@ -17,16 +17,25 @@ package com.example.wiring; import com.example.Main; +import com.example.wiring.actions.echo.ActionWithMetadata; +import com.example.wiring.actions.echo.EchoAction; import com.example.wiring.actions.echo.Message; import com.example.wiring.actions.headers.ForwardHeadersAction; import com.example.wiring.eventsourcedentities.counter.Counter; +import com.example.wiring.eventsourcedentities.counter.CounterEntity; import com.example.wiring.valueentities.customer.CustomerEntity; +import com.example.wiring.valueentities.user.AssignedCounterEntity; import com.example.wiring.valueentities.user.User; +import com.example.wiring.valueentities.user.UserEntity; import com.example.wiring.valueentities.user.UserSideEffect; import com.example.wiring.views.CustomerByCreationTime; import com.example.wiring.views.UserCounter; import com.example.wiring.views.UserCounters; import com.example.wiring.views.UserWithVersion; +import com.google.protobuf.any.Any; +import kalix.javasdk.DeferredCall; +import kalix.javasdk.client.ComponentClient; +import kalix.javasdk.client.EventSourcedEntityCallBuilder; import kalix.spring.KalixConfigurationTest; import org.hamcrest.core.IsEqual; import org.hamcrest.core.IsNull; @@ -46,7 +55,9 @@ import java.time.Duration; import java.time.Instant; import java.util.List; +import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; import java.util.stream.Collectors; import static java.time.temporal.ChronoUnit.SECONDS; @@ -60,6 +71,8 @@ public class SpringSdkIntegrationTest { @Autowired private WebClient webClient; + @Autowired + private ComponentClient componentClient; private Duration timeout = Duration.of(10, SECONDS); @@ -134,30 +147,20 @@ public void verifyJavaPrimitivesAsParams() { @Test public void verifyEchoActionWiring() { - Message response = - webClient - .get() - .uri("/echo/message/abc") - .retrieve() - .bodyToMono(Message.class) - .block(timeout); + Message response = execute(componentClient.forAction() + .call(EchoAction::stringMessage) + .params("abc")); assertThat(response.text).isEqualTo("Parrot says: 'abc'"); } + @Test public void verifyEchoActionRequestParam() { - Message response = - webClient - .get() - .uri(uriBuilder -> uriBuilder - .path("/echo/message") - .queryParam("msg", "queryParam") - .build()) - .retrieve() - .bodyToMono(Message.class) - .block(timeout); + Message response = execute(componentClient.forAction() + .call(EchoAction::stringMessageFromParam) + .params("queryParam")); assertThat(response.text).isEqualTo("Parrot says: 'queryParam'"); @@ -165,20 +168,42 @@ public void verifyEchoActionRequestParam() { webClient .get() .uri("/echo/message") - .retrieve() - .toEntity(String.class) - .onErrorResume(WebClientResponseException.class, error -> { - if (error.getStatusCode().is4xxClientError()) { - return Mono.just(ResponseEntity.status(error.getStatusCode()).body(error.getResponseBodyAsString())); - } else { - return Mono.error(error); - } - }) + .retrieve() + .toEntity(String.class) + .onErrorResume(WebClientResponseException.class, error -> { + if (error.getStatusCode().is4xxClientError()) { + return Mono.just(ResponseEntity.status(error.getStatusCode()).body(error.getResponseBodyAsString())); + } else { + return Mono.error(error); + } + }) .block(timeout); assertThat(failedReq.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST); assertThat(failedReq.getBody()).contains("Required request parameter is missing: msg"); } + @Test + public void verifyEchoActionRequestParamWithForward() { + + String reqParam = "a b&c@d"; + Message response = execute(componentClient.forAction() + .call(EchoAction::stringMessageFromParamFw) + .params(reqParam)); + + assertThat(response.text).isEqualTo("Parrot says: '" + reqParam + "'"); + } + + @Test + public void verifyEchoActionRequestParamWithTypedForward() { + + String reqParam = "a b&c@d"; + Message response = execute(componentClient.forAction() + .call(EchoAction::stringMessageFromParamFwTyped) + .params(reqParam)); + + assertThat(response.text).isEqualTo("Parrot says: '" + reqParam + "'"); + } + @Test public void verifyStreamActions() { @@ -197,148 +222,98 @@ public void verifyStreamActions() { @Test public void verifyCounterEventSourceSubscription() { // GIVEN IncreaseAction is subscribed to CounterEntity events - // WHEN the CounterEntity is requested to increase 42 - webClient - .post() - .uri("/counter/hello1/increase/42") - .retrieve() - .bodyToMono(Integer.class) - .block(timeout); + // WHEN the CounterEntity is requested to increase 42\ + String entityId = "hello1"; + execute(componentClient.forEventSourcedEntity(entityId) + .call(CounterEntity::increase) + .params(42)); // THEN IncreaseAction receives the event 42 and increases the counter 1 more await() .ignoreExceptions() .atMost(10, TimeUnit.of(SECONDS)) - .until( - () -> - webClient - .get() - .uri("/counter/hello1") - .retrieve() - .bodyToMono(Integer.class) - .block(timeout), - new IsEqual(42 + 1)); + .untilAsserted(() -> { + Integer result = execute(componentClient.forEventSourcedEntity(entityId) + .call(CounterEntity::get)); + + assertThat(result).isEqualTo(43); //42 +1 + }); } @Test public void verifySideEffects() { // GIVEN IncreaseAction is subscribed to CounterEntity events // WHEN the CounterEntity is requested to increase 4422 - webClient - .post() - .uri("/counter/hello4422/increase/4422") - .retrieve() - .bodyToMono(Integer.class) - .block(timeout); + String entityId = "hello4422"; + execute(componentClient.forEventSourcedEntity(entityId) + .call(CounterEntity::increase) + .params(4422)); // THEN IncreaseAction receives the event 4422 and increases the counter 1 more await() .ignoreExceptions() .atMost(10, TimeUnit.of(SECONDS)) - .until( - () -> - webClient - .get() - .uri("/counter/hello4422") - .retrieve() - .bodyToMono(Integer.class) - .block(timeout), - new IsEqual(4422 + 1)); + .untilAsserted(() -> { + Integer result = execute(componentClient.forEventSourcedEntity(entityId) + .call(CounterEntity::get)); + + assertThat(result).isEqualTo(4423); + }); } @Test public void verifyActionIsNotSubscribedToMultiplyAndRouterIgnores() { - - webClient - .post() - .uri("counter/counterId2/increase/1") - .retrieve() - .bodyToMono(Integer.class) - .block(timeout); - - webClient - .post() - .uri("counter/counterId2/multiply/2") - .retrieve() - .bodyToMono(Integer.class) - .block(timeout); - - - Integer lastKnownValue = - webClient - .post() - .uri("counter/counterId2/increase/1234") - .retrieve() - .bodyToMono(Integer.class) - .block(timeout); + var entityId = "counterId2"; + EventSourcedEntityCallBuilder callBuilder = componentClient.forEventSourcedEntity(entityId); + execute(callBuilder.call(CounterEntity::increase).params(1)); + execute(callBuilder.call(CounterEntity::times).params(2)); + Integer lastKnownValue = execute(callBuilder.call(CounterEntity::increase).params(1234)); assertThat(lastKnownValue).isEqualTo(1 * 2 + 1234); //Once the action IncreaseActionWithIgnore processes event 1234 it adds 1 more to the counter await() - .atMost(10, TimeUnit.SECONDS) - .until( - () -> - webClient - .get() - .uri("/counter/counterId2") - .retrieve() - .bodyToMono(Integer.class) - .block(timeout), - new IsEqual(1 * 2 + 1234 + 1 )); + .atMost(10, TimeUnit.SECONDS) + .untilAsserted(() -> { + Integer result = execute(componentClient.forEventSourcedEntity(entityId) + .call(CounterEntity::get)); + + assertThat(result).isEqualTo(1 * 2 + 1234 + 1); + }); } @Test public void verifyViewIsNotSubscribedToMultiplyAndRouterIgnores() { - webClient - .post() - .uri("counter/counterId4/increase/1") - .retrieve() - .bodyToMono(Integer.class) - .block(timeout); - webClient - .post() - .uri("counter/counterId4/multiply/2") - .retrieve() - .bodyToMono(Integer.class) - .block(timeout); - - Integer counterGet = webClient - .post() - .uri("counter/counterId4/increase/1") - .retrieve() - .bodyToMono(Integer.class) - .block(timeout); + var entityId = "counterId4"; + EventSourcedEntityCallBuilder callBuilder = componentClient.forEventSourcedEntity(entityId); + execute(callBuilder.call(CounterEntity::increase).params(1)); + execute(callBuilder.call(CounterEntity::times).params(2)); + Integer counterGet = execute(callBuilder.call(CounterEntity::increase).params(1)); assertThat(counterGet).isEqualTo(1 * 2 + 1); await() - .ignoreExceptions() - .atMost(10, TimeUnit.SECONDS) - .until( - () -> webClient - .get() - .uri("/counters-ignore/by-value-with-ignore/2") - .retrieve() - .bodyToMono(Counter.class) - .map(counter -> counter.value) - .block(timeout), - new IsEqual(1 + 1)); + .ignoreExceptions() + .atMost(10, TimeUnit.SECONDS) + .until( + () -> webClient + .get() + .uri("/counters-ignore/by-value-with-ignore/2") + .retrieve() + .bodyToMono(Counter.class) + .map(counter -> counter.value()) + .block(timeout), + new IsEqual(1 + 1)); } @Test public void verifyFindCounterByValue() { - ResponseEntity response = - webClient - .post() - .uri("/counter/abc/increase/10") - .retrieve() - .toEntity(String.class) - .block(timeout); + execute(componentClient.forEventSourcedEntity("abc") + .call(CounterEntity::increase) + .params(10)); - assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); // the view is eventually updated await() @@ -351,31 +326,21 @@ public void verifyFindCounterByValue() { .uri("/counters/by-value/10") .retrieve() .bodyToMono(Counter.class) - .map(counter -> counter.value) + .map(counter -> counter.value()) .block(timeout), new IsEqual(10)); } @Test public void verifyCounterViewMultipleSubscriptions() throws InterruptedException { - ResponseEntity response1 = - webClient - .post() - .uri("/counter/hello2/increase/1") - .retrieve() - .toEntity(Integer.class) - .block(timeout); - assertThat(response1.getStatusCode()).isEqualTo(HttpStatus.OK); - ResponseEntity response2 = - webClient - .post() - .uri("/counter/hello3/increase/1") - .retrieve() - .toEntity(Integer.class) - .block(timeout); + execute(componentClient.forEventSourcedEntity("hello2") + .call(CounterEntity::increase) + .params(1)); - assertThat(response2.getStatusCode()).isEqualTo(HttpStatus.OK); + execute(componentClient.forEventSourcedEntity("hello3") + .call(CounterEntity::increase) + .params(1)); await() .ignoreExceptions() @@ -595,13 +560,10 @@ public void verifyForwardHeaders() { public void shouldPropagateMetadataWithHttpDeferredCall() { String value = "someValue"; - String actionResponse = webClient.get().uri("/action-with-meta/myKey/" + value) - .retrieve() - .bodyToMono(Message.class) - .map(m -> m.text) - .block(timeout); + Message actionResponse = execute(componentClient.forAction().call(ActionWithMetadata::actionWithMeta) + .params("myKey", value)); - assertThat(actionResponse).isEqualTo(value); + assertThat(actionResponse.text).isEqualTo(value); } @Test @@ -612,15 +574,15 @@ public void searchWithInstant() { // the view is eventually updated await() - .ignoreExceptions() - .atMost(20, TimeUnit.SECONDS) - .until(() -> getCustomersByCreationDate(now).size(), new IsEqual(1)); + .ignoreExceptions() + .atMost(20, TimeUnit.SECONDS) + .until(() -> getCustomersByCreationDate(now).size(), new IsEqual(1)); var later = now.plusSeconds(60 * 5); await() - .ignoreExceptions() - .atMost(20, TimeUnit.SECONDS) - .until(() -> getCustomersByCreationDate(later).size(), new IsEqual(0)); + .ignoreExceptions() + .atMost(20, TimeUnit.SECONDS) + .until(() -> getCustomersByCreationDate(later).size(), new IsEqual(0)); } @@ -628,7 +590,7 @@ public void searchWithInstant() { private List getUsersByName(String name) { return webClient .get() - .uri("/users/by-name/"+name) + .uri("/users/by-name/" + name) .retrieve() .bodyToFlux(User.class) .toStream() @@ -646,37 +608,26 @@ private UserWithVersion getUserByEmail(String email) { } private void updateUser(TestUser user) { - String userUpdate = - webClient - .post() - .uri("/user/" + user.id + "/" + user.email + "/" + user.name) - .retrieve() - .bodyToMono(String.class) - .block(timeout); + String userUpdate = execute(componentClient.forValueEntity(user.id) + .call(UserEntity::createOrUpdateUser) + .params(user.email, user.name)); assertThat(userUpdate).isEqualTo("\"Ok\""); } private void createUser(TestUser user) { - String userCreation = - webClient - .post() - .uri("/user/" + user.id + "/" + user.email + "/" + user.name) - .retrieve() - .bodyToMono(String.class) - .block(timeout); + String userCreation = execute(componentClient.forValueEntity(user.id) + .call(UserEntity::createOrUpdateUser) + .params(user.email, user.name)); assertThat(userCreation).isEqualTo("\"Ok\""); } private void createCustomer(CustomerEntity.Customer customer) { - String created = - webClient - .put() - .uri("/customers/" + customer.name()) - .bodyValue(customer) - .retrieve() - .bodyToMono(String.class) - .block(timeout); + + String created = execute(componentClient.forValueEntity(customer.name()) + .call(CustomerEntity::create) + .params(customer)); + assertThat(created).isEqualTo("\"Ok\""); } @@ -684,52 +635,38 @@ private void createCustomer(CustomerEntity.Customer customer) { @NotNull private List getCustomersByCreationDate(Instant createdOn) { return webClient - .post() - .uri("/customers/by_creation_time") - .bodyValue(new CustomerByCreationTime.ByTimeRequest(createdOn)) - .retrieve() - .bodyToMono(CustomerByCreationTime.CustomerList.class) - .block(timeout) - .customers(); + .post() + .uri("/customers/by_creation_time") + .bodyValue(new CustomerByCreationTime.ByTimeRequest(createdOn)) + .retrieve() + .bodyToMono(CustomerByCreationTime.CustomerList.class) + .block(timeout) + .customers(); } private void deleteUser(TestUser user) { - String deleteUser = - webClient - .delete() - .uri("/user/" + user.id) - .retrieve() - .bodyToMono(String.class) - .block(timeout); + String deleteUser = execute(componentClient.forValueEntity(user.id) + .call(UserEntity::deleteUser)); assertThat(deleteUser).isEqualTo("\"Ok from delete\""); } private void increaseCounter(String id, int value) { - webClient - .post() - .uri("counter/" + id + "/increase/" + value) - .retrieve() - .bodyToMono(Integer.class) - .block(timeout); + execute(componentClient.forEventSourcedEntity(id) + .call(CounterEntity::increase) + .params(value)); } private void multiplyCounter(String id, int value) { - webClient - .post() - .uri("counter/" + id + "/multiply/" + value) - .retrieve() - .bodyToMono(Integer.class) - .block(timeout); + execute(componentClient.forEventSourcedEntity(id) + .call(CounterEntity::times) + .params(value)); } private void assignCounter(String id, String assignee) { - webClient - .post() - .uri("assigned-counter/" + id + "/assign/" + assignee) - .retrieve() - .bodyToMono(String.class) - .block(timeout); + execute(componentClient.forValueEntity(id) + .call(AssignedCounterEntity::assign) + .params(assignee)); } private UserCounters getUserCounters(String userId) { @@ -740,6 +677,14 @@ private UserCounters getUserCounters(String userId) { .bodyToMono(UserCounters.class) .block(); } + + private T execute(DeferredCall deferredCall) { + try { + return deferredCall.execute().toCompletableFuture().get(timeout.toMillis(), TimeUnit.MILLISECONDS); + } catch (InterruptedException | ExecutionException | TimeoutException e) { + throw new RuntimeException(e); + } + } } class TestUser { @@ -756,6 +701,7 @@ public TestUser(String id, String email, String name) { public TestUser withName(String newName) { return new TestUser(id, email, newName); } + public TestUser withEmail(String newEmail) { return new TestUser(id, newEmail, name); } diff --git a/sdk/java-sdk-spring/src/it/java/com/example/wiring/actions/echo/ActionWithMetadata.java b/sdk/java-sdk-spring/src/it/java/com/example/wiring/actions/echo/ActionWithMetadata.java index 24d42e71f6..e588e9f727 100644 --- a/sdk/java-sdk-spring/src/it/java/com/example/wiring/actions/echo/ActionWithMetadata.java +++ b/sdk/java-sdk-spring/src/it/java/com/example/wiring/actions/echo/ActionWithMetadata.java @@ -18,25 +18,24 @@ import kalix.javasdk.Metadata; import kalix.javasdk.action.Action; -import kalix.spring.KalixClient; +import kalix.javasdk.client.ComponentClient; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; public class ActionWithMetadata extends Action { - private KalixClient kalixClient; + private ComponentClient componentClient; - public ActionWithMetadata(KalixClient kalixClient) { - this.kalixClient = kalixClient; + public ActionWithMetadata(ComponentClient componentClient) { + this.componentClient = componentClient; } @GetMapping("/action-with-meta/{key}/{value}") public Effect actionWithMeta(@PathVariable String key, @PathVariable String value) { - var def = kalixClient.get("/return-meta/" + key, Message.class); + var def = componentClient.forAction().call(ActionWithMetadata::returnMeta).params(key); return effects().forward(def.withMetadata(Metadata.EMPTY.add(key, value))); } - @GetMapping("/return-meta/{key}") public Effect returnMeta(@PathVariable String key) { var metaValue = actionContext().metadata().get(key).get(); diff --git a/sdk/java-sdk-spring/src/it/java/com/example/wiring/actions/echo/EchoAction.java b/sdk/java-sdk-spring/src/it/java/com/example/wiring/actions/echo/EchoAction.java index b21b1e68cc..c3f81d122f 100644 --- a/sdk/java-sdk-spring/src/it/java/com/example/wiring/actions/echo/EchoAction.java +++ b/sdk/java-sdk-spring/src/it/java/com/example/wiring/actions/echo/EchoAction.java @@ -18,22 +18,31 @@ import kalix.javasdk.action.Action; import kalix.javasdk.action.ActionCreationContext; +import kalix.javasdk.client.ComponentClient; +import kalix.spring.KalixClient; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestParam; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; import java.util.concurrent.CompletableFuture; public class EchoAction extends Action { private Parrot parrot; private ActionCreationContext ctx; + private final KalixClient kalixClient; + private final ComponentClient componentClient; - public EchoAction(Parrot parrot, ActionCreationContext ctx) { + public EchoAction(Parrot parrot, ActionCreationContext ctx, KalixClient kalixClient, ComponentClient componentClient) { this.parrot = parrot; this.ctx = ctx; + this.kalixClient = kalixClient; + this.componentClient = componentClient; } @GetMapping("/echo/message/{msg}") @@ -47,8 +56,20 @@ public Effect stringMessageFromParam(@RequestParam String msg) { return stringMessage(msg); } + @PostMapping("/echo/message/forward") + public Effect stringMessageFromParamFw(@RequestParam String msg) { + var result = kalixClient.get("/echo/message?msg=" + URLEncoder.encode(msg, StandardCharsets.UTF_8), Message.class); + return effects().forward(result); + } + + @PostMapping("/echo/message/forward") + public Effect stringMessageFromParamFwTyped(@RequestParam String msg) { + var result = componentClient.forAction().call(EchoAction::stringMessageFromParam).params(msg); + return effects().forward(result); + } + @GetMapping("/echo/repeat/{msg}/times/{times}") - public Flux> stringMessage( + public Flux> stringMessageRepeat( @PathVariable String msg, @PathVariable Integer times) { return Flux.range(1, times) // add an async boundary just to have some thread switching diff --git a/sdk/java-sdk-spring/src/it/java/com/example/wiring/actions/echo/ShortenedEchoAction.java b/sdk/java-sdk-spring/src/it/java/com/example/wiring/actions/echo/ShortenedEchoAction.java index c54aefbc7f..3e82aee887 100644 --- a/sdk/java-sdk-spring/src/it/java/com/example/wiring/actions/echo/ShortenedEchoAction.java +++ b/sdk/java-sdk-spring/src/it/java/com/example/wiring/actions/echo/ShortenedEchoAction.java @@ -18,33 +18,30 @@ import kalix.javasdk.action.Action; import kalix.javasdk.action.ActionCreationContext; -import kalix.spring.KalixClient; +import kalix.javasdk.client.ComponentClient; import org.springframework.web.bind.annotation.*; -import java.net.URLEncoder; -import java.nio.charset.StandardCharsets; - public class ShortenedEchoAction extends Action { private ActionCreationContext ctx; - private KalixClient kalixClient; + private ComponentClient componentClient; - public ShortenedEchoAction(ActionCreationContext ctx, KalixClient kalixClient) { + public ShortenedEchoAction(ActionCreationContext ctx, ComponentClient componentClient) { this.ctx = ctx; - this.kalixClient = kalixClient; + this.componentClient = componentClient; } @GetMapping("/echo/message/{msg}/short") public Effect stringMessage(@PathVariable String msg) { - var shortenedMsg = URLEncoder.encode(msg.replaceAll("[AEIOUaeiou]", ""), StandardCharsets.UTF_8); - var result = kalixClient.get("/echo/message/" + shortenedMsg, Message.class).execute(); + var shortenedMsg = msg.replaceAll("[AEIOUaeiou]", ""); + var result = componentClient.forAction().call(EchoAction::stringMessage).params(shortenedMsg).execute(); return effects().asyncReply(result); } @GetMapping("/echo/message-short") public Effect leetShortUsingFwd(@RequestParam String msg) { - var shortenedMsg = URLEncoder.encode(leetShort(msg), StandardCharsets.UTF_8); - var result = kalixClient.get("/echo/message?msg=" + shortenedMsg, Message.class); + var shortenedMsg = leetShort(msg); + var result = componentClient.forAction().call(EchoAction::stringMessageFromParam).params(shortenedMsg); return effects().forward(result); } @@ -55,8 +52,8 @@ public Effect leetMessageFromPathUsingFwd(@PathVariable String msg) { @PostMapping("/echo/message/leetshort") public Effect leetMessageWithFwdPost(@RequestBody Message msg) { - var shortenedMsg = URLEncoder.encode(leetShort(msg.text), StandardCharsets.UTF_8); - var result = kalixClient.get("/echo/message/" + shortenedMsg, Message.class); + var shortenedMsg = leetShort(msg.text); + var result = componentClient.forAction().call(EchoAction::stringMessage).params(shortenedMsg); return effects().forward(result); } diff --git a/sdk/java-sdk-spring/src/it/java/com/example/wiring/eventsourcedentities/counter/Counter.java b/sdk/java-sdk-spring/src/it/java/com/example/wiring/eventsourcedentities/counter/Counter.java index 28f6f4ff46..c2d93b4c13 100644 --- a/sdk/java-sdk-spring/src/it/java/com/example/wiring/eventsourcedentities/counter/Counter.java +++ b/sdk/java-sdk-spring/src/it/java/com/example/wiring/eventsourcedentities/counter/Counter.java @@ -16,17 +16,7 @@ package com.example.wiring.eventsourcedentities.counter; -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonProperty; - -public class Counter { - - public final Integer value; - - @JsonCreator - public Counter(@JsonProperty("value") Integer value) { - this.value = value; - } +public record Counter(Integer value) { public Counter onValueIncreased(CounterEvent.ValueIncreased evt) { return new Counter(this.value + evt.value()); diff --git a/sdk/java-sdk-spring/src/it/java/com/example/wiring/eventsourcedentities/counter/CounterEntity.java b/sdk/java-sdk-spring/src/it/java/com/example/wiring/eventsourcedentities/counter/CounterEntity.java index 4b17c7d8dd..7aae11ebbb 100644 --- a/sdk/java-sdk-spring/src/it/java/com/example/wiring/eventsourcedentities/counter/CounterEntity.java +++ b/sdk/java-sdk-spring/src/it/java/com/example/wiring/eventsourcedentities/counter/CounterEntity.java @@ -49,23 +49,23 @@ public Counter emptyState() { @PostMapping("/increase/{value}") public Effect increase(@PathVariable Integer value) { - return effects().emitEvent(new CounterEvent.ValueIncreased(value)).thenReply(c -> c.value); + return effects().emitEvent(new CounterEvent.ValueIncreased(value)).thenReply(c -> c.value()); } @PostMapping("/set/{value}") public Effect set(@PathVariable Integer value) { - return effects().emitEvent(new CounterEvent.ValueSet(value)).thenReply(c -> c.value); + return effects().emitEvent(new CounterEvent.ValueSet(value)).thenReply(c -> c.value()); } @PostMapping("/set") public Effect setFromReqParam(@RequestParam Integer value) { - return effects().emitEvent(new CounterEvent.ValueSet(value)).thenReply(c -> c.value); + return effects().emitEvent(new CounterEvent.ValueSet(value)).thenReply(c -> c.value()); } @GetMapping - public Effect get() { + public Effect get() { // don't modify, we want to make sure we call currentState().value here - return effects().reply(currentState().value.toString()); + return effects().reply(currentState().value()); } @PostMapping("/multiply/{value}") @@ -78,7 +78,7 @@ public Effect times(@PathVariable Integer value) { currentState(), value); - return effects().emitEvent(new CounterEvent.ValueMultiplied(value)).thenReply(c -> c.value); + return effects().emitEvent(new CounterEvent.ValueMultiplied(value)).thenReply(c -> c.value()); } @PostMapping("/restart") diff --git a/sdk/java-sdk-spring/src/it/java/com/example/wiring/eventsourcedentities/counter/IncreaseAction.java b/sdk/java-sdk-spring/src/it/java/com/example/wiring/eventsourcedentities/counter/IncreaseAction.java index b301046202..86665e758a 100644 --- a/sdk/java-sdk-spring/src/it/java/com/example/wiring/eventsourcedentities/counter/IncreaseAction.java +++ b/sdk/java-sdk-spring/src/it/java/com/example/wiring/eventsourcedentities/counter/IncreaseAction.java @@ -22,7 +22,7 @@ import kalix.javasdk.action.Action; import kalix.javasdk.action.ActionCreationContext; import kalix.javasdk.annotations.Subscribe; -import kalix.spring.KalixClient; +import kalix.javasdk.client.ComponentClient; import kalix.spring.KalixConfigurationTest; import org.springframework.context.annotation.Import; @@ -31,12 +31,12 @@ @Import(KalixConfigurationTest.class) public class IncreaseAction extends Action { - private KalixClient kalixClient; + private ComponentClient componentClient; private ActionCreationContext context; - public IncreaseAction(KalixClient kalixClient, ActionCreationContext context) { - this.kalixClient = kalixClient; + public IncreaseAction(ComponentClient componentClient, ActionCreationContext context) { + this.componentClient = componentClient; this.context = context; } @@ -54,11 +54,10 @@ public Effect printSet(CounterEvent.ValueSet event) { public Effect printIncrease(CounterEvent.ValueIncreased event) { String entityId = this.actionContext().metadata().asCloudEvent().subject().get(); if (event.value() == 42) { - CompletionStage res = - kalixClient.post("/counter/" + entityId + "/increase/1", Integer.class).execute(); + CompletionStage res = componentClient.forEventSourcedEntity(entityId).call(CounterEntity::increase).params(1).execute(); return effects().asyncReply(res); } else if (event.value() == 4422) { - DeferredCall inc = kalixClient.post("/counter/" + entityId + "/increase/1", Integer.class); + DeferredCall inc = componentClient.forEventSourcedEntity(entityId).call(CounterEntity::increase).params(1); return effects().reply(event.value()) .addSideEffect(SideEffect.of(inc)); } diff --git a/sdk/java-sdk-spring/src/it/java/com/example/wiring/eventsourcedentities/counter/IncreaseActionWithIgnore.java b/sdk/java-sdk-spring/src/it/java/com/example/wiring/eventsourcedentities/counter/IncreaseActionWithIgnore.java index a4d8a70d5b..a2c20f414b 100644 --- a/sdk/java-sdk-spring/src/it/java/com/example/wiring/eventsourcedentities/counter/IncreaseActionWithIgnore.java +++ b/sdk/java-sdk-spring/src/it/java/com/example/wiring/eventsourcedentities/counter/IncreaseActionWithIgnore.java @@ -19,7 +19,7 @@ import kalix.javasdk.action.Action; import kalix.javasdk.action.ActionCreationContext; import kalix.javasdk.annotations.Subscribe; -import kalix.spring.KalixClient; +import kalix.javasdk.client.ComponentClient; import kalix.spring.KalixConfigurationTest; import org.springframework.context.annotation.Import; @@ -29,12 +29,12 @@ @Subscribe.EventSourcedEntity(value = CounterEntity.class, ignoreUnknown = true) public class IncreaseActionWithIgnore extends Action { - private KalixClient kalixClient; + private ComponentClient componentClient; private ActionCreationContext context; - public IncreaseActionWithIgnore(KalixClient kalixClient, ActionCreationContext context) { - this.kalixClient = kalixClient; + public IncreaseActionWithIgnore(ComponentClient componentClient, ActionCreationContext context) { + this.componentClient = componentClient; this.context = context; } @@ -42,7 +42,7 @@ public Effect oneShallPass(CounterEvent.ValueIncreased event) { String entityId = this.actionContext().metadata().asCloudEvent().subject().get(); if (event.value() == 1234) { CompletionStage res = - kalixClient.post("/counter/" + entityId + "/increase/1", Integer.class).execute(); + componentClient.forEventSourcedEntity(entityId).call(CounterEntity::increase).params(1).execute(); return effects().asyncReply(res); } return effects().reply(event.value()); diff --git a/sdk/java-sdk-spring/src/it/java/com/example/wiring/valueentities/user/ValidateUserAction.java b/sdk/java-sdk-spring/src/it/java/com/example/wiring/valueentities/user/ValidateUserAction.java index 27b3e75ebd..c90d2464f8 100644 --- a/sdk/java-sdk-spring/src/it/java/com/example/wiring/valueentities/user/ValidateUserAction.java +++ b/sdk/java-sdk-spring/src/it/java/com/example/wiring/valueentities/user/ValidateUserAction.java @@ -19,7 +19,7 @@ import kalix.javasdk.StatusCode; import kalix.javasdk.action.Action; import kalix.javasdk.action.ActionCreationContext; -import kalix.spring.KalixClient; +import kalix.javasdk.client.ComponentClient; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.PatchMapping; import org.springframework.web.bind.annotation.PathVariable; @@ -30,11 +30,11 @@ public class ValidateUserAction extends Action { private ActionCreationContext ctx; - private KalixClient kalixClient; + private ComponentClient componentClient; - public ValidateUserAction(ActionCreationContext ctx, KalixClient kalixClient) { + public ValidateUserAction(ActionCreationContext ctx, ComponentClient componentClient) { this.ctx = ctx; - this.kalixClient = kalixClient; + this.componentClient = componentClient; } @PutMapping("/{email}/{name}") @@ -42,7 +42,7 @@ public Action.Effect createOrUpdateUser(@PathVariable String user, @Path if (email.isEmpty() || name.isEmpty()) return effects().error("No field can be empty", StatusCode.ErrorCode.BAD_REQUEST); - var defCall = kalixClient.put("/user/" + user + "/" + email + "/" + name, String.class); + var defCall = componentClient.forValueEntity(user).call(UserEntity::createUser).params(email, name); return effects().forward(defCall); } @@ -51,13 +51,13 @@ public Action.Effect updateEmail(@PathVariable String user, @PathVariabl if (email.isEmpty()) return effects().error("No field can be empty", StatusCode.ErrorCode.BAD_REQUEST); - var defCall = kalixClient.patch("/user/" + user + "/email/" + email, String.class); + var defCall = componentClient.forValueEntity(user).call(UserEntity::updateEmail).params(email); return effects().forward(defCall); } @DeleteMapping public Action.Effect delete(@PathVariable String user) { - var defCall = kalixClient.delete("/user/" + user, String.class); + var defCall = componentClient.forValueEntity(user).call(UserEntity::deleteUser); return effects().forward(defCall); } } diff --git a/sdk/java-sdk-spring/src/it/java/com/example/wiring/views/UserWithId.java b/sdk/java-sdk-spring/src/it/java/com/example/wiring/views/UserWithId.java index 4dcae4b7e1..b14b3c8cd6 100644 --- a/sdk/java-sdk-spring/src/it/java/com/example/wiring/views/UserWithId.java +++ b/sdk/java-sdk-spring/src/it/java/com/example/wiring/views/UserWithId.java @@ -16,17 +16,5 @@ package com.example.wiring.views; -import com.fasterxml.jackson.annotation.JsonCreator; - -public class UserWithId { - public String id; - public String email; - public String name; - - @JsonCreator - public UserWithId(String id, String email, String name) { - this.id = id; - this.email = email; - this.name = name; - } +public record UserWithId(String id, String email, String name) { } diff --git a/sdk/java-sdk-spring/src/it/java/com/example/wiring/workflowentities/FailingCounterEntity.java b/sdk/java-sdk-spring/src/it/java/com/example/wiring/workflowentities/FailingCounterEntity.java index 8a4eb04f7d..84a8e6d0d3 100644 --- a/sdk/java-sdk-spring/src/it/java/com/example/wiring/workflowentities/FailingCounterEntity.java +++ b/sdk/java-sdk-spring/src/it/java/com/example/wiring/workflowentities/FailingCounterEntity.java @@ -53,13 +53,13 @@ public Effect increase(@PathVariable Integer value) { if (value % 3 != 0) { return effects().error("wrong value: " + value); } else { - return effects().emitEvent(new CounterEvent.ValueIncreased(value)).thenReply(c -> c.value); + return effects().emitEvent(new CounterEvent.ValueIncreased(value)).thenReply(c -> c.value()); } } @GetMapping - public Effect get() { - return effects().reply(currentState().value.toString()); + public Effect get() { + return effects().reply(currentState().value()); } @EventHandler diff --git a/sdk/java-sdk-spring/src/it/java/com/example/wiring/workflowentities/SpringWorkflowIntegrationTest.java b/sdk/java-sdk-spring/src/it/java/com/example/wiring/workflowentities/SpringWorkflowIntegrationTest.java index 9f2f053d3c..0a2f43bdbf 100644 --- a/sdk/java-sdk-spring/src/it/java/com/example/wiring/workflowentities/SpringWorkflowIntegrationTest.java +++ b/sdk/java-sdk-spring/src/it/java/com/example/wiring/workflowentities/SpringWorkflowIntegrationTest.java @@ -18,6 +18,9 @@ import com.example.wiring.TestkitConfig; import com.example.wiring.actions.echo.Message; +import com.google.protobuf.any.Any; +import kalix.javasdk.DeferredCall; +import kalix.javasdk.client.ComponentClient; import kalix.spring.KalixConfigurationTest; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -33,7 +36,9 @@ import java.time.Duration; import java.util.UUID; +import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; import static java.time.temporal.ChronoUnit.SECONDS; import static org.assertj.core.api.Assertions.assertThat; @@ -47,6 +52,9 @@ public class SpringWorkflowIntegrationTest { @Autowired private WebClient webClient; + @Autowired + private ComponentClient componentClient; + private Duration timeout = Duration.of(10, SECONDS); @@ -79,17 +87,13 @@ public void shouldTransferMoney() { createWallet(walletId1, 100); createWallet(walletId2, 100); var transferId = randomTransferId(); - var transferUrl = "/transfer/" + transferId; var transfer = new Transfer(walletId1, walletId2, 10); - String response = webClient.put().uri(transferUrl) - .bodyValue(transfer) - .retrieve() - .bodyToMono(Message.class) - .map(m -> m.text) - .block(timeout); + Message response = execute(componentClient.forWorkflow(transferId) + .call(TransferWorkflow::startTransfer) + .params(transfer)); - assertThat(response).isEqualTo("transfer started"); + assertThat(response.text).isEqualTo("transfer started"); await() .atMost(10, TimeUnit.of(SECONDS)) @@ -110,17 +114,13 @@ public void shouldTransferMoneyWithoutStepInputs() { createWallet(walletId1, 100); createWallet(walletId2, 100); var transferId = randomTransferId(); - var transferUrl = "/transfer-without-inputs/" + transferId; var transfer = new Transfer(walletId1, walletId2, 10); - String response = webClient.put().uri(transferUrl) - .bodyValue(transfer) - .retrieve() - .bodyToMono(Message.class) - .map(m -> m.text) - .block(timeout); + Message response = execute(componentClient.forWorkflow(transferId) + .call(TransferWorkflowWithoutInputs::startTransfer) + .params(transfer)); - assertThat(response).isEqualTo("transfer started"); + assertThat(response.text).isEqualTo("transfer started"); await() .atMost(10, TimeUnit.of(SECONDS)) @@ -140,17 +140,13 @@ public void shouldTransferAsyncMoneyWithoutStepInputs() { createWallet(walletId1, 100); createWallet(walletId2, 100); var transferId = randomTransferId(); - var transferUrl = "/transfer-without-inputs/" + transferId + "/async"; var transfer = new Transfer(walletId1, walletId2, 10); - String response = webClient.put().uri(transferUrl) - .bodyValue(transfer) - .retrieve() - .bodyToMono(Message.class) - .map(m -> m.text) - .block(timeout); + Message response = execute(componentClient.forWorkflow(transferId) + .call(TransferWorkflowWithoutInputs::startTransferAsync) + .params(transfer)); - assertThat(response).isEqualTo("transfer started"); + assertThat(response.text).isEqualTo("transfer started"); await() .atMost(10, TimeUnit.of(SECONDS)) @@ -171,17 +167,13 @@ public void shouldTransferMoneyWithFraudDetection() { createWallet(walletId1, 100); createWallet(walletId2, 100); var transferId = randomTransferId(); - var transferUrl = "/transfer-with-fraud-detection/" + transferId; var transfer = new Transfer(walletId1, walletId2, 10); - String response = webClient.put().uri(transferUrl) - .bodyValue(transfer) - .retrieve() - .bodyToMono(Message.class) - .map(m -> m.text) - .block(timeout); + Message response = execute(componentClient.forWorkflow(transferId) + .call(TransferWorkflowWithFraudDetection::startTransfer) + .params(transfer)); - assertThat(response).isEqualTo("transfer started"); + assertThat(response.text).isEqualTo("transfer started"); await() .atMost(10, TimeUnit.of(SECONDS)) @@ -201,36 +193,29 @@ public void shouldTransferMoneyWithFraudDetectionAndManualAcceptance() { createWallet(walletId1, 100000); createWallet(walletId2, 100000); var transferId = randomTransferId(); - var transferUrl = "/transfer-with-fraud-detection/" + transferId; var transfer = new Transfer(walletId1, walletId2, 1000); - String response = webClient.put().uri(transferUrl) - .bodyValue(transfer) - .retrieve() - .bodyToMono(Message.class) - .map(m -> m.text) - .block(timeout); + Message response = execute(componentClient.forWorkflow(transferId) + .call(TransferWorkflowWithFraudDetection::startTransfer) + .params(transfer)); - assertThat(response).isEqualTo("transfer started"); + assertThat(response.text).isEqualTo("transfer started"); await() .atMost(10, TimeUnit.of(SECONDS)) .untilAsserted(() -> { - var transferState = getTransferState(transferUrl); + var transferState = execute(componentClient.forWorkflow(transferId).call(TransferWorkflowWithFraudDetection::getTransferState)); assertThat(transferState.finished).isFalse(); assertThat(transferState.accepted).isFalse(); assertThat(transferState.lastStep).isEqualTo("fraud-detection"); }); - String acceptanceResponse = webClient.patch().uri(transferUrl + "/accept") - .bodyValue(transfer) - .retrieve() - .bodyToMono(Message.class) - .map(m -> m.text) - .block(timeout); + Message acceptedResponse = execute(componentClient.forWorkflow(transferId) + .call(TransferWorkflowWithFraudDetection::acceptTransfer)); + + assertThat(acceptedResponse.text).isEqualTo("transfer accepted"); - assertThat(acceptanceResponse).isEqualTo("transfer accepted"); await() .atMost(10, TimeUnit.of(SECONDS)) @@ -250,17 +235,13 @@ public void shouldNotTransferMoneyWhenFraudDetectionRejectTransfer() { createWallet(walletId1, 100); createWallet(walletId2, 100); var transferId = randomTransferId(); - var transferUrl = "/transfer-with-fraud-detection/" + transferId; var transfer = new Transfer(walletId1, walletId2, 1000000); - String response = webClient.put().uri(transferUrl) - .bodyValue(transfer) - .retrieve() - .bodyToMono(Message.class) - .map(m -> m.text) - .block(timeout); + Message response = execute(componentClient.forWorkflow(transferId) + .call(TransferWorkflowWithFraudDetection::startTransfer) + .params(transfer)); - assertThat(response).isEqualTo("transfer started"); + assertThat(response.text).isEqualTo("transfer started"); await() .atMost(10, TimeUnit.of(SECONDS)) @@ -271,7 +252,7 @@ public void shouldNotTransferMoneyWhenFraudDetectionRejectTransfer() { assertThat(balance1).isEqualTo(100); assertThat(balance2).isEqualTo(100); - var transferState = getTransferState(transferUrl); + var transferState = execute(componentClient.forWorkflow(transferId).call(TransferWorkflowWithFraudDetection::getTransferState)); assertThat(transferState.finished).isTrue(); assertThat(transferState.accepted).isFalse(); assertThat(transferState.lastStep).isEqualTo("fraud-detection"); @@ -283,16 +264,14 @@ public void shouldRecoverFailingCounterWorkflowWithDefaultRecoverStrategy() { //given var counterId = randomId(); var workflowId = randomId(); - String path = "/workflow-with-default-recover-strategy/" + workflowId; //when - String response = webClient.put().uri(path + "/" + counterId) - .retrieve() - .bodyToMono(Message.class) - .map(m -> m.text) - .block(timeout); + Message response = execute(componentClient.forWorkflow(workflowId) + .call(WorkflowWithDefaultRecoverStrategy::startFailingCounter) + .params(counterId)); + + assertThat(response.text).isEqualTo("workflow started"); - assertThat(response).isEqualTo("workflow started"); //then await() @@ -305,7 +284,7 @@ public void shouldRecoverFailingCounterWorkflowWithDefaultRecoverStrategy() { await() .atMost(10, TimeUnit.of(SECONDS)) .untilAsserted(() -> { - var state = getWorkflowState(path); + var state = execute(componentClient.forWorkflow(workflowId).call(WorkflowWithDefaultRecoverStrategy::get)); assertThat(state.finished()).isTrue(); }); } @@ -315,16 +294,13 @@ public void shouldRecoverFailingCounterWorkflowWithRecoverStrategy() { //given var counterId = randomId(); var workflowId = randomId(); - String path = "/workflow-with-recover-strategy/" + workflowId; //when - String response = webClient.put().uri(path + "/" + counterId) - .retrieve() - .bodyToMono(Message.class) - .map(m -> m.text) - .block(timeout); + Message response = execute(componentClient.forWorkflow(workflowId) + .call(WorkflowWithRecoverStrategy::startFailingCounter) + .params(counterId)); - assertThat(response).isEqualTo("workflow started"); + assertThat(response.text).isEqualTo("workflow started"); //then await() @@ -337,7 +313,7 @@ public void shouldRecoverFailingCounterWorkflowWithRecoverStrategy() { await() .atMost(10, TimeUnit.of(SECONDS)) .untilAsserted(() -> { - var state = getWorkflowState(path); + var state = execute(componentClient.forWorkflow(workflowId).call(WorkflowWithRecoverStrategy::get)); assertThat(state.finished()).isTrue(); }); } @@ -347,16 +323,13 @@ public void shouldRecoverWorkflowTimeout() { //given var counterId = randomId(); var workflowId = randomId(); - String path = "/workflow-with-timeout/" + workflowId; //when - String response = webClient.put().uri(path + "/" + counterId) - .retrieve() - .bodyToMono(Message.class) - .map(m -> m.text) - .block(timeout); + Message response = execute(componentClient.forWorkflow(workflowId) + .call(WorkflowWithTimeout::startFailingCounter) + .params(counterId)); - assertThat(response).isEqualTo("workflow started"); + assertThat(response.text).isEqualTo("workflow started"); //then await() @@ -369,33 +342,30 @@ public void shouldRecoverWorkflowTimeout() { await() .atMost(10, TimeUnit.of(SECONDS)) .untilAsserted(() -> { - var state = getWorkflowState(path); + var state = execute(componentClient.forWorkflow(workflowId).call(WorkflowWithTimeout::get)); assertThat(state.finished()).isTrue(); }); } @Test - public void shouldRecoverWorkflowStepTimeout() throws InterruptedException { + public void shouldRecoverWorkflowStepTimeout() { //given var counterId = randomId(); var workflowId = randomId(); - String path = "/workflow-with-step-timeout/" + workflowId; //when - String response = webClient.put().uri(path + "/" + counterId) - .retrieve() - .bodyToMono(Message.class) - .map(m -> m.text) - .block(timeout); + Message response = execute(componentClient.forWorkflow(workflowId) + .call(WorkflowWithStepTimeout::startFailingCounter) + .params(counterId)); - assertThat(response).isEqualTo("workflow started"); + assertThat(response.text).isEqualTo("workflow started"); //then await() .atMost(10, TimeUnit.of(SECONDS)) .ignoreExceptions() .untilAsserted(() -> { - var state = getWorkflowState(path); + var state = execute(componentClient.forWorkflow(workflowId).call(WorkflowWithStepTimeout::get)); assertThat(state.value()).isEqualTo(2); assertThat(state.finished()).isTrue(); }); @@ -406,22 +376,19 @@ public void shouldUseTimerInWorkflowDefinition() { //given var counterId = randomId(); var workflowId = randomId(); - String path = "/workflow-with-timer/" + workflowId; //when - String response = webClient.put().uri(path + "/" + counterId) - .retrieve() - .bodyToMono(Message.class) - .map(m -> m.text) - .block(timeout); + Message response = execute(componentClient.forWorkflow(workflowId) + .call(WorkflowWithTimer::startFailingCounter) + .params(counterId)); - assertThat(response).isEqualTo("workflow started"); + assertThat(response.text).isEqualTo("workflow started"); //then await() .atMost(10, TimeUnit.of(SECONDS)) .untilAsserted(() -> { - var state = getWorkflowState(path); + var state = execute(componentClient.forWorkflow(workflowId).call(WorkflowWithTimer::get)); assertThat(state.finished()).isTrue(); assertThat(state.value()).isEqualTo(12); }); @@ -435,15 +402,27 @@ public void failRequestWhenReqParamsIsNotPresent() { //when ResponseEntity response = webClient.put().uri(path) - .retrieve() - .toEntity(String.class) - .onErrorResume(WebClientResponseException.class, error -> Mono.just(ResponseEntity.status(error.getStatusCode()).body(error.getResponseBodyAsString()))) - .block(timeout); + .retrieve() + .toEntity(String.class) + .onErrorResume(WebClientResponseException.class, error -> Mono.just(ResponseEntity.status(error.getStatusCode()).body(error.getResponseBodyAsString()))) + .block(timeout); assertThat(response.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST); assertThat(response.getBody()).isEqualTo("Required request parameter is missing: counterId"); } + private T execute(DeferredCall deferredCall) { + return execute(deferredCall, timeout); + } + + private T execute(DeferredCall deferredCall, Duration timeout) { + try { + return deferredCall.execute().toCompletableFuture().get(timeout.toMillis(), TimeUnit.MILLISECONDS); + } catch (InterruptedException | ExecutionException | TimeoutException e) { + throw new RuntimeException(e); + } + } + private String randomTransferId() { return randomId(); } @@ -453,43 +432,18 @@ private static String randomId() { } private Integer getFailingCounterValue(String counterId) { - return webClient.get().uri("/failing-counter/" + counterId) - .retrieve() - .bodyToMono(String.class) - .map(s -> s.replace("\"", "")) - .map(Integer::valueOf) - .block(Duration.ofSeconds(20)); - } - - private FailingCounterState getWorkflowState(String url) { - return webClient.get().uri(url) - .retrieve() - .bodyToMono(FailingCounterState.class) - .block(Duration.ofSeconds(20)); + return execute(componentClient.forEventSourcedEntity(counterId) + .call(FailingCounterEntity::get), Duration.ofSeconds(20)); } private void createWallet(String walletId, int amount) { - ResponseEntity response = webClient.post().uri("/wallet/" + walletId + "/create/" + amount) - .retrieve() - .toBodilessEntity() - .block(timeout); - - assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); + execute(componentClient.forValueEntity(walletId) + .call(WalletEntity::create) + .params(amount)); } private int getWalletBalance(String walletId) { - Balance response = webClient.get().uri("/wallet/" + walletId) - .retrieve() - .bodyToMono(Balance.class) - .block(timeout); - - return response.value; - } - - private TransferState getTransferState(String url) { - return webClient.get().uri(url) - .retrieve() - .bodyToMono(TransferState.class) - .block(timeout); + return execute(componentClient.forValueEntity(walletId) + .call(WalletEntity::get)).value; } } diff --git a/sdk/java-sdk-spring/src/it/java/com/example/wiring/workflowentities/TransferWorkflow.java b/sdk/java-sdk-spring/src/it/java/com/example/wiring/workflowentities/TransferWorkflow.java index b4a6d5c8b0..99249e47cb 100644 --- a/sdk/java-sdk-spring/src/it/java/com/example/wiring/workflowentities/TransferWorkflow.java +++ b/sdk/java-sdk-spring/src/it/java/com/example/wiring/workflowentities/TransferWorkflow.java @@ -18,35 +18,35 @@ import com.example.wiring.actions.echo.Message; import io.grpc.Status; +import kalix.javasdk.client.ComponentClient; import kalix.javasdk.annotations.Id; import kalix.javasdk.annotations.TypeId; import kalix.javasdk.workflow.Workflow; -import kalix.spring.KalixClient; import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import java.time.Duration; -@TypeId("transfer-workflow") @Id("transferId") +@TypeId("transfer-workflow") @RequestMapping("/transfer/{transferId}") public class TransferWorkflow extends Workflow { private final String withdrawStepName = "withdraw"; private final String depositStepName = "deposit"; - private KalixClient kalixClient; + private ComponentClient componentClient; - public TransferWorkflow(KalixClient kalixClient) { - this.kalixClient = kalixClient; + public TransferWorkflow(ComponentClient componentClient) { + this.componentClient = componentClient; } @Override public WorkflowDef definition() { var withdraw = step(withdrawStepName) - .call(Withdraw.class, cmd -> kalixClient.patch("/wallet/" + cmd.from + "/withdraw/" + cmd.amount, String.class)) + .call(Withdraw.class, cmd -> componentClient.forValueEntity(cmd.from).call(WalletEntity::withdraw).params(cmd.amount)) .andThen(String.class, __ -> { var state = currentState().withLastStep("withdrawn").accepted(); @@ -59,8 +59,8 @@ public WorkflowDef definition() { var deposit = step(depositStepName) - .call(Deposit.class, cmd -> kalixClient.patch("/wallet/" + cmd.to + "/deposit/" + cmd.amount, String.class)) - .andThen(String.class, __ -> { + .call(Deposit.class, cmd -> componentClient.forValueEntity(cmd.to).call(WalletEntity::deposit).params(cmd.amount) + ).andThen(String.class, __ -> { var state = currentState().withLastStep("deposited").finished(); return effects().updateState(state).end(); }); diff --git a/sdk/java-sdk-spring/src/it/java/com/example/wiring/workflowentities/TransferWorkflowWithFraudDetection.java b/sdk/java-sdk-spring/src/it/java/com/example/wiring/workflowentities/TransferWorkflowWithFraudDetection.java index b7645b2caf..bf4e430060 100644 --- a/sdk/java-sdk-spring/src/it/java/com/example/wiring/workflowentities/TransferWorkflowWithFraudDetection.java +++ b/sdk/java-sdk-spring/src/it/java/com/example/wiring/workflowentities/TransferWorkflowWithFraudDetection.java @@ -20,18 +20,23 @@ import com.example.wiring.workflowentities.FraudDetectionResult.TransferRejected; import com.example.wiring.workflowentities.FraudDetectionResult.TransferRequiresManualAcceptation; import com.example.wiring.workflowentities.FraudDetectionResult.TransferVerified; +import kalix.javasdk.client.ComponentClient; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PatchMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; import kalix.javasdk.annotations.Id; import kalix.javasdk.annotations.TypeId; import kalix.javasdk.workflow.Workflow; -import kalix.spring.KalixClient; -import org.springframework.web.bind.annotation.*; + import java.util.concurrent.CompletionStage; import static java.util.concurrent.CompletableFuture.completedFuture; -@TypeId("transfer-workflow-with-fraud-detection") @Id("transferId") +@TypeId("transfer-workflow-with-fraud-detection") @RequestMapping("/transfer-with-fraud-detection/{transferId}") public class TransferWorkflowWithFraudDetection extends Workflow { @@ -39,10 +44,10 @@ public class TransferWorkflowWithFraudDetection extends Workflow private final String withdrawStepName = "withdraw"; private final String depositStepName = "deposit"; - private KalixClient kalixClient; + private ComponentClient componentClient; - public TransferWorkflowWithFraudDetection(KalixClient kalixClient) { - this.kalixClient = kalixClient; + public TransferWorkflowWithFraudDetection(ComponentClient componentClient) { + this.componentClient = componentClient; } @Override @@ -54,12 +59,13 @@ public WorkflowDef definition() { var withdraw = step(withdrawStepName) - .call(Withdraw.class, cmd -> kalixClient.patch("/wallet/" + cmd.from + "/withdraw/" + cmd.amount, String.class)) + .call(Withdraw.class, cmd -> + componentClient.forValueEntity(cmd.from).call(WalletEntity::withdraw).params(cmd.amount)) .andThen(String.class, this::moveToDeposit); var deposit = step(depositStepName) - .call(Deposit.class, cmd -> kalixClient.patch("/wallet/" + cmd.to + "/deposit/" + cmd.amount, String.class)) + .call(Deposit.class, cmd -> componentClient.forValueEntity(cmd.to).call(WalletEntity::deposit).params(cmd.amount)) .andThen(String.class, this::finishWithSuccess); return workflow() diff --git a/sdk/java-sdk-spring/src/it/java/com/example/wiring/workflowentities/TransferWorkflowWithoutInputs.java b/sdk/java-sdk-spring/src/it/java/com/example/wiring/workflowentities/TransferWorkflowWithoutInputs.java index 8aecc30c52..43781d1c38 100644 --- a/sdk/java-sdk-spring/src/it/java/com/example/wiring/workflowentities/TransferWorkflowWithoutInputs.java +++ b/sdk/java-sdk-spring/src/it/java/com/example/wiring/workflowentities/TransferWorkflowWithoutInputs.java @@ -18,16 +18,16 @@ import com.example.wiring.actions.echo.Message; import io.grpc.Status; +import kalix.javasdk.client.ComponentClient; import kalix.javasdk.annotations.Id; import kalix.javasdk.annotations.TypeId; import kalix.javasdk.workflow.Workflow; -import kalix.spring.KalixClient; import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; -@TypeId("transfer-workflow-without-inputs") @Id("transferId") +@TypeId("transfer-workflow-without-inputs") @RequestMapping("/transfer-without-inputs/{transferId}") public class TransferWorkflowWithoutInputs extends Workflow { @@ -36,10 +36,10 @@ public class TransferWorkflowWithoutInputs extends Workflow { private final String depositStepName = "deposit"; private final String depositAsyncStepName = "deposit-async"; - private KalixClient kalixClient; + private ComponentClient componentClient; - public TransferWorkflowWithoutInputs(KalixClient kalixClient) { - this.kalixClient = kalixClient; + public TransferWorkflowWithoutInputs(ComponentClient componentClient) { + this.componentClient = componentClient; } @Override @@ -48,7 +48,7 @@ public WorkflowDef definition() { step(withdrawStepName) .call(() -> { var transfer = currentState().transfer; - return kalixClient.patch("/wallet/" + transfer.from + "/withdraw/" + transfer.amount, String.class); + return componentClient.forValueEntity(transfer.from).call(WalletEntity::withdraw).params(transfer.amount); }) .andThen(String.class, response -> { var state = currentState().withLastStep("withdrawn").accepted(); @@ -61,7 +61,7 @@ public WorkflowDef definition() { step(withdrawAsyncStepName) .asyncCall(() -> { var transfer = currentState().transfer; - return kalixClient.patch("/wallet/" + transfer.from + "/withdraw/" + transfer.amount, String.class).execute(); + return componentClient.forValueEntity(transfer.from).call(WalletEntity::withdraw).params(transfer.amount).execute(); }) .andThen(String.class, response -> { var state = currentState().withLastStep("withdrawn").accepted(); @@ -75,7 +75,7 @@ public WorkflowDef definition() { step(depositStepName) .call(() -> { var transfer = currentState().transfer; - return kalixClient.patch("/wallet/" + transfer.to + "/deposit/" + transfer.amount, String.class); + return componentClient.forValueEntity(transfer.to).call(WalletEntity::deposit).params(transfer.amount); }) .andThen(String.class, __ -> { var state = currentState().withLastStep("deposited").finished(); @@ -86,7 +86,7 @@ public WorkflowDef definition() { step(depositAsyncStepName) .asyncCall(() -> { var transfer = currentState().transfer; - return kalixClient.patch("/wallet/" + transfer.to + "/deposit/" + transfer.amount, String.class).execute(); + return componentClient.forValueEntity(transfer.to).call(WalletEntity::deposit).params(transfer.amount).execute(); }) .andThen(String.class, __ -> { var state = currentState().withLastStep("deposited").finished(); diff --git a/sdk/java-sdk-spring/src/it/java/com/example/wiring/workflowentities/WalletEntity.java b/sdk/java-sdk-spring/src/it/java/com/example/wiring/workflowentities/WalletEntity.java index 49acfcdcd7..1575892d81 100644 --- a/sdk/java-sdk-spring/src/it/java/com/example/wiring/workflowentities/WalletEntity.java +++ b/sdk/java-sdk-spring/src/it/java/com/example/wiring/workflowentities/WalletEntity.java @@ -35,8 +35,8 @@ public class WalletEntity extends ValueEntity { private static final Logger logger = LoggerFactory.getLogger(WalletEntity.class); @PostMapping("/create/{amount}") - public Effect create(@PathVariable String id, @PathVariable int amount) { - return effects().updateState(new Wallet(id, amount)).thenReply("Ok"); + public Effect create(@PathVariable int amount) { + return effects().updateState(new Wallet(commandContext().entityId(), amount)).thenReply("Ok"); } @PatchMapping("/withdraw/{amount}") diff --git a/sdk/java-sdk-spring/src/it/java/com/example/wiring/workflowentities/WorkflowWithDefaultRecoverStrategy.java b/sdk/java-sdk-spring/src/it/java/com/example/wiring/workflowentities/WorkflowWithDefaultRecoverStrategy.java index 8c3f35302e..4289bc9790 100644 --- a/sdk/java-sdk-spring/src/it/java/com/example/wiring/workflowentities/WorkflowWithDefaultRecoverStrategy.java +++ b/sdk/java-sdk-spring/src/it/java/com/example/wiring/workflowentities/WorkflowWithDefaultRecoverStrategy.java @@ -20,7 +20,7 @@ import kalix.javasdk.annotations.Id; import kalix.javasdk.annotations.TypeId; import kalix.javasdk.workflow.Workflow; -import kalix.spring.KalixClient; +import kalix.javasdk.client.ComponentClient; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PutMapping; @@ -31,18 +31,18 @@ import static java.time.Duration.ofSeconds; import static kalix.javasdk.workflow.Workflow.RecoverStrategy.maxRetries; -@TypeId("workflow-with-default-recover-strategy") @Id("workflowId") +@TypeId("workflow-with-default-recover-strategy") @RequestMapping("/workflow-with-default-recover-strategy/{workflowId}") public class WorkflowWithDefaultRecoverStrategy extends Workflow { private final String counterStepName = "counter"; private final String counterFailoverStepName = "counter-failover"; - private KalixClient kalixClient; + private ComponentClient componentClient; - public WorkflowWithDefaultRecoverStrategy(KalixClient kalixClient) { - this.kalixClient = kalixClient; + public WorkflowWithDefaultRecoverStrategy(ComponentClient componentClient) { + this.componentClient = componentClient; } @Override @@ -51,7 +51,7 @@ public WorkflowDef definition() { step(counterStepName) .call(() -> { var nextValue = currentState().value() + 1; - return kalixClient.post("/failing-counter/" + currentState().counterId() + "/increase/" + nextValue, Integer.class); + return componentClient.forEventSourcedEntity(currentState().counterId()).call(FailingCounterEntity::increase).params(nextValue); }) .andThen(Integer.class, __ -> effects() .updateState(currentState().asFinished()) diff --git a/sdk/java-sdk-spring/src/it/java/com/example/wiring/workflowentities/WorkflowWithRecoverStrategy.java b/sdk/java-sdk-spring/src/it/java/com/example/wiring/workflowentities/WorkflowWithRecoverStrategy.java index 7abe366b6b..1766f3297f 100644 --- a/sdk/java-sdk-spring/src/it/java/com/example/wiring/workflowentities/WorkflowWithRecoverStrategy.java +++ b/sdk/java-sdk-spring/src/it/java/com/example/wiring/workflowentities/WorkflowWithRecoverStrategy.java @@ -17,10 +17,10 @@ package com.example.wiring.workflowentities; import com.example.wiring.actions.echo.Message; +import kalix.javasdk.client.ComponentClient; import kalix.javasdk.annotations.Id; import kalix.javasdk.annotations.TypeId; import kalix.javasdk.workflow.Workflow; -import kalix.spring.KalixClient; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PutMapping; @@ -31,18 +31,18 @@ import static java.time.Duration.ofSeconds; import static kalix.javasdk.workflow.Workflow.RecoverStrategy.maxRetries; -@TypeId("workflow-with-recover-strategy") @Id("workflowId") +@TypeId("workflow-with-recover-strategy") @RequestMapping("/workflow-with-recover-strategy/{workflowId}") public class WorkflowWithRecoverStrategy extends Workflow { private final String counterStepName = "counter"; private final String counterFailoverStepName = "counter-failover"; - private KalixClient kalixClient; + private ComponentClient componentClient; - public WorkflowWithRecoverStrategy(KalixClient kalixClient) { - this.kalixClient = kalixClient; + public WorkflowWithRecoverStrategy(ComponentClient componentClient) { + this.componentClient = componentClient; } @@ -52,7 +52,7 @@ public WorkflowDef definition() { step(counterStepName) .call(() -> { var nextValue = currentState().value() + 1; - return kalixClient.post("/failing-counter/" + currentState().counterId() + "/increase/" + nextValue, Integer.class); + return componentClient.forEventSourcedEntity(currentState().counterId()).call(FailingCounterEntity::increase).params(nextValue); }) .andThen(Integer.class, __ -> effects() .updateState(currentState().asFinished()) diff --git a/sdk/java-sdk-spring/src/it/java/com/example/wiring/workflowentities/WorkflowWithStepTimeout.java b/sdk/java-sdk-spring/src/it/java/com/example/wiring/workflowentities/WorkflowWithStepTimeout.java index 8c42d2a392..a491092da8 100644 --- a/sdk/java-sdk-spring/src/it/java/com/example/wiring/workflowentities/WorkflowWithStepTimeout.java +++ b/sdk/java-sdk-spring/src/it/java/com/example/wiring/workflowentities/WorkflowWithStepTimeout.java @@ -45,13 +45,6 @@ public class WorkflowWithStepTimeout extends Workflow { private final String counterStepName = "counter"; private final String counterFailoverStepName = "counter-failover"; - private KalixClient kalixClient; - - public WorkflowWithStepTimeout(KalixClient kalixClient) { - this.kalixClient = kalixClient; - } - - public Executor delayedExecutor = CompletableFuture.delayedExecutor(1, TimeUnit.SECONDS); @Override diff --git a/sdk/java-sdk-spring/src/it/java/com/example/wiring/workflowentities/WorkflowWithTimeout.java b/sdk/java-sdk-spring/src/it/java/com/example/wiring/workflowentities/WorkflowWithTimeout.java index a4206c27f7..5a6824627a 100644 --- a/sdk/java-sdk-spring/src/it/java/com/example/wiring/workflowentities/WorkflowWithTimeout.java +++ b/sdk/java-sdk-spring/src/it/java/com/example/wiring/workflowentities/WorkflowWithTimeout.java @@ -17,10 +17,10 @@ package com.example.wiring.workflowentities; import com.example.wiring.actions.echo.Message; +import kalix.javasdk.client.ComponentClient; import kalix.javasdk.annotations.Id; import kalix.javasdk.annotations.TypeId; import kalix.javasdk.workflow.Workflow; -import kalix.spring.KalixClient; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PutMapping; @@ -35,18 +35,18 @@ import static java.time.Duration.ofSeconds; import static kalix.javasdk.workflow.Workflow.RecoverStrategy.maxRetries; -@TypeId("workflow-with-timeout") @Id("workflowId") +@TypeId("workflow-with-timeout") @RequestMapping("/workflow-with-timeout/{workflowId}") public class WorkflowWithTimeout extends Workflow { private final String counterStepName = "counter"; private final String counterFailoverStepName = "counter-failover"; - private KalixClient kalixClient; + private ComponentClient componentClient; - public WorkflowWithTimeout(KalixClient kalixClient) { - this.kalixClient = kalixClient; + public WorkflowWithTimeout(ComponentClient componentClient) { + this.componentClient = componentClient; } @@ -62,7 +62,7 @@ public WorkflowDef definition() { var counterIncFailover = step(counterFailoverStepName) - .call(Integer.class, value -> kalixClient.post("/failing-counter/" + currentState().counterId() + "/increase/" + value, Integer.class)) + .call(Integer.class, value -> componentClient.forEventSourcedEntity(currentState().counterId()).call(FailingCounterEntity::increase).params(value)) .andThen(Integer.class, __ -> effects() .updateState(currentState().asFinished()) diff --git a/sdk/java-sdk-spring/src/it/java/kalix/spring/KalixConfigurationTest.java b/sdk/java-sdk-spring/src/it/java/kalix/spring/KalixConfigurationTest.java index c770573f70..3449050be0 100644 --- a/sdk/java-sdk-spring/src/it/java/kalix/spring/KalixConfigurationTest.java +++ b/sdk/java-sdk-spring/src/it/java/kalix/spring/KalixConfigurationTest.java @@ -17,9 +17,11 @@ package kalix.spring; import kalix.javasdk.JsonSupport; +import kalix.javasdk.client.ComponentClient; import kalix.javasdk.testkit.KalixTestKit; import kalix.spring.boot.KalixConfiguration; import kalix.spring.impl.KalixSpringApplication; +import kalix.spring.impl.WebClientProviderHolder; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; @@ -66,12 +68,19 @@ public WebClient createWebClient(KalixTestKit kalixTestKit) { } @Bean - public KalixTestKit kalixTestKit(KalixTestKit.Settings settings) { + public KalixTestKit kalixTestKit(KalixSpringApplication kalixSpringApplication, KalixTestKit.Settings settings) { logger.info("Starting Kalix TestKit..."); - KalixTestKit kalixTestKit = new KalixTestKit(kalixSpringApplication().kalix(), settings); - + KalixTestKit kalixTestKit = new KalixTestKit(kalixSpringApplication.kalix(), settings); kalixTestKit.start(kalixConfiguration.config()); logger.info("Kalix Proxy running on port: " + kalixTestKit.getPort()); + //when ComponentClient is used in integration test, we must initiate webclient before the first request + WebClientProviderHolder holder = WebClientProviderHolder.get(kalixTestKit.getRunner().system()); + kalixSpringApplication.kalixClient().setWebClient(holder.webClientProvider().localWebClient()); return kalixTestKit; } + + @Bean + public ComponentClient componentClient(KalixSpringApplication kalixSpringApplication) { + return kalixSpringApplication.componentClient(); + } } diff --git a/sdk/java-sdk-spring/src/main/java/kalix/javasdk/client/ActionCallBuilder.java b/sdk/java-sdk-spring/src/main/java/kalix/javasdk/client/ActionCallBuilder.java new file mode 100644 index 0000000000..4f0c19499c --- /dev/null +++ b/sdk/java-sdk-spring/src/main/java/kalix/javasdk/client/ActionCallBuilder.java @@ -0,0 +1,209 @@ +/* + * Copyright 2021 Lightbend Inc. + * + * 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 + * + * 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. + */ + +package kalix.javasdk.client; + +import akka.japi.function.Function; +import akka.japi.function.Function10; +import akka.japi.function.Function11; +import akka.japi.function.Function12; +import akka.japi.function.Function13; +import akka.japi.function.Function14; +import akka.japi.function.Function15; +import akka.japi.function.Function16; +import akka.japi.function.Function17; +import akka.japi.function.Function18; +import akka.japi.function.Function19; +import akka.japi.function.Function2; +import akka.japi.function.Function20; +import akka.japi.function.Function21; +import akka.japi.function.Function22; +import akka.japi.function.Function3; +import akka.japi.function.Function4; +import akka.japi.function.Function5; +import akka.japi.function.Function6; +import akka.japi.function.Function7; +import akka.japi.function.Function8; +import akka.japi.function.Function9; +import com.google.protobuf.any.Any; +import kalix.javasdk.DeferredCall; +import kalix.javasdk.action.Action; +import kalix.spring.KalixClient; + +import java.util.Optional; + +public class ActionCallBuilder { + + private final KalixClient kalixClient; + + public ActionCallBuilder(KalixClient kalixClient) { + this.kalixClient = kalixClient; + } + + /** + * Pass in Action method reference annotated as a REST endpoint, e.g. MyAction::create + */ + public DeferredCall call(Function> methodRef) { + return ComponentCall.noParams(kalixClient, methodRef, Optional.empty()); + } + + /** + * Pass in Action method reference annotated as a REST endpoint, e.g. MyAction::create + */ + public ComponentCall call(Function2> methodRef) { + return new ComponentCall<>(kalixClient, methodRef, Optional.empty()); + } + + /** + * Pass in Action method reference annotated as a REST endpoint, e.g. MyAction::create + */ + public ComponentCall2 call(Function3> methodRef) { + return new ComponentCall2<>(kalixClient, methodRef, Optional.empty()); + } + + /** + * Pass in Action method reference annotated as a REST endpoint, e.g. MyAction::create + */ + public ComponentCall3 call(Function4> methodRef) { + return new ComponentCall3<>(kalixClient, methodRef, Optional.empty()); + } + + /** + * Pass in Action method reference annotated as a REST endpoint, e.g. MyAction::create + */ + public ComponentCall4 call(Function5> methodRef) { + return new ComponentCall4<>(kalixClient, methodRef, Optional.empty()); + } + + /** + * Pass in Action method reference annotated as a REST endpoint, e.g. MyAction::create + */ + public ComponentCall5 call(Function6> methodRef) { + return new ComponentCall5<>(kalixClient, methodRef, Optional.empty()); + } + + /** + * Pass in Action method reference annotated as a REST endpoint, e.g. MyAction::create + */ + public ComponentCall6 call(Function7> methodRef) { + return new ComponentCall6<>(kalixClient, methodRef, Optional.empty()); + } + + /** + * Pass in Action method reference annotated as a REST endpoint, e.g. MyAction::create + */ + public ComponentCall7 call(Function8> methodRef) { + return new ComponentCall7<>(kalixClient, methodRef, Optional.empty()); + } + + /** + * Pass in Action method reference annotated as a REST endpoint, e.g. MyAction::create + */ + public ComponentCall8 call(Function9> methodRef) { + return new ComponentCall8<>(kalixClient, methodRef, Optional.empty()); + } + + /** + * Pass in Action method reference annotated as a REST endpoint, e.g. MyAction::create + */ + public ComponentCall9 call(Function10> methodRef) { + return new ComponentCall9<>(kalixClient, methodRef, Optional.empty()); + } + + /** + * Pass in Action method reference annotated as a REST endpoint, e.g. MyAction::create + */ + public ComponentCall10 call(Function11> methodRef) { + return new ComponentCall10<>(kalixClient, methodRef, Optional.empty()); + } + + /** + * Pass in Action method reference annotated as a REST endpoint, e.g. MyAction::create + */ + public ComponentCall11 call(Function12> methodRef) { + return new ComponentCall11<>(kalixClient, methodRef, Optional.empty()); + } + + /** + * Pass in Action method reference annotated as a REST endpoint, e.g. MyAction::create + */ + public ComponentCall12 call(Function13> methodRef) { + return new ComponentCall12<>(kalixClient, methodRef, Optional.empty()); + } + + /** + * Pass in Action method reference annotated as a REST endpoint, e.g. MyAction::create + */ + public ComponentCall13 call(Function14> methodRef) { + return new ComponentCall13<>(kalixClient, methodRef, Optional.empty()); + } + + /** + * Pass in Action method reference annotated as a REST endpoint, e.g. MyAction::create + */ + public ComponentCall14 call(Function15> methodRef) { + return new ComponentCall14<>(kalixClient, methodRef, Optional.empty()); + } + + /** + * Pass in Action method reference annotated as a REST endpoint, e.g. MyAction::create + */ + public ComponentCall15 call(Function16> methodRef) { + return new ComponentCall15<>(kalixClient, methodRef, Optional.empty()); + } + + /** + * Pass in Action method reference annotated as a REST endpoint, e.g. MyAction::create + */ + public ComponentCall16 call(Function17> methodRef) { + return new ComponentCall16<>(kalixClient, methodRef, Optional.empty()); + } + + /** + * Pass in Action method reference annotated as a REST endpoint, e.g. MyAction::create + */ + public ComponentCall17 call(Function18> methodRef) { + return new ComponentCall17<>(kalixClient, methodRef, Optional.empty()); + } + + /** + * Pass in Action method reference annotated as a REST endpoint, e.g. MyAction::create + */ + public ComponentCall18 call(Function19> methodRef) { + return new ComponentCall18<>(kalixClient, methodRef, Optional.empty()); + } + + /** + * Pass in Action method reference annotated as a REST endpoint, e.g. MyAction::create + */ + public ComponentCall19 call(Function20> methodRef) { + return new ComponentCall19<>(kalixClient, methodRef, Optional.empty()); + } + + /** + * Pass in Action method reference annotated as a REST endpoint, e.g. MyAction::create + */ + public ComponentCall20 call(Function21> methodRef) { + return new ComponentCall20<>(kalixClient, methodRef, Optional.empty()); + } + + /** + * Pass in Action method reference annotated as a REST endpoint, e.g. MyAction::create + */ + public ComponentCall21 call(Function22> methodRef) { + return new ComponentCall21<>(kalixClient, methodRef, Optional.empty()); + } +} diff --git a/sdk/java-sdk-spring/src/main/java/kalix/javasdk/client/ComponentClient.java b/sdk/java-sdk-spring/src/main/java/kalix/javasdk/client/ComponentClient.java new file mode 100644 index 0000000000..a2ef7ec2be --- /dev/null +++ b/sdk/java-sdk-spring/src/main/java/kalix/javasdk/client/ComponentClient.java @@ -0,0 +1,105 @@ +/* + * Copyright 2021 Lightbend Inc. + * + * 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 + * + * 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. + */ + +package kalix.javasdk.client; + +import kalix.spring.KalixClient; + +/** + * Utility to send requests to other Kalix components by composing a DeferredCall. To compose a call: + * 1. select component type (and pass id if necessary) + * 2. select component method, by using Java method reference operator (::) + * 3. provide parameters (if required) + * + *

+ * Example of use on a cross-component call: + *

{@code
+ * public Effect createUser(String userId, String email, String name) {
+ *   //validation here
+ *   var defCall = componentClient.forValueEntity(userId).call(UserEntity::createUser).params(email, name);
+ *   return effects().forward(defCall);
+ * }
+ * }
+ */ +public class ComponentClient { + + private final KalixClient kalixClient; + + public ComponentClient(KalixClient kalixClient) { + this.kalixClient = kalixClient; + } + + /** + * Select Action as a call target component. + */ + public ActionCallBuilder forAction() { + return new ActionCallBuilder(kalixClient); + } + + /** + * Select ValueEntity as a call target component. + *

+ * For calling methods annotated with @{@link kalix.javasdk.annotations.GenerateId} + */ + public ValueEntityCallBuilder forValueEntity() { + return new ValueEntityCallBuilder(kalixClient); + } + + /** + * Select ValueEntity as a call target component. + * + * @param valueEntityId - value entity id used to create a call. + */ + public ValueEntityCallBuilder forValueEntity(String valueEntityId) { + return new ValueEntityCallBuilder(kalixClient, valueEntityId); + } + + /** + * Select EventSourcedEntity as a call target component. + *

+ * For calling methods annotated with @{@link kalix.javasdk.annotations.GenerateId} + */ + public EventSourcedEntityCallBuilder forEventSourcedEntity() { + return new EventSourcedEntityCallBuilder(kalixClient); + } + + /** + * Select EventSourcedEntity as a call target component. + * + * @param eventSourcedEntityId - event sourced entity id used to create a call. + */ + public EventSourcedEntityCallBuilder forEventSourcedEntity(String eventSourcedEntityId) { + return new EventSourcedEntityCallBuilder(kalixClient, eventSourcedEntityId); + } + + /** + * Select Workflow as a call target component. + *

+ * For calling methods annotated with @{@link kalix.javasdk.annotations.GenerateId} + */ + public WorkflowCallBuilder forWorkflow() { + return new WorkflowCallBuilder(kalixClient); + } + + /** + * Select Workflow as a call target component. + * + * @param workflowId - workflow id used to create a call. + */ + public WorkflowCallBuilder forWorkflow(String workflowId) { + return new WorkflowCallBuilder(kalixClient, workflowId); + } +} diff --git a/sdk/java-sdk-spring/src/main/java/kalix/javasdk/client/EventSourcedEntityCallBuilder.java b/sdk/java-sdk-spring/src/main/java/kalix/javasdk/client/EventSourcedEntityCallBuilder.java new file mode 100644 index 0000000000..a4cd976722 --- /dev/null +++ b/sdk/java-sdk-spring/src/main/java/kalix/javasdk/client/EventSourcedEntityCallBuilder.java @@ -0,0 +1,216 @@ +/* + * Copyright 2021 Lightbend Inc. + * + * 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 + * + * 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. + */ + +package kalix.javasdk.client; + +import akka.japi.function.Function; +import akka.japi.function.Function10; +import akka.japi.function.Function11; +import akka.japi.function.Function12; +import akka.japi.function.Function13; +import akka.japi.function.Function14; +import akka.japi.function.Function15; +import akka.japi.function.Function16; +import akka.japi.function.Function17; +import akka.japi.function.Function18; +import akka.japi.function.Function19; +import akka.japi.function.Function2; +import akka.japi.function.Function20; +import akka.japi.function.Function21; +import akka.japi.function.Function22; +import akka.japi.function.Function3; +import akka.japi.function.Function4; +import akka.japi.function.Function5; +import akka.japi.function.Function6; +import akka.japi.function.Function7; +import akka.japi.function.Function8; +import akka.japi.function.Function9; +import com.google.protobuf.any.Any; +import kalix.javasdk.DeferredCall; +import kalix.javasdk.eventsourcedentity.EventSourcedEntity; +import kalix.spring.KalixClient; + +import java.util.Optional; + +public class EventSourcedEntityCallBuilder { + + private final KalixClient kalixClient; + private final Optional entityId; + + public EventSourcedEntityCallBuilder(KalixClient kalixClient, String entityId) { + this.kalixClient = kalixClient; + this.entityId = Optional.of(entityId); + } + + public EventSourcedEntityCallBuilder(KalixClient kalixClient) { + this.kalixClient = kalixClient; + this.entityId = Optional.empty(); + } + + /** + * Pass in Event Sourced Entity method reference annotated as a REST endpoint, e.g. UserEntity::create + */ + public DeferredCall call(Function> methodRef) { + return ComponentCall.noParams(kalixClient, methodRef, entityId); + } + + /** + * Pass in Event Sourced Entity method reference annotated as a REST endpoint, e.g. UserEntity::create + */ + public ComponentCall call(Function2> methodRef) { + return new ComponentCall<>(kalixClient, methodRef, entityId); + } + + /** + * Pass in Event Sourced Entity method reference annotated as a REST endpoint, e.g. UserEntity::create + */ + public ComponentCall2 call(Function3> methodRef) { + return new ComponentCall2<>(kalixClient, methodRef, entityId); + } + + /** + * Pass in Event Sourced Entity method reference annotated as a REST endpoint, e.g. UserEntity::create + */ + public ComponentCall3 call(Function4> methodRef) { + return new ComponentCall3<>(kalixClient, methodRef, Optional.empty()); + } + + /** + * Pass in Event Sourced Entity method reference annotated as a REST endpoint, e.g. UserEntity::create + */ + public ComponentCall4 call(Function5> methodRef) { + return new ComponentCall4<>(kalixClient, methodRef, Optional.empty()); + } + + /** + * Pass in Event Sourced Entity method reference annotated as a REST endpoint, e.g. UserEntity::create + */ + public ComponentCall5 call(Function6> methodRef) { + return new ComponentCall5<>(kalixClient, methodRef, Optional.empty()); + } + + /** + * Pass in Event Sourced Entity method reference annotated as a REST endpoint, e.g. UserEntity::create + */ + public ComponentCall6 call(Function7> methodRef) { + return new ComponentCall6<>(kalixClient, methodRef, Optional.empty()); + } + + /** + * Pass in Event Sourced Entity method reference annotated as a REST endpoint, e.g. UserEntity::create + */ + public ComponentCall7 call(Function8> methodRef) { + return new ComponentCall7<>(kalixClient, methodRef, Optional.empty()); + } + + /** + * Pass in Event Sourced Entity method reference annotated as a REST endpoint, e.g. UserEntity::create + */ + public ComponentCall8 call(Function9> methodRef) { + return new ComponentCall8<>(kalixClient, methodRef, Optional.empty()); + } + + /** + * Pass in Event Sourced Entity method reference annotated as a REST endpoint, e.g. UserEntity::create + */ + public ComponentCall9 call(Function10> methodRef) { + return new ComponentCall9<>(kalixClient, methodRef, Optional.empty()); + } + + /** + * Pass in Event Sourced Entity method reference annotated as a REST endpoint, e.g. UserEntity::create + */ + public ComponentCall10 call(Function11> methodRef) { + return new ComponentCall10<>(kalixClient, methodRef, Optional.empty()); + } + + /** + * Pass in Event Sourced Entity method reference annotated as a REST endpoint, e.g. UserEntity::create + */ + public ComponentCall11 call(Function12> methodRef) { + return new ComponentCall11<>(kalixClient, methodRef, Optional.empty()); + } + + /** + * Pass in Event Sourced Entity method reference annotated as a REST endpoint, e.g. UserEntity::create + */ + public ComponentCall12 call(Function13> methodRef) { + return new ComponentCall12<>(kalixClient, methodRef, Optional.empty()); + } + + /** + * Pass in Event Sourced Entity method reference annotated as a REST endpoint, e.g. UserEntity::create + */ + public ComponentCall13 call(Function14> methodRef) { + return new ComponentCall13<>(kalixClient, methodRef, Optional.empty()); + } + + /** + * Pass in Event Sourced Entity method reference annotated as a REST endpoint, e.g. UserEntity::create + */ + public ComponentCall14 call(Function15> methodRef) { + return new ComponentCall14<>(kalixClient, methodRef, Optional.empty()); + } + + /** + * Pass in Event Sourced Entity method reference annotated as a REST endpoint, e.g. UserEntity::create + */ + public ComponentCall15 call(Function16> methodRef) { + return new ComponentCall15<>(kalixClient, methodRef, Optional.empty()); + } + + /** + * Pass in Event Sourced Entity method reference annotated as a REST endpoint, e.g. UserEntity::create + */ + public ComponentCall16 call(Function17> methodRef) { + return new ComponentCall16<>(kalixClient, methodRef, Optional.empty()); + } + + /** + * Pass in Event Sourced Entity method reference annotated as a REST endpoint, e.g. UserEntity::create + */ + public ComponentCall17 call(Function18> methodRef) { + return new ComponentCall17<>(kalixClient, methodRef, Optional.empty()); + } + + /** + * Pass in Event Sourced Entity method reference annotated as a REST endpoint, e.g. UserEntity::create + */ + public ComponentCall18 call(Function19> methodRef) { + return new ComponentCall18<>(kalixClient, methodRef, Optional.empty()); + } + + /** + * Pass in Event Sourced Entity method reference annotated as a REST endpoint, e.g. UserEntity::create + */ + public ComponentCall19 call(Function20> methodRef) { + return new ComponentCall19<>(kalixClient, methodRef, Optional.empty()); + } + + /** + * Pass in Event Sourced Entity method reference annotated as a REST endpoint, e.g. UserEntity::create + */ + public ComponentCall20 call(Function21> methodRef) { + return new ComponentCall20<>(kalixClient, methodRef, Optional.empty()); + } + + /** + * Pass in Event Sourced Entity method reference annotated as a REST endpoint, e.g. UserEntity::create + */ + public ComponentCall21 call(Function22> methodRef) { + return new ComponentCall21<>(kalixClient, methodRef, Optional.empty()); + } +} diff --git a/sdk/java-sdk-spring/src/main/java/kalix/javasdk/client/ValueEntityCallBuilder.java b/sdk/java-sdk-spring/src/main/java/kalix/javasdk/client/ValueEntityCallBuilder.java new file mode 100644 index 0000000000..ad76a40019 --- /dev/null +++ b/sdk/java-sdk-spring/src/main/java/kalix/javasdk/client/ValueEntityCallBuilder.java @@ -0,0 +1,216 @@ +/* + * Copyright 2021 Lightbend Inc. + * + * 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 + * + * 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. + */ + +package kalix.javasdk.client; + +import akka.japi.function.Function; +import akka.japi.function.Function10; +import akka.japi.function.Function11; +import akka.japi.function.Function12; +import akka.japi.function.Function13; +import akka.japi.function.Function14; +import akka.japi.function.Function15; +import akka.japi.function.Function16; +import akka.japi.function.Function17; +import akka.japi.function.Function18; +import akka.japi.function.Function19; +import akka.japi.function.Function2; +import akka.japi.function.Function20; +import akka.japi.function.Function21; +import akka.japi.function.Function22; +import akka.japi.function.Function3; +import akka.japi.function.Function4; +import akka.japi.function.Function5; +import akka.japi.function.Function6; +import akka.japi.function.Function7; +import akka.japi.function.Function8; +import akka.japi.function.Function9; +import com.google.protobuf.any.Any; +import kalix.javasdk.DeferredCall; +import kalix.javasdk.valueentity.ValueEntity; +import kalix.spring.KalixClient; + +import java.util.Optional; + +public class ValueEntityCallBuilder { + + private final KalixClient kalixClient; + private final Optional entityId; + + public ValueEntityCallBuilder(KalixClient kalixClient, String entityId) { + this.kalixClient = kalixClient; + this.entityId = Optional.of(entityId); + } + + public ValueEntityCallBuilder(KalixClient kalixClient) { + this.kalixClient = kalixClient; + this.entityId = Optional.empty(); + } + + /** + * Pass in Value Entity method reference annotated as a REST endpoint, e.g. UserEntity::create + */ + public DeferredCall call(Function> methodRef) { + return ComponentCall.noParams(kalixClient, methodRef, entityId); + } + + /** + * Pass in Value Entity method reference annotated as a REST endpoint, e.g. UserEntity::create + */ + public ComponentCall call(Function2> methodRef) { + return new ComponentCall<>(kalixClient, methodRef, entityId); + } + + /** + * Pass in Value Entity method reference annotated as a REST endpoint, e.g. UserEntity::create + */ + public ComponentCall2 call(Function3> methodRef) { + return new ComponentCall2<>(kalixClient, methodRef, entityId); + } + + /** + * Pass in Value Entity method reference annotated as a REST endpoint, e.g. UserEntity::create + */ + public ComponentCall3 call(Function4> methodRef) { + return new ComponentCall3<>(kalixClient, methodRef, entityId); + } + + /** + * Pass in Value Entity method reference annotated as a REST endpoint, e.g. UserEntity::create + */ + public ComponentCall4 call(Function5> methodRef) { + return new ComponentCall4<>(kalixClient, methodRef, Optional.empty()); + } + + /** + * Pass in Value Entity method reference annotated as a REST endpoint, e.g. UserEntity::create + */ + public ComponentCall5 call(Function6> methodRef) { + return new ComponentCall5<>(kalixClient, methodRef, Optional.empty()); + } + + /** + * Pass in Value Entity method reference annotated as a REST endpoint, e.g. UserEntity::create + */ + public ComponentCall6 call(Function7> methodRef) { + return new ComponentCall6<>(kalixClient, methodRef, Optional.empty()); + } + + /** + * Pass in Value Entity method reference annotated as a REST endpoint, e.g. UserEntity::create + */ + public ComponentCall7 call(Function8> methodRef) { + return new ComponentCall7<>(kalixClient, methodRef, Optional.empty()); + } + + /** + * Pass in Value Entity method reference annotated as a REST endpoint, e.g. UserEntity::create + */ + public ComponentCall8 call(Function9> methodRef) { + return new ComponentCall8<>(kalixClient, methodRef, Optional.empty()); + } + + /** + * Pass in Value Entity method reference annotated as a REST endpoint, e.g. UserEntity::create + */ + public ComponentCall9 call(Function10> methodRef) { + return new ComponentCall9<>(kalixClient, methodRef, Optional.empty()); + } + + /** + * Pass in Value Entity method reference annotated as a REST endpoint, e.g. UserEntity::create + */ + public ComponentCall10 call(Function11> methodRef) { + return new ComponentCall10<>(kalixClient, methodRef, Optional.empty()); + } + + /** + * Pass in Value Entity method reference annotated as a REST endpoint, e.g. UserEntity::create + */ + public ComponentCall11 call(Function12> methodRef) { + return new ComponentCall11<>(kalixClient, methodRef, Optional.empty()); + } + + /** + * Pass in Value Entity method reference annotated as a REST endpoint, e.g. UserEntity::create + */ + public ComponentCall12 call(Function13> methodRef) { + return new ComponentCall12<>(kalixClient, methodRef, Optional.empty()); + } + + /** + * Pass in Value Entity method reference annotated as a REST endpoint, e.g. UserEntity::create + */ + public ComponentCall13 call(Function14> methodRef) { + return new ComponentCall13<>(kalixClient, methodRef, Optional.empty()); + } + + /** + * Pass in Value Entity method reference annotated as a REST endpoint, e.g. UserEntity::create + */ + public ComponentCall14 call(Function15> methodRef) { + return new ComponentCall14<>(kalixClient, methodRef, Optional.empty()); + } + + /** + * Pass in Value Entity method reference annotated as a REST endpoint, e.g. UserEntity::create + */ + public ComponentCall15 call(Function16> methodRef) { + return new ComponentCall15<>(kalixClient, methodRef, Optional.empty()); + } + + /** + * Pass in Value Entity method reference annotated as a REST endpoint, e.g. UserEntity::create + */ + public ComponentCall16 call(Function17> methodRef) { + return new ComponentCall16<>(kalixClient, methodRef, Optional.empty()); + } + + /** + * Pass in Value Entity method reference annotated as a REST endpoint, e.g. UserEntity::create + */ + public ComponentCall17 call(Function18> methodRef) { + return new ComponentCall17<>(kalixClient, methodRef, Optional.empty()); + } + + /** + * Pass in Value Entity method reference annotated as a REST endpoint, e.g. UserEntity::create + */ + public ComponentCall18 call(Function19> methodRef) { + return new ComponentCall18<>(kalixClient, methodRef, Optional.empty()); + } + + /** + * Pass in Value Entity method reference annotated as a REST endpoint, e.g. UserEntity::create + */ + public ComponentCall19 call(Function20> methodRef) { + return new ComponentCall19<>(kalixClient, methodRef, Optional.empty()); + } + + /** + * Pass in Value Entity method reference annotated as a REST endpoint, e.g. UserEntity::create + */ + public ComponentCall20 call(Function21> methodRef) { + return new ComponentCall20<>(kalixClient, methodRef, Optional.empty()); + } + + /** + * Pass in Value Entity method reference annotated as a REST endpoint, e.g. UserEntity::create + */ + public ComponentCall21 call(Function22> methodRef) { + return new ComponentCall21<>(kalixClient, methodRef, Optional.empty()); + } +} diff --git a/sdk/java-sdk-spring/src/main/java/kalix/javasdk/client/WorkflowCallBuilder.java b/sdk/java-sdk-spring/src/main/java/kalix/javasdk/client/WorkflowCallBuilder.java new file mode 100644 index 0000000000..8c7a5fa18d --- /dev/null +++ b/sdk/java-sdk-spring/src/main/java/kalix/javasdk/client/WorkflowCallBuilder.java @@ -0,0 +1,216 @@ +/* + * Copyright 2021 Lightbend Inc. + * + * 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 + * + * 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. + */ + +package kalix.javasdk.client; + +import akka.japi.function.Function; +import akka.japi.function.Function10; +import akka.japi.function.Function11; +import akka.japi.function.Function12; +import akka.japi.function.Function13; +import akka.japi.function.Function14; +import akka.japi.function.Function15; +import akka.japi.function.Function16; +import akka.japi.function.Function17; +import akka.japi.function.Function18; +import akka.japi.function.Function19; +import akka.japi.function.Function2; +import akka.japi.function.Function20; +import akka.japi.function.Function21; +import akka.japi.function.Function22; +import akka.japi.function.Function3; +import akka.japi.function.Function4; +import akka.japi.function.Function5; +import akka.japi.function.Function6; +import akka.japi.function.Function7; +import akka.japi.function.Function8; +import akka.japi.function.Function9; +import com.google.protobuf.any.Any; +import kalix.javasdk.DeferredCall; +import kalix.javasdk.workflow.Workflow; +import kalix.spring.KalixClient; + +import java.util.Optional; + +public class WorkflowCallBuilder { + + private final KalixClient kalixClient; + private final Optional workflowId; + + public WorkflowCallBuilder(KalixClient kalixClient, String workflowId) { + this.kalixClient = kalixClient; + this.workflowId = Optional.of(workflowId); + } + + public WorkflowCallBuilder(KalixClient kalixClient) { + this.kalixClient = kalixClient; + this.workflowId = Optional.empty(); + } + + /** + * Pass in Workflow method reference annotated as a REST endpoint, e.g. MyWorkflow::start + */ + public DeferredCall call(Function> methodRef) { + return ComponentCall.noParams(kalixClient, methodRef, workflowId); + } + + /** + * Pass in Workflow method reference annotated as a REST endpoint, e.g. MyWorkflow::start + */ + public ComponentCall call(Function2> methodRef) { + return new ComponentCall<>(kalixClient, methodRef, workflowId); + } + + /** + * Pass in Workflow method reference annotated as a REST endpoint, e.g. MyWorkflow::start + */ + public ComponentCall2 call(Function3> methodRef) { + return new ComponentCall2<>(kalixClient, methodRef, workflowId); + } + + /** + * Pass in Workflow method reference annotated as a REST endpoint, e.g. MyWorkflow::start + */ + public ComponentCall3 call(Function4> methodRef) { + return new ComponentCall3<>(kalixClient, methodRef, Optional.empty()); + } + + /** + * Pass in Workflow method reference annotated as a REST endpoint, e.g. MyWorkflow::start + */ + public ComponentCall4 call(Function5> methodRef) { + return new ComponentCall4<>(kalixClient, methodRef, Optional.empty()); + } + + /** + * Pass in Workflow method reference annotated as a REST endpoint, e.g. MyWorkflow::start + */ + public ComponentCall5 call(Function6> methodRef) { + return new ComponentCall5<>(kalixClient, methodRef, Optional.empty()); + } + + /** + * Pass in Workflow method reference annotated as a REST endpoint, e.g. MyWorkflow::start + */ + public ComponentCall6 call(Function7> methodRef) { + return new ComponentCall6<>(kalixClient, methodRef, Optional.empty()); + } + + /** + * Pass in Workflow method reference annotated as a REST endpoint, e.g. MyWorkflow::start + */ + public ComponentCall7 call(Function8> methodRef) { + return new ComponentCall7<>(kalixClient, methodRef, Optional.empty()); + } + + /** + * Pass in Workflow method reference annotated as a REST endpoint, e.g. MyWorkflow::start + */ + public ComponentCall8 call(Function9> methodRef) { + return new ComponentCall8<>(kalixClient, methodRef, Optional.empty()); + } + + /** + * Pass in Workflow method reference annotated as a REST endpoint, e.g. MyWorkflow::start + */ + public ComponentCall9 call(Function10> methodRef) { + return new ComponentCall9<>(kalixClient, methodRef, Optional.empty()); + } + + /** + * Pass in Workflow method reference annotated as a REST endpoint, e.g. MyWorkflow::start + */ + public ComponentCall10 call(Function11> methodRef) { + return new ComponentCall10<>(kalixClient, methodRef, Optional.empty()); + } + + /** + * Pass in Workflow method reference annotated as a REST endpoint, e.g. MyWorkflow::start + */ + public ComponentCall11 call(Function12> methodRef) { + return new ComponentCall11<>(kalixClient, methodRef, Optional.empty()); + } + + /** + * Pass in Workflow method reference annotated as a REST endpoint, e.g. MyWorkflow::start + */ + public ComponentCall12 call(Function13> methodRef) { + return new ComponentCall12<>(kalixClient, methodRef, Optional.empty()); + } + + /** + * Pass in Workflow method reference annotated as a REST endpoint, e.g. MyWorkflow::start + */ + public ComponentCall13 call(Function14> methodRef) { + return new ComponentCall13<>(kalixClient, methodRef, Optional.empty()); + } + + /** + * Pass in Workflow method reference annotated as a REST endpoint, e.g. MyWorkflow::start + */ + public ComponentCall14 call(Function15> methodRef) { + return new ComponentCall14<>(kalixClient, methodRef, Optional.empty()); + } + + /** + * Pass in Workflow method reference annotated as a REST endpoint, e.g. MyWorkflow::start + */ + public ComponentCall15 call(Function16> methodRef) { + return new ComponentCall15<>(kalixClient, methodRef, Optional.empty()); + } + + /** + * Pass in Workflow method reference annotated as a REST endpoint, e.g. MyWorkflow::start + */ + public ComponentCall16 call(Function17> methodRef) { + return new ComponentCall16<>(kalixClient, methodRef, Optional.empty()); + } + + /** + * Pass in Workflow method reference annotated as a REST endpoint, e.g. MyWorkflow::start + */ + public ComponentCall17 call(Function18> methodRef) { + return new ComponentCall17<>(kalixClient, methodRef, Optional.empty()); + } + + /** + * Pass in Workflow method reference annotated as a REST endpoint, e.g. MyWorkflow::start + */ + public ComponentCall18 call(Function19> methodRef) { + return new ComponentCall18<>(kalixClient, methodRef, Optional.empty()); + } + + /** + * Pass in Workflow method reference annotated as a REST endpoint, e.g. MyWorkflow::start + */ + public ComponentCall19 call(Function20> methodRef) { + return new ComponentCall19<>(kalixClient, methodRef, Optional.empty()); + } + + /** + * Pass in Workflow method reference annotated as a REST endpoint, e.g. MyWorkflow::start + */ + public ComponentCall20 call(Function21> methodRef) { + return new ComponentCall20<>(kalixClient, methodRef, Optional.empty()); + } + + /** + * Pass in Workflow method reference annotated as a REST endpoint, e.g. MyWorkflow::start + */ + public ComponentCall21 call(Function22> methodRef) { + return new ComponentCall21<>(kalixClient, methodRef, Optional.empty()); + } +} diff --git a/sdk/java-sdk-spring/src/main/scala/kalix/javasdk/client/ComponentCall.scala b/sdk/java-sdk-spring/src/main/scala/kalix/javasdk/client/ComponentCall.scala new file mode 100644 index 0000000000..ba59c99f81 --- /dev/null +++ b/sdk/java-sdk-spring/src/main/scala/kalix/javasdk/client/ComponentCall.scala @@ -0,0 +1,393 @@ +/* + * Copyright 2021 Lightbend Inc. + * + * 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 + * + * 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. + */ + +package kalix.javasdk.client + +import java.lang.reflect.Method +import java.lang.reflect.ParameterizedType +import java.util +import java.util.Optional + +import scala.jdk.OptionConverters._ + +import akka.http.scaladsl.model.HttpMethods +import com.google.protobuf.any.Any +import kalix.javasdk.DeferredCall +import kalix.javasdk.annotations.EntityType +import kalix.javasdk.annotations.TypeId +import kalix.javasdk.impl.client.MethodRefResolver +import kalix.javasdk.impl.reflection.IdExtractor +import kalix.javasdk.impl.reflection.RestServiceIntrospector +import kalix.javasdk.impl.reflection.RestServiceIntrospector.BodyParameter +import kalix.javasdk.impl.reflection.RestServiceIntrospector.PathParameter +import kalix.javasdk.impl.reflection.RestServiceIntrospector.QueryParamParameter +import kalix.javasdk.impl.reflection.RestServiceIntrospector.RestService +import kalix.javasdk.impl.reflection.SyntheticRequestServiceMethod +import kalix.spring.KalixClient +import kalix.spring.impl.RestKalixClientImpl +import org.springframework.web.bind.annotation.RequestMethod + +final class ComponentCall[A1, R](kalixClient: KalixClient, lambda: scala.Any, id: Optional[String]) { + def params(a1: A1): DeferredCall[Any, R] = { + ComponentCall.invoke(Seq(a1), kalixClient, lambda, id.toScala) + } +} + +object ComponentCall { + + def noParams[R](kalixClient: KalixClient, lambda: scala.Any, id: Optional[String]): DeferredCall[Any, R] = { + invoke(Seq.empty, kalixClient, lambda, id.toScala) + } + + private[client] def invoke[R]( + params: Seq[scala.Any], + kalixClient: KalixClient, + lambda: scala.Any, + id: Option[String]): DeferredCall[Any, R] = { + + val method = MethodRefResolver.resolveMethodRef(lambda) + val declaringClass = method.getDeclaringClass + + val returnType = + method.getGenericReturnType.asInstanceOf[ParameterizedType].getActualTypeArguments.head.asInstanceOf[Class[R]] + + val restService: RestService = RestServiceIntrospector.inspectService(declaringClass) + val restMethod: SyntheticRequestServiceMethod = + restService.methods.find(_.javaMethod.getName == method.getName) match { + case Some(method) => method + case None => throw new IllegalStateException(s"Method [${method.getName}] is not annotated as a REST endpoint.") + } + + val requestMethod: RequestMethod = restMethod.requestMethod + + val queryParams: Map[String, util.List[scala.Any]] = restMethod.params + .collect { case p: QueryParamParameter => p } + .map(p => (p.name, getQueryParam(params, p.param.getParameterIndex))) + .toMap + + val pathVariables: Map[String, ?] = restMethod.params + .collect { case p: PathParameter => p } + .map(p => (p.name, getPathParam(params, p.param.getParameterIndex, p.name))) + .toMap ++ idVariables(id, method) + + val bodyIndex = restMethod.params.collect { case p: BodyParameter => p }.map(_.param.getParameterIndex).headOption + val body = bodyIndex.map(params(_)) + + val kalixClientImpl = kalixClient.asInstanceOf[RestKalixClientImpl] + + val pathTemplate = restMethod.parsedPath.path + + requestMethod match { + case RequestMethod.GET => + kalixClientImpl.runWithoutBody(HttpMethods.GET, pathTemplate, pathVariables, queryParams, returnType) + case RequestMethod.HEAD => notSupported(requestMethod, pathTemplate) + case RequestMethod.POST => + kalixClientImpl.runWithBody(HttpMethods.POST, pathTemplate, pathVariables, queryParams, body, returnType) + case RequestMethod.PUT => + kalixClientImpl.runWithBody(HttpMethods.PUT, pathTemplate, pathVariables, queryParams, body, returnType) + case RequestMethod.PATCH => + kalixClientImpl.runWithBody(HttpMethods.PATCH, pathTemplate, pathVariables, queryParams, body, returnType) + case RequestMethod.DELETE => + kalixClientImpl.runWithoutBody(HttpMethods.DELETE, pathTemplate, pathVariables, queryParams, returnType) + case RequestMethod.OPTIONS => notSupported(requestMethod, pathTemplate) + case RequestMethod.TRACE => notSupported(requestMethod, pathTemplate) + } + } + + private def getQueryParam(params: Seq[scala.Any], parameterIndex: Int): util.List[scala.Any] = { + val value = params(parameterIndex) + if (value == null) { + util.List.of() + } else if (value.isInstanceOf[util.List[_]]) { + value.asInstanceOf[util.List[scala.Any]] + } else { + util.List.of(value) + } + } + + private def getPathParam(params: Seq[scala.Any], parameterIndex: Int, paramName: String): scala.Any = { + val value = params(parameterIndex) + if (value == null) { + throw new IllegalStateException(s"Path param [$paramName] cannot be null.") + } + value + } + + private def notSupported(requestMethod: RequestMethod, pathTemplate: String) = { + throw new IllegalStateException(s"HTTP $requestMethod not supported when calling $pathTemplate") + } + + private def idVariables(id: Option[String], method: Method): Map[String, String] = { + + val declaringClass = method.getDeclaringClass + if (declaringClass.getAnnotation(classOf[EntityType]) == null && + declaringClass.getAnnotation(classOf[TypeId]) == null) { + //not an entity or workflows + Map.empty + } else if (IdExtractor.shouldGenerateId(method)) { + Map.empty + } else { + val idNames = IdExtractor.extractIds(declaringClass, method) + id match { + case Some(value) => Map(idNames.head -> value) //TODO handle compound keys + case None => throw new IllegalStateException(s"Id is missing while calling ${method.getName}") + } + } + } +} + +// format: off +final class ComponentCall2[A1, A2, R](kalixClient: KalixClient, lambda: scala.Any, entityId: Optional[String]) { + + /** + * Pass in the parameters that are required to execute this call. + * + * The types and order of parameters are the same as defined in the method reference + * used to build this DeferredCall. + */ + def params(a1: A1, a2: A2): DeferredCall[Any, R] = { + ComponentCall.invoke(Seq(a1, a2), kalixClient, lambda, entityId.toScala) + } +} +final class ComponentCall3[A1, A2, A3, R](kalixClient: KalixClient, lambda: scala.Any, entityId: Optional[String]) { + + /** + * Pass in the parameters that are required to execute this call. + * + * The types and order of parameters are the same as defined in the method reference + * used to build this DeferredCall. + */ + def params(a1: A1, a2: A2, a3: A3): DeferredCall[Any, R] = { + ComponentCall.invoke(Seq(a1, a2, a3), kalixClient, lambda, entityId.toScala) + } +} +final class ComponentCall4[A1, A2, A3, A4, R](kalixClient: KalixClient, lambda: scala.Any, entityId: Optional[String]) { + + /** + * Pass in the parameters that are required to execute this call. + * + * The types and order of parameters are the same as defined in the method reference + * used to build this DeferredCall. + */ + def params(a1: A1, a2: A2, a3: A3, a4: A4): DeferredCall[Any, R] = { + ComponentCall.invoke(Seq(a1, a2, a3, a4), kalixClient, lambda, entityId.toScala) + } +} +final class ComponentCall5[A1, A2, A3, A4, A5, R](kalixClient: KalixClient, lambda: scala.Any, entityId: Optional[String]) { + + /** + * Pass in the parameters that are required to execute this call. + * + * The types and order of parameters are the same as defined in the method reference + * used to build this DeferredCall. + */ + def params(a1: A1, a2: A2, a3: A3, a4: A4, a5: A5): DeferredCall[Any, R] = { + ComponentCall.invoke(Seq(a1, a2, a3, a4, a5), kalixClient, lambda, entityId.toScala) + } +} +final class ComponentCall6[A1, A2, A3, A4, A5, A6, R](kalixClient: KalixClient, lambda: scala.Any, entityId: Optional[String]) { + + /** + * Pass in the parameters that are required to execute this call. + * + * The types and order of parameters are the same as defined in the method reference + * used to build this DeferredCall. + */ + def params(a1: A1, a2: A2, a3: A3, a4: A4, a5: A5, a6: A6): DeferredCall[Any, R] = { + ComponentCall.invoke(Seq(a1, a2, a3, a4, a5, a6), kalixClient, lambda, entityId.toScala) + } +} +final class ComponentCall7[A1, A2, A3, A4, A5, A6, A7, R](kalixClient: KalixClient, lambda: scala.Any, entityId: Optional[String]) { + + /** + * Pass in the parameters that are required to execute this call. + * + * The types and order of parameters are the same as defined in the method reference + * used to build this DeferredCall. + */ + def params(a1: A1, a2: A2, a3: A3, a4: A4, a5: A5, a6: A6, a7: A7): DeferredCall[Any, R] = { + ComponentCall.invoke(Seq(a1, a2, a3, a4, a5, a6, a7), kalixClient, lambda, entityId.toScala) + } +} +final class ComponentCall8[A1, A2, A3, A4, A5, A6, A7, A8, R](kalixClient: KalixClient, lambda: scala.Any, entityId: Optional[String]) { + + /** + * Pass in the parameters that are required to execute this call. + * + * The types and order of parameters are the same as defined in the method reference + * used to build this DeferredCall. + */ + def params(a1: A1, a2: A2, a3: A3, a4: A4, a5: A5, a6: A6, a7: A7, a8: A8): DeferredCall[Any, R] = { + ComponentCall.invoke(Seq(a1, a2, a3, a4, a5, a6, a7, a8), kalixClient, lambda, entityId.toScala) + } +} +final class ComponentCall9[A1, A2, A3, A4, A5, A6, A7, A8, A9, R](kalixClient: KalixClient, lambda: scala.Any, entityId: Optional[String]) { + + /** + * Pass in the parameters that are required to execute this call. + * + * The types and order of parameters are the same as defined in the method reference + * used to build this DeferredCall. + */ + def params(a1: A1, a2: A2, a3: A3, a4: A4, a5: A5, a6: A6, a7: A7, a8: A8, a9: A9): DeferredCall[Any, R] = { + ComponentCall.invoke(Seq(a1, a2, a3, a4, a5, a6, a7, a8, a9), kalixClient, lambda, entityId.toScala) + } +} +final class ComponentCall10[A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, R](kalixClient: KalixClient, lambda: scala.Any, entityId: Optional[String]) { + + /** + * Pass in the parameters that are required to execute this call. + * + * The types and order of parameters are the same as defined in the method reference + * used to build this DeferredCall. + */ + def params(a1: A1, a2: A2, a3: A3, a4: A4, a5: A5, a6: A6, a7: A7, a8: A8, a9: A9, a10: A10): DeferredCall[Any, R] = { + ComponentCall.invoke(Seq(a1, a2, a3, a4, a5, a6, a7, a8, a9, a10), kalixClient, lambda, entityId.toScala) + } +} +final class ComponentCall11[A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11, R](kalixClient: KalixClient, lambda: scala.Any, entityId: Optional[String]) { + + /** + * Pass in the parameters that are required to execute this call. + * + * The types and order of parameters are the same as defined in the method reference + * used to build this DeferredCall. + */ + def params(a1: A1, a2: A2, a3: A3, a4: A4, a5: A5, a6: A6, a7: A7, a8: A8, a9: A9, a10: A10, a11: A11): DeferredCall[Any, R] = { + ComponentCall.invoke(Seq(a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11), kalixClient, lambda, entityId.toScala) + } +} +final class ComponentCall12[A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11, A12, R](kalixClient: KalixClient, lambda: scala.Any, entityId: Optional[String]) { + + /** + * Pass in the parameters that are required to execute this call. + * + * The types and order of parameters are the same as defined in the method reference + * used to build this DeferredCall. + */ + def params(a1: A1, a2: A2, a3: A3, a4: A4, a5: A5, a6: A6, a7: A7, a8: A8, a9: A9, a10: A10, a11: A11, a12: A12): DeferredCall[Any, R] = { + ComponentCall.invoke(Seq(a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12), kalixClient, lambda, entityId.toScala) + } +} +final class ComponentCall13[A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11, A12, A13, R](kalixClient: KalixClient, lambda: scala.Any, entityId: Optional[String]) { + + /** + * Pass in the parameters that are required to execute this call. + * + * The types and order of parameters are the same as defined in the method reference + * used to build this DeferredCall. + */ + def params(a1: A1, a2: A2, a3: A3, a4: A4, a5: A5, a6: A6, a7: A7, a8: A8, a9: A9, a10: A10, a11: A11, a12: A12, a13: A13): DeferredCall[Any, R] = { + ComponentCall.invoke(Seq(a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13), kalixClient, lambda, entityId.toScala) + } +} +final class ComponentCall14[A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11, A12, A13, A14, R](kalixClient: KalixClient, lambda: scala.Any, entityId: Optional[String]) { + + /** + * Pass in the parameters that are required to execute this call. + * + * The types and order of parameters are the same as defined in the method reference + * used to build this DeferredCall. + */ + def params(a1: A1, a2: A2, a3: A3, a4: A4, a5: A5, a6: A6, a7: A7, a8: A8, a9: A9, a10: A10, a11: A11, a12: A12, a13: A13, a14: A14): DeferredCall[Any, R] = { + ComponentCall.invoke(Seq(a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14), kalixClient, lambda, entityId.toScala) + } +} +final class ComponentCall15[A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11, A12, A13, A14, A15, R](kalixClient: KalixClient, lambda: scala.Any, entityId: Optional[String]) { + + /** + * Pass in the parameters that are required to execute this call. + * + * The types and order of parameters are the same as defined in the method reference + * used to build this DeferredCall. + */ + def params(a1: A1, a2: A2, a3: A3, a4: A4, a5: A5, a6: A6, a7: A7, a8: A8, a9: A9, a10: A10, a11: A11, a12: A12, a13: A13, a14: A14, a15: A15): DeferredCall[Any, R] = { + ComponentCall.invoke(Seq(a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15), kalixClient, lambda, entityId.toScala) + } +} +final class ComponentCall16[A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11, A12, A13, A14, A15, A16, R](kalixClient: KalixClient, lambda: scala.Any, entityId: Optional[String]) { + + /** + * Pass in the parameters that are required to execute this call. + * + * The types and order of parameters are the same as defined in the method reference + * used to build this DeferredCall. + */ + def params(a1: A1, a2: A2, a3: A3, a4: A4, a5: A5, a6: A6, a7: A7, a8: A8, a9: A9, a10: A10, a11: A11, a12: A12, a13: A13, a14: A14, a15: A15, a16: A16): DeferredCall[Any, R] = { + ComponentCall.invoke(Seq(a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16), kalixClient, lambda, entityId.toScala) + } +} +final class ComponentCall17[A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11, A12, A13, A14, A15, A16, A17, R](kalixClient: KalixClient, lambda: scala.Any, entityId: Optional[String]) { + + /** + * Pass in the parameters that are required to execute this call. + * + * The types and order of parameters are the same as defined in the method reference + * used to build this DeferredCall. + */ + def params(a1: A1, a2: A2, a3: A3, a4: A4, a5: A5, a6: A6, a7: A7, a8: A8, a9: A9, a10: A10, a11: A11, a12: A12, a13: A13, a14: A14, a15: A15, a16: A16, a17: A17): DeferredCall[Any, R] = { + ComponentCall.invoke(Seq(a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16, a17), kalixClient, lambda, entityId.toScala) + } +} +final class ComponentCall18[A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11, A12, A13, A14, A15, A16, A17, A18, R](kalixClient: KalixClient, lambda: scala.Any, entityId: Optional[String]) { + + /** + * Pass in the parameters that are required to execute this call. + * + * The types and order of parameters are the same as defined in the method reference + * used to build this DeferredCall. + */ + def params(a1: A1, a2: A2, a3: A3, a4: A4, a5: A5, a6: A6, a7: A7, a8: A8, a9: A9, a10: A10, a11: A11, a12: A12, a13: A13, a14: A14, a15: A15, a16: A16, a17: A17, a18: A18): DeferredCall[Any, R] = { + ComponentCall.invoke(Seq(a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16, a17, a18), kalixClient, lambda, entityId.toScala) + } +} +final class ComponentCall19[A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11, A12, A13, A14, A15, A16, A17, A18, A19, R](kalixClient: KalixClient, lambda: scala.Any, entityId: Optional[String]) { + + /** + * Pass in the parameters that are required to execute this call. + * + * The types and order of parameters are the same as defined in the method reference + * used to build this DeferredCall. + */ + def params(a1: A1, a2: A2, a3: A3, a4: A4, a5: A5, a6: A6, a7: A7, a8: A8, a9: A9, a10: A10, a11: A11, a12: A12, a13: A13, a14: A14, a15: A15, a16: A16, a17: A17, a18: A18, a19: A19): DeferredCall[Any, R] = { + ComponentCall.invoke(Seq(a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16, a17, a18, a19), kalixClient, lambda, entityId.toScala) + } +} +final class ComponentCall20[A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11, A12, A13, A14, A15, A16, A17, A18, A19, A20, R](kalixClient: KalixClient, lambda: scala.Any, entityId: Optional[String]) { + + /** + * Pass in the parameters that are required to execute this call. + * + * The types and order of parameters are the same as defined in the method reference + * used to build this DeferredCall. + */ + def params(a1: A1, a2: A2, a3: A3, a4: A4, a5: A5, a6: A6, a7: A7, a8: A8, a9: A9, a10: A10, a11: A11, a12: A12, a13: A13, a14: A14, a15: A15, a16: A16, a17: A17, a18: A18, a19: A19, a20: A20): DeferredCall[Any, R] = { + ComponentCall.invoke(Seq(a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16, a17, a18, a19, a20), kalixClient, lambda, entityId.toScala) + } +} +final class ComponentCall21[A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11, A12, A13, A14, A15, A16, A17, A18, A19, A20, A21, R](kalixClient: KalixClient, lambda: scala.Any, entityId: Optional[String]) { + + /** + * Pass in the parameters that are required to execute this call. + * + * The types and order of parameters are the same as defined in the method reference + * used to build this DeferredCall. + */ + def params(a1: A1, a2: A2, a3: A3, a4: A4, a5: A5, a6: A6, a7: A7, a8: A8, a9: A9, a10: A10, a11: A11, a12: A12, a13: A13, a14: A14, a15: A15, a16: A16, a17: A17, a18: A18, a19: A19, a20: A20, a21: A21): DeferredCall[Any, R] = { + ComponentCall.invoke(Seq(a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16, a17, a18, a19, a20, a21), kalixClient, lambda, entityId.toScala) + } +} +// format: on diff --git a/sdk/java-sdk-spring/src/main/scala/kalix/javasdk/impl/EntityDescriptorFactory.scala b/sdk/java-sdk-spring/src/main/scala/kalix/javasdk/impl/EntityDescriptorFactory.scala index a20f638d02..89d428d0f7 100644 --- a/sdk/java-sdk-spring/src/main/scala/kalix/javasdk/impl/EntityDescriptorFactory.scala +++ b/sdk/java-sdk-spring/src/main/scala/kalix/javasdk/impl/EntityDescriptorFactory.scala @@ -16,18 +16,12 @@ package kalix.javasdk.impl -import java.lang.reflect.AnnotatedElement - import kalix.KeyGeneratorMethodOptions.Generator -import kalix.javasdk.annotations.EntityKey -import kalix.javasdk.annotations.GenerateEntityKey -import kalix.javasdk.annotations.GenerateId -import kalix.javasdk.annotations.Id import kalix.javasdk.impl.ComponentDescriptorFactory.buildJWTOptions +import kalix.javasdk.impl.reflection.IdExtractor.extractIds import kalix.javasdk.impl.reflection.KalixMethod import kalix.javasdk.impl.reflection.NameGenerator import kalix.javasdk.impl.reflection.RestServiceIntrospector -import kalix.javasdk.impl.reflection.ServiceIntrospectionException private[impl] object EntityDescriptorFactory extends ComponentDescriptorFactory { @@ -36,53 +30,18 @@ private[impl] object EntityDescriptorFactory extends ComponentDescriptorFactory messageCodec: JsonMessageCodec, nameGenerator: NameGenerator): ComponentDescriptor = { - def idValue(annotatedElement: AnnotatedElement) = - if (annotatedElement.getAnnotation(classOf[Id]) != null) - annotatedElement.getAnnotation(classOf[Id]).value() - else if (annotatedElement.getAnnotation(classOf[EntityKey]) != null) - annotatedElement.getAnnotation(classOf[EntityKey]).value() - else - Array.empty[String] - - def shouldGenerateId(annotatedElement: AnnotatedElement) = - if (annotatedElement.getAnnotation(classOf[GenerateId]) != null) - true - else - annotatedElement.getAnnotation(classOf[GenerateEntityKey]) != null - - val idOnType = idValue(component) - val kalixMethods = RestServiceIntrospector.inspectService(component).methods.map { restMethod => - val entityIdOnMethod = idValue(restMethod.javaMethod) - val generateEntityId = shouldGenerateId(restMethod.javaMethod) - if (entityIdOnMethod.nonEmpty && generateEntityId) - throw ServiceIntrospectionException( - restMethod.javaMethod, - "Invalid annotation usage. Found both @Id and @GenerateId annotations. " + - "A method can only be annotated with one of them, but not both.") + val ids = extractIds(component, restMethod.javaMethod) val kalixMethod = - if (generateEntityId) { + if (ids.isEmpty) { val keyGenOptions = kalix.KeyGeneratorMethodOptions.newBuilder().setKeyGenerator(Generator.VERSION_4_UUID) val methodOpts = kalix.MethodOptions.newBuilder().setEntity(keyGenOptions) KalixMethod(restMethod).withKalixOptions(methodOpts.build()) - } else { - // keys defined on Method level get precedence - val entityKeysToUse = - if (entityIdOnMethod.nonEmpty) entityIdOnMethod - else idOnType - - if (entityKeysToUse.isEmpty) - throw ServiceIntrospectionException( - restMethod.javaMethod, - "Invalid command method. No @Id nor @GenerateId annotations found. " + - "A command method should be annotated with either @Id or @GenerateId, or " + - "an @Id annotation should be present at class level.") - - KalixMethod(restMethod, entityKeys = entityKeysToUse.toIndexedSeq) + KalixMethod(restMethod, entityKeys = ids) } kalixMethod.withKalixOptions(buildJWTOptions(restMethod.javaMethod)) diff --git a/sdk/java-sdk-spring/src/main/scala/kalix/javasdk/impl/client/MethodRefResolver.scala b/sdk/java-sdk-spring/src/main/scala/kalix/javasdk/impl/client/MethodRefResolver.scala new file mode 100644 index 0000000000..8a35c4d820 --- /dev/null +++ b/sdk/java-sdk-spring/src/main/scala/kalix/javasdk/impl/client/MethodRefResolver.scala @@ -0,0 +1,104 @@ +/* + * Copyright 2021 Lightbend Inc. + * + * 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 + * + * 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. + */ + +package kalix.javasdk.impl.client + +import java.lang.invoke.SerializedLambda +import java.lang.reflect.Method + +private[kalix] object MethodRefResolver { + + /** + * Resolve the method ref for a lambda. + */ + def resolveMethodRef(lambda: Any): Method = { + val lambdaType = lambda.getClass + + if (!classOf[java.io.Serializable].isInstance(lambda)) { + throw new IllegalArgumentException( + "Can only resolve method references from serializable SAMs, class was: " + lambdaType) + } + + val writeReplace = + try { + lambda.getClass.getDeclaredMethod("writeReplace") + } catch { + case e: NoSuchMethodError => + throw new IllegalArgumentException( + "Passed in object does not provide a writeReplace method, hence it can't be a Java 8 method reference.", + e) + } + + writeReplace.setAccessible(true) + + val serializedLambda = writeReplace.invoke(lambda) match { + case s: SerializedLambda => s + case other => + throw new IllegalArgumentException( + "Passed in object does not writeReplace itself with SerializedLambda, hence it can't be a Java 8 method reference.") + } + + // Try to load the class that the method ref is defined on + val ownerClass = loadClass(lambdaType.getClassLoader, serializedLambda.getImplClass) + + val argumentClasses = getArgumentClasses(lambdaType.getClassLoader, serializedLambda.getImplMethodSignature) + if (serializedLambda.getImplClass.equals("")) { + throw new IllegalArgumentException("Passed in method ref is a constructor.") + } else { + ownerClass.getDeclaredMethod(serializedLambda.getImplMethodName, argumentClasses: _*) + } + } + + private def loadClass(classLoader: ClassLoader, internalName: String) = { + Class.forName(internalName.replace('/', '.'), false, classLoader) + } + + private def getArgumentClasses(classLoader: ClassLoader, methodDescriptor: String): List[Class[_]] = { + def parseArgumentClasses(offset: Int, arrayDepth: Int): List[Class[_]] = { + methodDescriptor.charAt(offset) match { + case ')' => Nil + case 'L' => + val end = methodDescriptor.indexOf(';', offset) + val className = if (arrayDepth > 0) { + methodDescriptor.substring(offset - arrayDepth, end) + } else { + methodDescriptor.substring(offset + 1, end) + } + loadClass(classLoader, className) :: parseArgumentClasses(end + 1, 0) + case '[' => + parseArgumentClasses(offset + 1, arrayDepth + 1) + case _ if arrayDepth > 0 => + val className = methodDescriptor.substring(offset - arrayDepth, offset + 1) + loadClass(classLoader, className) :: parseArgumentClasses(offset + 1, 0) + case other => + val clazz = other match { + case 'Z' => classOf[Boolean] + case 'C' => classOf[Char] + case 'B' => classOf[Byte] + case 'S' => classOf[Short] + case 'I' => classOf[Int] + case 'F' => classOf[Float] + case 'J' => classOf[Long] + case 'D' => classOf[Double] + case unknown => throw sys.error("Unknown primitive type: " + unknown) + } + clazz :: parseArgumentClasses(offset + 1, 0) + } + } + + parseArgumentClasses(1, 0) + } +} diff --git a/sdk/java-sdk-spring/src/main/scala/kalix/javasdk/impl/http/HttpEndpointMethodDefinition.scala b/sdk/java-sdk-spring/src/main/scala/kalix/javasdk/impl/http/HttpEndpointMethodDefinition.scala index aeaf0bf4d1..53036d0ec4 100644 --- a/sdk/java-sdk-spring/src/main/scala/kalix/javasdk/impl/http/HttpEndpointMethodDefinition.scala +++ b/sdk/java-sdk-spring/src/main/scala/kalix/javasdk/impl/http/HttpEndpointMethodDefinition.scala @@ -18,13 +18,13 @@ package kalix.javasdk.impl.http import java.net.URLDecoder import java.nio.charset.StandardCharsets.UTF_8 +import java.util import java.util.regex.Matcher import scala.annotation.tailrec import scala.jdk.CollectionConverters._ import scala.util.control.NonFatal -import akka.http.scaladsl.model.Uri.Path import akka.http.scaladsl.model.ErrorInfo import akka.http.scaladsl.model.HttpMethod import akka.http.scaladsl.model.HttpMethods @@ -32,7 +32,7 @@ import akka.http.scaladsl.model.IllegalRequestException import akka.http.scaladsl.model.ParsingException import akka.http.scaladsl.model.RequestEntityAcceptance import akka.http.scaladsl.model.StatusCodes -import akka.http.scaladsl.model.Uri +import akka.http.scaladsl.model.Uri.Path import com.google.api.HttpRule.PatternCase import com.google.api.annotations.AnnotationsProto import com.google.api.http.CustomHttpPattern @@ -40,15 +40,17 @@ import com.google.api.http.HttpRule import com.google.api.http.HttpRule.Pattern._ import com.google.api.{ AnnotationsProto => JavaAnnotationsProto } import com.google.api.{ HttpRule => JavaHttpRule } +import com.google.protobuf.Descriptors import com.google.protobuf.Descriptors.Descriptor import com.google.protobuf.Descriptors.FieldDescriptor import com.google.protobuf.Descriptors.MethodDescriptor import com.google.protobuf.Descriptors.ServiceDescriptor +import com.google.protobuf.DynamicMessage import com.google.protobuf.descriptor.{ MethodOptions => spbMethodOptions } import com.google.protobuf.util.Durations import com.google.protobuf.util.Timestamps -import com.google.protobuf.Descriptors -import com.google.protobuf.DynamicMessage +import kalix.javasdk.impl.http.HttpEndpointMethodDefinition.lookupFieldByName +import kalix.javasdk.impl.http.HttpEndpointMethodDefinition.parsingError import kalix.javasdk.impl.path.PathPatternParseException import org.slf4j.LoggerFactory @@ -254,7 +256,7 @@ object HttpEndpointMethodDefinition { s"HTTP API path template for [${methDesc.getFullName}] references [$fieldName] but that is a map field") else if (field.isRepeated) parsingError( - s"HTTP API path template for [${methDesc.getFullName}] references [$fieldName] but that is a repeated fieldfield") + s"HTTP API path template for [${methDesc.getFullName}] references [$fieldName] but that is a repeated field") else { val notSupported = (message: String) => parsingError(s"HTTP API path for [${methDesc.getFullName}]: $message") @@ -311,7 +313,7 @@ object HttpEndpointMethodDefinition { private def lookupFieldByName(desc: Descriptor, selector: String): FieldDescriptor = desc.findFieldByName(selector) // TODO potentially start supporting path-like selectors with maximum nesting level? - private val parsingError: String => Nothing = s => throw ParsingException(new ErrorInfo(s)) + private def parsingError(msg: String): Nothing = throw ParsingException(new ErrorInfo(msg)) } final case class HttpEndpointMethodDefinition private ( @@ -324,19 +326,48 @@ final case class HttpEndpointMethodDefinition private ( responseBodyDescriptor: Option[FieldDescriptor]) { // Making this a method so we can ensure it's used the same way - private def pathMatcher(path: Uri.Path): Matcher = + private def pathMatcher(path: String): Matcher = pathTemplate.regex.pattern .matcher( - path.toString() + path ) // FIXME path.toString is costly, and using Regexes are too, switch to using a generated parser instead - def matches(path: Uri.Path): Boolean = + def matches(path: String): Boolean = pathMatcher(path).matches() private def lookupRequestFieldByPath(selector: String): Descriptors.FieldDescriptor = HttpEndpointMethodDefinition.lookupFieldByPath(methodDescriptor.getInputType, selector) - def parsePathParametersInto(path: Path, inputBuilder: DynamicMessage.Builder): Unit = { + def parseTypedPathParametersInto(pathVariables: Map[String, ?], inputBuilder: DynamicMessage.Builder): Unit = { + + //TODO fix exceptions msgs + pathVariables.foreach { case (fieldName, value) => + val field = lookupFieldByName(methodDescriptor.getInputType, fieldName) match { + case null => + parsingError( + s"HTTP API path template for [${methodDescriptor.getFullName}] references an unknown field named [$fieldName], methDesc)") + case field => + if (field.isMapField) + parsingError( + s"HTTP API path template for [${methodDescriptor.getFullName}] references [$fieldName] but that is a map field") + else if (field.isRepeated) + parsingError( + s"HTTP API path template for [${methodDescriptor.getFullName}] references [$fieldName] but that is a repeated fieldfield") + else { + val notSupported = + (message: String) => parsingError(s"HTTP API path for [${methodDescriptor.getFullName}]: $message") + + //we don't need a parser, just to check if the type is supported + HttpEndpointMethod.suitableParserFor(field)(notSupported) + field + } + } + + inputBuilder.setField(field, value) + } + } + + def parsePathParametersInto(path: String, inputBuilder: DynamicMessage.Builder): Unit = { val matcher = pathMatcher(path) matcher.find() pathExtractor( @@ -354,8 +385,32 @@ final case class HttpEndpointMethodDefinition private ( "google.protobuf.Duration" -> Durations.parse) // We use this to signal to the requester that there's something wrong with the request - private val requestError: String => Nothing = s => - throw IllegalRequestException(StatusCodes.BadRequest, new ErrorInfo(s)) + private def requestError(msg: String): Nothing = + throw IllegalRequestException(StatusCodes.BadRequest, new ErrorInfo(msg)) + + def parseTypedRequestParametersInto( + queryParams: Map[String, util.List[scala.Any]], + inputBuilder: DynamicMessage.Builder): Unit = { + queryParams.foreach { case (name, values) => + if (!values.isEmpty) { + lookupRequestFieldByPath(name) match { + case null => requestError(s"Query parameter [$name] refers to a non-existent field.") + case field if field.getJavaType == FieldDescriptor.JavaType.MESSAGE => + requestError(s"Query parameter [$name] refers to a message type. Only scalar value types are supported.") + case field if !field.isRepeated && values.size() > 1 => + requestError(s"Query parameter [$name] has multiple values for a non-repeated field.") + case field => + if (field.isRepeated) { + values.forEach(v => { + inputBuilder.addRepeatedField(field, v) + }) + } else { + inputBuilder.setField(field, values.get(0)) + } + } + } + } + } def parseRequestParametersInto(query: Map[String, List[String]], inputBuilder: DynamicMessage.Builder): Unit = query.foreach { case (selector, values) => @@ -363,6 +418,7 @@ final case class HttpEndpointMethodDefinition private ( lookupRequestFieldByPath(selector) match { case null => requestError(s"Query parameter [$selector] refers to a non-existent field.") case field if field.getJavaType == FieldDescriptor.JavaType.MESSAGE => + //this is actually not supported at the moment: https://github.com/lightbend/kalix-jvm-sdk/issues/1434 singleStringMessageParsers.get(field.getMessageType.getFullName) match { case Some(parser) => try { diff --git a/sdk/java-sdk-spring/src/main/scala/kalix/javasdk/impl/reflection/IdExtractor.scala b/sdk/java-sdk-spring/src/main/scala/kalix/javasdk/impl/reflection/IdExtractor.scala new file mode 100644 index 0000000000..ebbf31be95 --- /dev/null +++ b/sdk/java-sdk-spring/src/main/scala/kalix/javasdk/impl/reflection/IdExtractor.scala @@ -0,0 +1,74 @@ +/* + * Copyright 2021 Lightbend Inc. + * + * 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 + * + * 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. + */ + +package kalix.javasdk.impl.reflection + +import java.lang.reflect.AnnotatedElement + +import kalix.javasdk.annotations.EntityKey +import kalix.javasdk.annotations.GenerateEntityKey +import java.lang.reflect.Method + +import kalix.javasdk.annotations.GenerateId +import kalix.javasdk.annotations.Id + +object IdExtractor { + + private[kalix] def shouldGenerateId(annotatedElement: AnnotatedElement) = + if (annotatedElement.getAnnotation(classOf[GenerateId]) != null) + true + else + annotatedElement.getAnnotation(classOf[GenerateEntityKey]) != null + + def extractIds(component: Class[_], method: Method): Seq[String] = { + + def idValue(annotatedElement: AnnotatedElement) = + if (annotatedElement.getAnnotation(classOf[Id]) != null) + annotatedElement.getAnnotation(classOf[Id]).value() + else if (annotatedElement.getAnnotation(classOf[EntityKey]) != null) + annotatedElement.getAnnotation(classOf[EntityKey]).value() + else + Array.empty[String] + + val idsOnType = idValue(component) + val idsOnMethod = idValue(method) + + if (shouldGenerateId(method)) { + if (idsOnMethod.nonEmpty) + throw ServiceIntrospectionException( + method, + "Invalid annotation usage. Found both @Id and @GenerateId annotations. " + + "A method can only be annotated with one of them, but not both.") + else { + Seq.empty + } + } else { + // ids defined on Method level get precedence + val idsToUse = + if (idsOnMethod.nonEmpty) idsOnMethod + else idsOnType + + if (idsToUse.isEmpty) + throw ServiceIntrospectionException( + method, + "Invalid command method. No @Id nor @GenerateId annotations found. " + + "A command method should be annotated with either @Id or @GenerateId, or " + + "an @Id annotation should be present at class level.") + + idsToUse.toIndexedSeq + } + } +} diff --git a/sdk/java-sdk-spring/src/main/scala/kalix/spring/boot/KalixConfiguration.scala b/sdk/java-sdk-spring/src/main/scala/kalix/spring/boot/KalixConfiguration.scala index d17e6c613e..37346c48d1 100644 --- a/sdk/java-sdk-spring/src/main/scala/kalix/spring/boot/KalixConfiguration.scala +++ b/sdk/java-sdk-spring/src/main/scala/kalix/spring/boot/KalixConfiguration.scala @@ -18,7 +18,7 @@ package kalix.spring.boot import com.typesafe.config.Config import com.typesafe.config.ConfigFactory -import kalix.javasdk.KalixRunner +import kalix.javasdk.client.ComponentClient import kalix.spring.impl.KalixSpringApplication import org.springframework.beans.factory.config.BeanPostProcessor import org.springframework.boot.autoconfigure.AutoConfiguration @@ -52,6 +52,10 @@ class KalixConfiguration(applicationContext: ApplicationContext) { def kalixSpringApplication(config: Config): KalixSpringApplication = new KalixSpringApplication(applicationContext, config) + @Bean + def componentClient(kalixSpringApplication: KalixSpringApplication): ComponentClient = + kalixSpringApplication.componentClient + @Bean def kalixReactiveWebServerFactory(kalixSpringApplication: KalixSpringApplication) = new KalixReactiveWebServerFactory(kalixSpringApplication) diff --git a/sdk/java-sdk-spring/src/main/scala/kalix/spring/impl/KalixSpringApplication.scala b/sdk/java-sdk-spring/src/main/scala/kalix/spring/impl/KalixSpringApplication.scala index bc4211601c..47d12110b9 100644 --- a/sdk/java-sdk-spring/src/main/scala/kalix/spring/impl/KalixSpringApplication.scala +++ b/sdk/java-sdk-spring/src/main/scala/kalix/spring/impl/KalixSpringApplication.scala @@ -34,6 +34,7 @@ import kalix.javasdk.action.ActionCreationContext import kalix.javasdk.action.ActionProvider import kalix.javasdk.action.ReflectiveActionProvider import kalix.javasdk.annotations.ViewId +import kalix.javasdk.client.ComponentClient import kalix.javasdk.eventsourced.ReflectiveEventSourcedEntityProvider import kalix.javasdk.eventsourcedentity.EventSourcedEntity import kalix.javasdk.eventsourcedentity.EventSourcedEntityContext @@ -70,6 +71,7 @@ import kalix.spring.impl.KalixSpringApplication.ViewCreationContextFactoryBean import kalix.spring.impl.KalixSpringApplication.WebClientProviderFactoryBean import kalix.spring.impl.KalixSpringApplication.WorkflowContextFactoryBean import kalix.spring.BuildInfo +import kalix.spring.impl.KalixSpringApplication.ComponentClientFactoryBean import org.slf4j.Logger import org.slf4j.LoggerFactory import org.springframework.beans.factory.BeanCreationException @@ -228,14 +230,25 @@ object KalixSpringApplication { object KalixClientFactoryBean extends ThreadLocalFactoryBean[KalixClient] { override def isSingleton: Boolean = true // yes, we only need one + override def getObject: KalixClient = if (threadLocal.get() != null) threadLocal.get() else throw new BeanCreationException("KalixClient can only be injected in Kalix Actions and Workflows.") } + object ComponentClientFactoryBean extends ThreadLocalFactoryBean[ComponentClient] { + override def isSingleton: Boolean = true // yes, we only need one + + override def getObject: ComponentClient = + if (threadLocal.get() != null) threadLocal.get() + else + throw new BeanCreationException("ComponentClient can only be injected in Kalix Actions and Workflows.") + } + object WebClientProviderFactoryBean extends ThreadLocalFactoryBean[WebClientProvider] { override def isSingleton: Boolean = true // yes, we only need one + override def getObject: WebClientProvider = if (threadLocal.get() != null) threadLocal.get() else @@ -248,7 +261,8 @@ case class KalixSpringApplication(applicationContext: ApplicationContext, config private val logger: Logger = LoggerFactory.getLogger(getClass) private val messageCodec = new JsonMessageCodec - private val kalixClient = new RestKalixClientImpl(messageCodec) + private[kalix] val kalixClient = new RestKalixClientImpl(messageCodec) + private[kalix] val componentClient = new ComponentClient(kalixClient) private val kalixBeanFactory = new DefaultListableBeanFactory(applicationContext) @@ -258,6 +272,7 @@ case class KalixSpringApplication(applicationContext: ApplicationContext, config kalixBeanFactory.registerSingleton("valueEntityContext", ValueEntityContextFactoryBean) kalixBeanFactory.registerSingleton("viewCreationContext", ViewCreationContextFactoryBean) kalixBeanFactory.registerSingleton("kalixClient", KalixClientFactoryBean) + kalixBeanFactory.registerSingleton("componentClient", ComponentClientFactoryBean) kalixBeanFactory.registerSingleton("webClientProvider", WebClientProviderFactoryBean) // there should be only one class annotated with SpringBootApplication in the applicationContext @@ -378,6 +393,7 @@ case class KalixSpringApplication(applicationContext: ApplicationContext, config val webClientProviderHolder = WebClientProviderHolder(context.materializer().system) setKalixClient(clz, webClientProviderHolder) + setComponentClient(clz, webClientProviderHolder) if (hasContextConstructor(clz, classOf[WebClientProvider])) { val webClientProvider = webClientProviderHolder.webClientProvider @@ -396,6 +412,13 @@ case class KalixSpringApplication(applicationContext: ApplicationContext, config } } + private def setComponentClient[T](clz: Class[T], webClientProviderHolder: WebClientProviderHolder): Unit = { + if (hasContextConstructor(clz, classOf[ComponentClient])) { + kalixClient.setWebClient(webClientProviderHolder.webClientProvider.localWebClient) + ComponentClientFactoryBean.set(componentClient) + } + } + private def eventSourcedEntityProvider[S, E, ES <: EventSourcedEntity[S, E]]( clz: Class[ES]): EventSourcedEntityProvider[S, E, ES] = ReflectiveEventSourcedEntityProvider.of( @@ -419,6 +442,7 @@ case class KalixSpringApplication(applicationContext: ApplicationContext, config val webClientProviderHolder = WebClientProviderHolder(context.materializer().system) setKalixClient(clz, webClientProviderHolder) + setComponentClient(clz, webClientProviderHolder) val workflowEntity = kalixBeanFactory.getBean(clz) diff --git a/sdk/java-sdk-spring/src/main/scala/kalix/spring/impl/RestKalixClientImpl.scala b/sdk/java-sdk-spring/src/main/scala/kalix/spring/impl/RestKalixClientImpl.scala index 3bf06a53c4..134b765bd8 100644 --- a/sdk/java-sdk-spring/src/main/scala/kalix/spring/impl/RestKalixClientImpl.scala +++ b/sdk/java-sdk-spring/src/main/scala/kalix/spring/impl/RestKalixClientImpl.scala @@ -16,12 +16,15 @@ package kalix.spring.impl +import java.net.URI +import java.util import java.util.concurrent.CompletionStage +import java.util.function.Function import scala.concurrent.ExecutionContext.Implicits.global import scala.concurrent.Future import scala.concurrent.Promise -import scala.jdk.CollectionConverters.CollectionHasAsScala +import scala.jdk.CollectionConverters._ import scala.jdk.FutureConverters._ import akka.http.scaladsl.model.HttpMethod @@ -44,6 +47,7 @@ import org.slf4j.Logger import org.slf4j.LoggerFactory import org.springframework.web.reactive.function.client.WebClient import org.springframework.web.reactive.function.client.WebClientResponseException +import org.springframework.web.util.UriBuilder /** * INTERNAL API @@ -58,7 +62,9 @@ final class RestKalixClientImpl(messageCodec: JsonMessageCodec) extends KalixCli // and we need the ProxyInfo to build the WebClient, so we need a Promise[WebClient] private val promisedWebClient: Promise[WebClient] = Promise[WebClient]() - def setWebClient(localWebClient: WebClient) = promisedWebClient.trySuccess(localWebClient) + def setWebClient(localWebClient: WebClient) = { + if (!promisedWebClient.isCompleted) promisedWebClient.trySuccess(localWebClient) + } private val webClient: Future[WebClient] = promisedWebClient.future @@ -84,9 +90,14 @@ final class RestKalixClientImpl(messageCodec: JsonMessageCodec) extends KalixCli } override def get[R](uriStr: String, returnType: Class[R]): DeferredCall[Any, R] = { - matchMethodOrThrow(HttpMethods.GET, uriStr) { httpDef => + runGet(uriStr, returnType) + } + + private def runGet[R](uriStr: String, returnType: Class[R]): DeferredCall[Any, R] = { + val uri = Uri(uriStr) + matchMethodOrThrow(HttpMethods.GET, uri.path.toString()) { httpDef => requestToRestDefCall( - uriStr, + uri, body = None, httpDef, () => @@ -103,34 +114,37 @@ final class RestKalixClientImpl(messageCodec: JsonMessageCodec) extends KalixCli } override def post[P, R](uriStr: String, body: P, returnType: Class[R]): DeferredCall[Any, R] = { - matchMethodOrThrow(HttpMethods.POST, uriStr) { httpDef => - requestToRestDefCall( - uriStr, - Some(body), - httpDef, - () => - webClient.flatMap { - _.post() - .uri(uriStr) - .bodyValue(body) - .retrieve() - .bodyToMono(returnType) - .toFuture - .asScala - }.asJava) + runPost(uriStr, Some(body), returnType) + } + + private def buildUri( + path: String, + pathVariables: Map[String, scala.Any], + queryParams: Map[String, util.List[scala.Any]]): Function[UriBuilder, URI] = uriBuilder => { + val builder = uriBuilder + .path(path) + queryParams.map { case (name, values) => + builder.queryParam(name, values) } + builder.build(pathVariables.asJava) } - override def post[R](uriStr: String, returnType: Class[R]): DeferredCall[Any, R] = { - matchMethodOrThrow(HttpMethods.POST, uriStr) { httpDef => + private def runPost[R, P](uriStr: String, body: Option[P], returnType: Class[R]) = { + val akkaUri = Uri(uriStr) + matchMethodOrThrow(HttpMethods.POST, akkaUri.path.toString()) { httpDef => requestToRestDefCall( - uriStr, - None, + akkaUri, + body, httpDef, () => - webClient.flatMap { - _.post() + webClient.flatMap { client => + val requestBodySpec = client + .post() .uri(uriStr) + + body.foreach(requestBodySpec.bodyValue) + + requestBodySpec .retrieve() .bodyToMono(returnType) .toFuture @@ -139,17 +153,30 @@ final class RestKalixClientImpl(messageCodec: JsonMessageCodec) extends KalixCli } } + override def post[R](uriStr: String, returnType: Class[R]): DeferredCall[Any, R] = { + runPost(uriStr, None, returnType) + } + override def put[P, R](uriStr: String, body: P, returnType: Class[R]): DeferredCall[Any, R] = { - matchMethodOrThrow(HttpMethods.PUT, uriStr) { httpDef => + runPut(uriStr, Some(body), returnType) + } + + private def runPut[P, R](uriStr: String, body: Option[P], returnType: Class[R]): DeferredCall[Any, R] = { + val akkaUri = Uri(uriStr) + matchMethodOrThrow(HttpMethods.PUT, akkaUri.path.toString()) { httpDef => requestToRestDefCall( - uriStr, - Some(body), + akkaUri, + body, httpDef, () => - webClient.flatMap { - _.put() + webClient.flatMap { client => + val requestBodySpec = client + .put() .uri(uriStr) - .bodyValue(body) + + body.foreach(requestBodySpec.bodyValue) + + requestBodySpec .retrieve() .bodyToMono(returnType) .toFuture @@ -159,15 +186,31 @@ final class RestKalixClientImpl(messageCodec: JsonMessageCodec) extends KalixCli } override def put[R](uriStr: String, returnType: Class[R]): DeferredCall[Any, R] = { - matchMethodOrThrow(HttpMethods.PUT, uriStr) { httpDef => + runPut(uriStr, None, returnType) + } + + override def patch[P, R](uriStr: String, body: P, returnType: Class[R]): DeferredCall[Any, R] = { + runPatch(uriStr, Some(body), returnType) + } + + private def runPatch[R, P](uriStr: String, body: Option[P], returnType: Class[R]) = { + + val akkaUri = Uri(uriStr) + + matchMethodOrThrow(HttpMethods.PATCH, akkaUri.path.toString()) { httpDef => requestToRestDefCall( - uriStr, - None, + akkaUri, + Some(body), httpDef, () => - webClient.flatMap { - _.put() + webClient.flatMap { client => + val requestBodySpec = client + .patch() .uri(uriStr) + + body.foreach(requestBodySpec.bodyValue) + + requestBodySpec .retrieve() .bodyToMono(returnType) .toFuture @@ -176,17 +219,25 @@ final class RestKalixClientImpl(messageCodec: JsonMessageCodec) extends KalixCli } } - override def patch[P, R](uriStr: String, body: P, returnType: Class[R]): DeferredCall[Any, R] = { - matchMethodOrThrow(HttpMethods.PATCH, uriStr) { httpDef => + override def patch[R](uriStr: String, returnType: Class[R]): DeferredCall[Any, R] = { + runPatch(uriStr, None, returnType) + } + + override def delete[R](uriStr: String, returnType: Class[R]): DeferredCall[Any, R] = { + runDelete(uriStr, returnType) + } + + private def runDelete[R](uriStr: String, returnType: Class[R]): DeferredCall[Any, R] = { + val akkaUri = Uri(uriStr) + matchMethodOrThrow(HttpMethods.DELETE, akkaUri.path.toString()) { httpDef => requestToRestDefCall( - uriStr, - Some(body), + akkaUri, + None, httpDef, () => webClient.flatMap { - _.patch() + _.delete() .uri(uriStr) - .bodyValue(body) .retrieve() .bodyToMono(returnType) .toFuture @@ -195,16 +246,26 @@ final class RestKalixClientImpl(messageCodec: JsonMessageCodec) extends KalixCli } } - override def patch[R](uriStr: String, returnType: Class[R]): DeferredCall[Any, R] = { - matchMethodOrThrow(HttpMethods.PATCH, uriStr) { httpDef => - requestToRestDefCall( - uriStr, + private[kalix] def runWithoutBody[R, P]( + httpMethod: HttpMethod, + pathTemplate: String, + pathVariables: Map[String, ?], + queryParams: Map[String, util.List[scala.Any]], + returnType: Class[R]): RestDeferredCall[Any, R] = { + + matchMethodOrThrow(httpMethod, pathTemplate) { httpDef => + requestToRestDefCall2( + pathVariables, + queryParams, None, httpDef, () => - webClient.flatMap { - _.patch() - .uri(uriStr) + webClient.flatMap { client => + + val requestBodySpec = requestHeadersUriSpec(client, httpMethod) + .uri(buildUri(pathTemplate, pathVariables, queryParams)) + + requestBodySpec .retrieve() .bodyToMono(returnType) .toFuture @@ -213,16 +274,28 @@ final class RestKalixClientImpl(messageCodec: JsonMessageCodec) extends KalixCli } } - override def delete[R](uriStr: String, returnType: Class[R]): DeferredCall[Any, R] = { - matchMethodOrThrow(HttpMethods.DELETE, uriStr) { httpDef => - requestToRestDefCall( - uriStr, - None, + private[kalix] def runWithBody[R, P]( + httpMethod: HttpMethod, + pathTemplate: String, + pathVariables: Map[String, scala.Any], + queryParams: Map[String, util.List[scala.Any]], + body: Option[P], + returnType: Class[R]): RestDeferredCall[Any, R] = { + + matchMethodOrThrow(httpMethod, pathTemplate) { httpDef => + requestToRestDefCall2( + pathVariables, + queryParams, + body, httpDef, () => - webClient.flatMap { - _.delete() - .uri(uriStr) + webClient.flatMap { client => + val requestBodySpec = requestBodyUriSpec(client, httpMethod) + .uri(buildUri(pathTemplate, pathVariables, queryParams)) + + body.foreach(requestBodySpec.bodyValue) + + requestBodySpec .retrieve() .bodyToMono(returnType) .toFuture @@ -231,25 +304,74 @@ final class RestKalixClientImpl(messageCodec: JsonMessageCodec) extends KalixCli } } - private def matchMethodOrThrow[R](httpMethod: HttpMethod, uriStr: String)( + private def requestBodyUriSpec[P, R](client: WebClient, httpMethod: HttpMethod): WebClient.RequestBodyUriSpec = { + httpMethod match { + case HttpMethods.PUT => client.put() + case HttpMethods.POST => client.post() + case HttpMethods.PATCH => client.patch() + case other => throw new IllegalStateException(s"RequestBodyUriSpec not supported for HTTP method [$other]") + } + } + + private def requestHeadersUriSpec[P, R]( + client: WebClient, + httpMethod: HttpMethod): WebClient.RequestHeadersUriSpec[_] = { + httpMethod match { + case HttpMethods.GET => client.get() + case HttpMethods.DELETE => client.delete() + case other => throw new IllegalStateException(s"RequestHeadersUriSpec not supported for HTTP method [$other]") + } + } + + private def requestToRestDefCall2[P, R]( + pathVariables: Map[String, scala.Any], + queryParams: Map[String, util.List[scala.Any]], + body: Option[P], + httpDef: HttpEndpointMethodDefinition, + asyncCall: () => CompletionStage[R]): RestDeferredCall[Any, R] = { + + val inputBuilder = DynamicMessage.newBuilder(httpDef.methodDescriptor.getInputType) + + httpDef.parseTypedPathParametersInto(pathVariables, inputBuilder) + httpDef.parseTypedRequestParametersInto(queryParams, inputBuilder) + + val wrappedBody = buildWrappedBody(httpDef, inputBuilder, body) + + RestDeferredCall[Any, R]( + message = wrappedBody, + metadata = MetadataImpl.Empty, + fullServiceName = httpDef.methodDescriptor.getService.getFullName, + methodName = httpDef.methodDescriptor.getName, + asyncCall = () => + asyncCall().exceptionally { + case responseException: WebClientResponseException => + throw DeferredCallResponseException( + responseException.getMessage, + fromWebClientResponse(responseException), + responseException) + case other: Throwable => throw other + }) + } + + private def matchMethodOrThrow[R](httpMethod: HttpMethod, path: String)( createDefCall: => HttpEndpointMethodDefinition => RestDeferredCall[Any, R]) = { - val uri = Uri(uriStr) services - .find(d => (d.methodPattern == ANY_METHOD || httpMethod == d.methodPattern) && d.matches(uri.path)) - .map { createDefCall(_) } - .getOrElse(throw HttpMethodNotFoundException(httpMethod, uri.path.toString())) + .find(d => (d.methodPattern == ANY_METHOD || httpMethod == d.methodPattern) && d.matches(path)) + .map { + createDefCall(_) + } + .getOrElse(throw HttpMethodNotFoundException(httpMethod, path)) } private def requestToRestDefCall[P, R]( - uriStr: String, + uri: Uri, body: Option[P], httpDef: HttpEndpointMethodDefinition, asyncCall: () => CompletionStage[R]): RestDeferredCall[Any, R] = { - val uri = Uri(uriStr) val inputBuilder = DynamicMessage.newBuilder(httpDef.methodDescriptor.getInputType) - httpDef.parsePathParametersInto(uri.path, inputBuilder) + httpDef.parsePathParametersInto(uri.path.toString(), inputBuilder) httpDef.parseRequestParametersInto(uri.query().toMultiMap, inputBuilder) val wrappedBody = buildWrappedBody(httpDef, inputBuilder, body) diff --git a/sdk/java-sdk-spring/src/test/java/kalix/javasdk/client/ComponentClientTest.java b/sdk/java-sdk-spring/src/test/java/kalix/javasdk/client/ComponentClientTest.java new file mode 100644 index 0000000000..127d65f746 --- /dev/null +++ b/sdk/java-sdk-spring/src/test/java/kalix/javasdk/client/ComponentClientTest.java @@ -0,0 +1,297 @@ +/* + * Copyright 2021 Lightbend Inc. + * + * 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 + * + * 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. + */ + +package kalix.javasdk.client; + +import com.google.protobuf.ByteString; +import com.google.protobuf.Descriptors; +import com.google.protobuf.DynamicMessage; +import com.google.protobuf.InvalidProtocolBufferException; +import com.google.protobuf.any.Any; +import kalix.javasdk.JsonSupport; +import kalix.javasdk.impl.AnySupport; +import kalix.javasdk.impl.ComponentDescriptor; +import kalix.javasdk.impl.JsonMessageCodec; +import kalix.javasdk.impl.RestDeferredCall; +import kalix.javasdk.impl.Validations; +import kalix.spring.impl.RestKalixClientImpl; +import kalix.spring.testmodels.Message; +import kalix.spring.testmodels.Number; +import kalix.spring.testmodels.action.ActionsTestModels.GetClassLevel; +import kalix.spring.testmodels.action.ActionsTestModels.GetWithOneParam; +import kalix.spring.testmodels.action.ActionsTestModels.GetWithoutParam; +import kalix.spring.testmodels.action.ActionsTestModels.PostWithOneQueryParam; +import kalix.spring.testmodels.action.ActionsTestModels.PostWithTwoParam; +import kalix.spring.testmodels.action.ActionsTestModels.PostWithoutParam; +import kalix.spring.testmodels.valueentity.Counter; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + + +class ComponentClientTest { + + private final JsonMessageCodec messageCodec = new JsonMessageCodec(); + private RestKalixClientImpl restKalixClient; + private ComponentClient componentClient; + + @BeforeEach + public void initEach() { + restKalixClient = new RestKalixClientImpl(messageCodec); + componentClient = new ComponentClient(restKalixClient); + } + + @Test + public void shouldNotReturnDeferredCallMethodNotAnnotatedAsRESTEndpoint() { + assertThatThrownBy(() -> componentClient.forAction().call(GetWithoutParam::missingRestAnnotation)) + .hasMessage("Method [missingRestAnnotation] is not annotated as a REST endpoint."); + } + + @Test + public void shouldReturnDeferredCallForSimpleGETRequest() throws InvalidProtocolBufferException { + //given + var action = descriptorFor(GetWithoutParam.class, messageCodec); + restKalixClient.registerComponent(action.serviceDescriptor()); + var targetMethod = action.serviceDescriptor().findMethodByName("Message"); + + //when + RestDeferredCall call = (RestDeferredCall) componentClient.forAction().call(GetWithoutParam::message); + + //then + assertThat(call.fullServiceName()).isEqualTo(targetMethod.getService().getFullName()); + assertThat(call.methodName()).isEqualTo(targetMethod.getName()); + assertMethodParamsMatch(targetMethod, call.message()); + } + + @Test + public void shouldReturnDeferredCallForGETRequestWithParam() throws InvalidProtocolBufferException { + //given + var action = descriptorFor(GetWithOneParam.class, messageCodec); + restKalixClient.registerComponent(action.serviceDescriptor()); + var targetMethod = action.serviceDescriptor().findMethodByName("Message"); + String param = "a b&c@d"; + + //when + RestDeferredCall call = (RestDeferredCall) componentClient.forAction() + .call(GetWithOneParam::message) + .params(param); + + //then + assertThat(call.fullServiceName()).isEqualTo(targetMethod.getService().getFullName()); + assertThat(call.methodName()).isEqualTo(targetMethod.getName()); + assertMethodParamsMatch(targetMethod, call.message(), param); + } + + @Test + public void shouldReturnDeferredCallForGETRequestWithTwoParams() throws InvalidProtocolBufferException { + //given + var action = descriptorFor(GetClassLevel.class, messageCodec); + restKalixClient.registerComponent(action.serviceDescriptor()); + var targetMethod = action.serviceDescriptor().findMethodByName("Message"); + String param = "a b&c@d"; + Long param2 = 2L; + + //when + RestDeferredCall call = (RestDeferredCall) componentClient.forAction() + .call(GetClassLevel::message) + .params(param, param2); + + //then + assertThat(call.fullServiceName()).isEqualTo(targetMethod.getService().getFullName()); + assertThat(call.methodName()).isEqualTo(targetMethod.getName()); + assertMethodParamsMatch(targetMethod, call.message(), param, param2); + } + + @Test + public void shouldReturnDeferredCallForGETRequestWithTwoPathParamsAnd2ReqParams() throws InvalidProtocolBufferException { + //given + var action = descriptorFor(GetClassLevel.class, messageCodec); + restKalixClient.registerComponent(action.serviceDescriptor()); + var targetMethod = action.serviceDescriptor().findMethodByName("Message2"); + String param = "a b&c@d"; + Long param2 = 2L; + String param3 = "!@!#$%^%++___"; + int param4 = 4; + + //when + RestDeferredCall call = (RestDeferredCall) componentClient.forAction() + .call(GetClassLevel::message2) + .params(param, param2, param3, param4); + + //then + assertThat(call.fullServiceName()).isEqualTo(targetMethod.getService().getFullName()); + assertThat(call.methodName()).isEqualTo(targetMethod.getName()); + assertMethodParamsMatch(targetMethod, call.message(), param, param2, param3, param4); + } + + @Test + public void shouldReturnDeferredCallForGETRequestWithListAsReqParam() throws InvalidProtocolBufferException { + //given + var action = descriptorFor(GetClassLevel.class, messageCodec); + restKalixClient.registerComponent(action.serviceDescriptor()); + var targetMethod = action.serviceDescriptor().findMethodByName("Message3"); + String param = "a b&c@d"; + Long param2 = 2L; + String param3 = "!@!#$%^%++___"; + List param4 = List.of("1", "2"); + + //when + RestDeferredCall call = (RestDeferredCall) componentClient.forAction() + .call(GetClassLevel::message3) + .params(param, param2, param3, param4); + + //then + assertThat(call.fullServiceName()).isEqualTo(targetMethod.getService().getFullName()); + assertThat(call.methodName()).isEqualTo(targetMethod.getName()); + assertMethodParamsMatch(targetMethod, call.message(), param, param2, param3, param4); + } + + @Test + public void shouldReturnDeferredCallForSimplePOSTRequest() throws InvalidProtocolBufferException { + //given + var action = descriptorFor(PostWithoutParam.class, messageCodec); + restKalixClient.registerComponent(action.serviceDescriptor()); + var targetMethod = action.serviceDescriptor().findMethodByName("Message"); + Message body = new Message("hello world"); + + //when + RestDeferredCall call = (RestDeferredCall) componentClient.forAction().call(PostWithoutParam::message).params(body); + + //then + assertThat(call.fullServiceName()).isEqualTo(targetMethod.getService().getFullName()); + assertThat(call.methodName()).isEqualTo(targetMethod.getName()); + assertThat(getBody(targetMethod, call.message(), Message.class)).isEqualTo(body); + } + + @Test + public void shouldReturnDeferredCallForPOSTRequestWithTwoParamsAndBody() throws InvalidProtocolBufferException { + //given + var action = descriptorFor(PostWithTwoParam.class, messageCodec); + restKalixClient.registerComponent(action.serviceDescriptor()); + var targetMethod = action.serviceDescriptor().findMethodByName("Message"); + String param = "a b&c@d"; + Long param2 = 2L; + Message body = new Message("hello world"); + + //when + RestDeferredCall call = (RestDeferredCall) componentClient.forAction() + .call(PostWithTwoParam::message) + .params(param, param2, body); + + //then + assertThat(call.fullServiceName()).isEqualTo(targetMethod.getService().getFullName()); + assertThat(call.methodName()).isEqualTo(targetMethod.getName()); + assertMethodParamsMatch(targetMethod, call.message(), param, param2); + assertThat(getBody(targetMethod, call.message(), Message.class)).isEqualTo(body); + } + + @Test + public void shouldReturnDeferredCallForPOSTRequestWhenMultipleMethodsAreAvailable() throws InvalidProtocolBufferException { + //given + var action = descriptorFor(PostWithoutParam.class, messageCodec); + var action2 = descriptorFor(PostWithTwoParam.class, messageCodec); + restKalixClient.registerComponent(action.serviceDescriptor()); + restKalixClient.registerComponent(action2.serviceDescriptor()); + var targetMethod = action.serviceDescriptor().findMethodByName("Message"); + Message body = new Message("hello world"); + + //when + RestDeferredCall call = (RestDeferredCall) componentClient.forAction().call(PostWithoutParam::message).params(body); + + //then + assertThat(call.fullServiceName()).isEqualTo(targetMethod.getService().getFullName()); + assertThat(call.methodName()).isEqualTo(targetMethod.getName()); + assertThat(getBody(targetMethod, call.message(), Message.class)).isEqualTo(body); + } + + @Test + public void shouldReturnDeferredCallForPOSTWithRequestParams() throws InvalidProtocolBufferException { + //given + var action = descriptorFor(PostWithOneQueryParam.class, messageCodec); + restKalixClient.registerComponent(action.serviceDescriptor()); + var targetMethod = action.serviceDescriptor().findMethodByName("Message"); + String param = "a b&c@d"; + Message body = new Message("hello world"); + + //when + RestDeferredCall call = (RestDeferredCall) componentClient.forAction() + .call(PostWithOneQueryParam::message) + .params(param, body); + + //then + assertThat(call.fullServiceName()).isEqualTo(targetMethod.getService().getFullName()); + assertThat(call.methodName()).isEqualTo(targetMethod.getName()); + assertMethodParamsMatch(targetMethod, call.message(), param); + assertThat(getBody(targetMethod, call.message(), Message.class)).isEqualTo(body); + } + + @Test + public void shouldReturnDeferredCallForVEWithRandomId() throws InvalidProtocolBufferException { + //given + var counterVE = descriptorFor(Counter.class, messageCodec); + restKalixClient.registerComponent(counterVE.serviceDescriptor()); + var targetMethod = counterVE.serviceDescriptor().findMethodByName("RandomIncrease"); + Integer param = 10; + + //when + RestDeferredCall call = (RestDeferredCall) componentClient.forValueEntity() + .call(Counter::randomIncrease) + .params(param); + + //then + assertThat(call.fullServiceName()).isEqualTo(targetMethod.getService().getFullName()); + assertThat(call.methodName()).isEqualTo(targetMethod.getName()); + assertMethodParamsMatch(targetMethod, call.message(), param); + } + + private ComponentDescriptor descriptorFor(Class clazz, JsonMessageCodec messageCodec) { + Validations.validate(clazz).failIfInvalid(); + return ComponentDescriptor.descriptorFor(clazz, messageCodec); + } + + private T getBody(Descriptors.MethodDescriptor targetMethod, Any message, Class clazz) throws InvalidProtocolBufferException { + var dynamicMessage = DynamicMessage.parseFrom(targetMethod.getInputType(), message.value()); + var body = (DynamicMessage) targetMethod.getInputType() + .getFields().stream() + .filter(f -> f.getName().equals("json_body")) + .map(dynamicMessage::getField) + .findFirst().orElseThrow(); + + return decodeJson(body, clazz); + } + + private T decodeJson(DynamicMessage dm, Class clazz) { + String typeUrl = (String) dm.getField(Any.javaDescriptor().findFieldByName("type_url")); + ByteString bytes = (ByteString) dm.getField(Any.javaDescriptor().findFieldByName("value")); + + var any = com.google.protobuf.Any.newBuilder().setTypeUrl(typeUrl).setValue(bytes).build(); + + return JsonSupport.decodeJson(clazz, any); + } + + private void assertMethodParamsMatch(Descriptors.MethodDescriptor targetMethod, Any message, Object... methodArgs) throws InvalidProtocolBufferException { + assertThat(message.typeUrl()).isEqualTo(AnySupport.DefaultTypeUrlPrefix() + "/" + targetMethod.getInputType().getFullName()); + var dynamicMessage = DynamicMessage.parseFrom(targetMethod.getInputType(), message.value()); + + List args = targetMethod.getInputType().getFields().stream().filter(f -> !f.getName().equals("json_body")).map(dynamicMessage::getField).toList(); + + assertThat(args).containsOnly(methodArgs); + } +} \ No newline at end of file diff --git a/sdk/java-sdk-spring/src/test/java/kalix/spring/testmodels/Message.java b/sdk/java-sdk-spring/src/test/java/kalix/spring/testmodels/Message.java index 560c2d0a97..5744fbdd2f 100644 --- a/sdk/java-sdk-spring/src/test/java/kalix/spring/testmodels/Message.java +++ b/sdk/java-sdk-spring/src/test/java/kalix/spring/testmodels/Message.java @@ -16,15 +16,5 @@ package kalix.spring.testmodels; -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonProperty; - -public class Message { - - public final String value; - - @JsonCreator - public Message(@JsonProperty("value") String value) { - this.value = value; - } +public record Message(String value) { } diff --git a/sdk/java-sdk-spring/src/test/java/kalix/spring/testmodels/action/ActionsTestModels.java b/sdk/java-sdk-spring/src/test/java/kalix/spring/testmodels/action/ActionsTestModels.java index 215f4096ce..2cc7df81a7 100644 --- a/sdk/java-sdk-spring/src/test/java/kalix/spring/testmodels/action/ActionsTestModels.java +++ b/sdk/java-sdk-spring/src/test/java/kalix/spring/testmodels/action/ActionsTestModels.java @@ -22,6 +22,8 @@ import org.springframework.web.bind.annotation.*; import reactor.core.publisher.Flux; +import java.util.List; + public class ActionsTestModels { public static class GetWithoutParam extends Action { @@ -29,6 +31,10 @@ public static class GetWithoutParam extends Action { public Action.Effect message() { return effects().reply(new Message("hello")); } + + public Action.Effect missingRestAnnotation() { + return effects().reply(new Message("hello")); + } } public static class GetWithOneParam extends Action { @@ -75,6 +81,16 @@ public static class GetClassLevel extends Action { public Action.Effect message(@PathVariable String one, @PathVariable Long two) { return effects().reply(new Message("hello")); } + + @GetMapping("/message2/{two}") + public Action.Effect message2(@PathVariable String one, @PathVariable Long two, @RequestParam String three, @RequestParam int four) { + return effects().reply(new Message("hello")); + } + + @GetMapping("/message3/{two}") + public Action.Effect message3(@PathVariable String one, @PathVariable Long two, @RequestParam String three, @RequestParam List four) { + return effects().reply(new Message("hello")); + } } public static class PostWithoutParam extends Action { @@ -97,14 +113,14 @@ public Action.Effect message(@RequestBody Message msg) { public static class PostWithOneParam extends Action { @PostMapping("/message/{one}") public Action.Effect message(@PathVariable String one, @RequestBody Message msg) { - return effects().reply(new Message(msg.value)); + return effects().reply(new Message(msg.value())); } } public static class PostWithOneQueryParam extends Action { @PostMapping("/message") public Action.Effect message(@RequestParam String dest, @RequestBody Message msg) { - return effects().reply(new Message(msg.value)); + return effects().reply(new Message(msg.value())); } } @@ -112,14 +128,14 @@ public static class PostWithTwoParam extends Action { @PostMapping("/message/{one}/{two}") public Action.Effect message( @PathVariable String one, @PathVariable Long two, @RequestBody Message msg) { - return effects().reply(new Message(msg.value)); + return effects().reply(new Message(msg.value())); } } public static class PostWithTwoMethods extends Action { @PostMapping("/message/{text}") public Action.Effect message(@PathVariable String text, @RequestBody Message msg) { - return effects().reply(new Message(msg.value)); + return effects().reply(new Message(msg.value())); } @PostMapping("/message/{num}") @@ -131,28 +147,28 @@ public Action.Effect message(@PathVariable Long num, @RequestBody Messa public static class PutWithoutParam extends Action { @PutMapping("/message") public Action.Effect message(@RequestBody Message msg) { - return effects().reply(new Message(msg.value)); + return effects().reply(new Message(msg.value())); } } public static class PutWithOneParam extends Action { @PutMapping("/message/{one}") public Action.Effect message(@PathVariable String one, @RequestBody Message msg) { - return effects().reply(new Message(msg.value)); + return effects().reply(new Message(msg.value())); } } public static class PatchWithoutParam extends Action { @PatchMapping("/message") public Action.Effect message(@RequestBody Message msg) { - return effects().reply(new Message(msg.value)); + return effects().reply(new Message(msg.value())); } } public static class PatchWithOneParam extends Action { @PatchMapping("/message/{one}") public Action.Effect message(@PathVariable String one, @RequestBody Message msg) { - return effects().reply(new Message(msg.value)); + return effects().reply(new Message(msg.value())); } } diff --git a/sdk/java-sdk-spring/src/test/java/kalix/spring/testmodels/action/EchoAction.java b/sdk/java-sdk-spring/src/test/java/kalix/spring/testmodels/action/EchoAction.java index d8022f4f8b..6b2b660707 100644 --- a/sdk/java-sdk-spring/src/test/java/kalix/spring/testmodels/action/EchoAction.java +++ b/sdk/java-sdk-spring/src/test/java/kalix/spring/testmodels/action/EchoAction.java @@ -29,6 +29,6 @@ public Effect stringMessage(@PathVariable String msg) { @PostMapping("/echo") public Effect messageBody(@RequestParam("add") String add, @RequestBody Message msg) { - return effects().reply(new Message(msg.value + add)); + return effects().reply(new Message(msg.value() + add)); } } diff --git a/sdk/java-sdk-spring/src/test/java/kalix/spring/testmodels/subscriptions/PubSubTestModels.java b/sdk/java-sdk-spring/src/test/java/kalix/spring/testmodels/subscriptions/PubSubTestModels.java index 924933d7b0..b5f2d1b4d3 100644 --- a/sdk/java-sdk-spring/src/test/java/kalix/spring/testmodels/subscriptions/PubSubTestModels.java +++ b/sdk/java-sdk-spring/src/test/java/kalix/spring/testmodels/subscriptions/PubSubTestModels.java @@ -598,7 +598,7 @@ public static class PublishBytesToTopicAction extends Action { @Publish.Topic("foobar") public Effect produce(Message msg) { - return effects().reply(msg.value.getBytes()); + return effects().reply(msg.value().getBytes()); } } diff --git a/sdk/java-sdk-spring/src/test/java/kalix/spring/testmodels/valueentity/Counter.java b/sdk/java-sdk-spring/src/test/java/kalix/spring/testmodels/valueentity/Counter.java index dbaceeed12..6e27d1a677 100644 --- a/sdk/java-sdk-spring/src/test/java/kalix/spring/testmodels/valueentity/Counter.java +++ b/sdk/java-sdk-spring/src/test/java/kalix/spring/testmodels/valueentity/Counter.java @@ -16,6 +16,7 @@ package kalix.spring.testmodels.valueentity; +import kalix.javasdk.annotations.GenerateId; import kalix.javasdk.valueentity.ValueEntity; import kalix.javasdk.annotations.Id; import kalix.javasdk.annotations.TypeId; @@ -23,6 +24,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; @@ -51,6 +53,17 @@ public ValueEntity.Effect increase(@RequestBody Number num) { return effects().updateState(newCounter).thenReply(new Number(newCounter.value)); } + @GenerateId + @PostMapping("/increase/{value}") + public ValueEntity.Effect randomIncrease(@PathVariable Integer value) { + CounterState counterState = new CounterState(commandContext().entityId(), value); + logger.info( + "Increasing counter '{}' to value '{}'", + counterState.id, + counterState.value); + return effects().updateState(counterState).thenReply(new Number(counterState.value)); + } + @GetMapping("/{counterId}") public ValueEntity.Effect get() { logger.info("Counter '{}' is '{}'", commandContext().entityId(), currentState().value); diff --git a/sdk/java-sdk-spring/src/test/scala/kalix/javasdk/impl/RestKalixClientImplSpec.scala b/sdk/java-sdk-spring/src/test/scala/kalix/javasdk/impl/RestKalixClientImplSpec.scala index cef73d9a4a..ba457bd6fd 100644 --- a/sdk/java-sdk-spring/src/test/scala/kalix/javasdk/impl/RestKalixClientImplSpec.scala +++ b/sdk/java-sdk-spring/src/test/scala/kalix/javasdk/impl/RestKalixClientImplSpec.scala @@ -16,6 +16,9 @@ package kalix.javasdk.impl +import java.net.URLEncoder +import java.nio.charset.StandardCharsets + import scala.jdk.CollectionConverters.CollectionHasAsScala import com.google.protobuf.ByteString @@ -151,13 +154,15 @@ class RestKalixClientImplSpec extends AnyWordSpec with Matchers with BeforeAndAf restKalixClient.registerComponent(actionPost.serviceDescriptor) val msgSent = new Message("hello world") - val defCall = restKalixClient.post("/message?dest=john", msgSent, classOf[Message]) + val reqParam = "a b&c@d" + val encodedParam = URLEncoder.encode(reqParam, StandardCharsets.UTF_8) + val defCall = restKalixClient.post(s"/message?dest=$encodedParam", msgSent, classOf[Message]) assertRestDeferredCall(defCall) { restDefCall => val targetMethod = actionPost.serviceDescriptor.findMethodByName("Message") restDefCall.fullServiceName shouldBe targetMethod.getService.getFullName restDefCall.methodName shouldBe targetMethod.getName - assertMethodParamsMatch(targetMethod, restDefCall.message, "john") + assertMethodParamsMatch(targetMethod, restDefCall.message, reqParam) assertMethodBodyMatch(targetMethod, restDefCall.message) { body => decodeJson(body, classOf[Message]).value shouldBe msgSent.value } diff --git a/sdk/java-sdk-spring/src/test/scala/kalix/javasdk/impl/ValueEntityDescriptorFactorySpec.scala b/sdk/java-sdk-spring/src/test/scala/kalix/javasdk/impl/ValueEntityDescriptorFactorySpec.scala index 0b273475de..4c273fa211 100644 --- a/sdk/java-sdk-spring/src/test/scala/kalix/javasdk/impl/ValueEntityDescriptorFactorySpec.scala +++ b/sdk/java-sdk-spring/src/test/scala/kalix/javasdk/impl/ValueEntityDescriptorFactorySpec.scala @@ -18,6 +18,8 @@ package kalix.javasdk.impl import com.google.protobuf.Any import com.google.protobuf.Descriptors.FieldDescriptor.JavaType +import kalix.KeyGeneratorMethodOptions +import kalix.spring.testmodels.valueentity.Counter import kalix.spring.testmodels.valueentity.ValueEntitiesTestModels.GetWithQueryParams import kalix.spring.testmodels.valueentity.ValueEntitiesTestModels.PostWithEntityKeys import kalix.spring.testmodels.valueentity.ValueEntitiesTestModels.ValueEntityWithMethodLevelAcl @@ -41,6 +43,16 @@ class ValueEntityDescriptorFactorySpec extends AnyWordSpec with ComponentDescrip } } + "generate mappings for a Value Entity with generated id" in { + assertDescriptor[Counter] { desc => + val method = desc.commandHandlers("RandomIncrease") + assertRequestFieldNumberAndJavaType(method, "value", 2, JavaType.INT) + + val extension = findKalixMethodOptions(desc, "RandomIncrease") + extension.getEntity.getKeyGenerator shouldBe KeyGeneratorMethodOptions.Generator.VERSION_4_UUID + } + } + "generate mappings for a Value Entity query params in path" in { assertDescriptor[GetWithQueryParams] { desc => val method = desc.commandHandlers("GetUser")