From 7c24351b8384f10a040b5dee2b6d787ba3ff76cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johan=20Andr=C3=A9n?= Date: Thu, 23 Jan 2025 16:42:26 +0100 Subject: [PATCH 1/4] docs: A more complete example of an endpoint (no streaming though) --- .../example/api/ExampleGrpcEndpointImpl.java | 6 +- .../com/example/example_grpc_endpoint.proto | 3 +- .../java/customer/api/CustomerEndpoint.java | 4 +- .../api/CustomerGrpcEndpointImpl.java | 141 ++++++++++++++++++ .../customer/api/customer_grpc_endpoint.proto | 81 ++++++++++ 5 files changed, 228 insertions(+), 7 deletions(-) create mode 100644 samples/event-sourced-customer-registry/src/main/java/customer/api/CustomerGrpcEndpointImpl.java create mode 100644 samples/event-sourced-customer-registry/src/main/proto/customer/api/customer_grpc_endpoint.proto diff --git a/samples/doc-snippets/src/main/java/com/example/api/ExampleGrpcEndpointImpl.java b/samples/doc-snippets/src/main/java/com/example/api/ExampleGrpcEndpointImpl.java index 4a520ed03..0c8538671 100644 --- a/samples/doc-snippets/src/main/java/com/example/api/ExampleGrpcEndpointImpl.java +++ b/samples/doc-snippets/src/main/java/com/example/api/ExampleGrpcEndpointImpl.java @@ -5,9 +5,9 @@ import akka.stream.Materializer; import akka.stream.javadsl.Sink; import akka.stream.javadsl.Source; -import com.example.grpc.ExampleGrpcEndpoint; -import com.example.grpc.HelloReply; -import com.example.grpc.HelloRequest; +import com.example.proto.ExampleGrpcEndpoint; +import com.example.proto.HelloReply; +import com.example.proto.HelloRequest; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionStage; diff --git a/samples/doc-snippets/src/main/proto/com/example/example_grpc_endpoint.proto b/samples/doc-snippets/src/main/proto/com/example/example_grpc_endpoint.proto index 2f7734ec0..5b593c547 100644 --- a/samples/doc-snippets/src/main/proto/com/example/example_grpc_endpoint.proto +++ b/samples/doc-snippets/src/main/proto/com/example/example_grpc_endpoint.proto @@ -1,8 +1,7 @@ syntax = "proto3"; option java_multiple_files = true; -option java_package = "com.example.grpc"; -option java_outer_classname = "ExampleGrpc"; +option java_package = "com.example.proto"; package com.example; diff --git a/samples/event-sourced-customer-registry/src/main/java/customer/api/CustomerEndpoint.java b/samples/event-sourced-customer-registry/src/main/java/customer/api/CustomerEndpoint.java index 86d20e82a..2d037297a 100644 --- a/samples/event-sourced-customer-registry/src/main/java/customer/api/CustomerEndpoint.java +++ b/samples/event-sourced-customer-registry/src/main/java/customer/api/CustomerEndpoint.java @@ -76,14 +76,14 @@ public CompletionStage changeAddress(String customerId, Address ne } @Get("/by-name/{name}") - public CompletionStage userByName(String name) { + public CompletionStage customerByName(String name) { return componentClient.forView() .method(CustomerByNameView::getCustomers) .invokeAsync(name); } @Get("/by-email/{email}") - public CompletionStage userByEmail(String email) { + public CompletionStage customerByEmail(String email) { return componentClient.forView() .method(CustomerByEmailView::getCustomers) .invokeAsync(email); diff --git a/samples/event-sourced-customer-registry/src/main/java/customer/api/CustomerGrpcEndpointImpl.java b/samples/event-sourced-customer-registry/src/main/java/customer/api/CustomerGrpcEndpointImpl.java new file mode 100644 index 000000000..a0485a3d2 --- /dev/null +++ b/samples/event-sourced-customer-registry/src/main/java/customer/api/CustomerGrpcEndpointImpl.java @@ -0,0 +1,141 @@ +package customer.api; + +import akka.grpc.GrpcServiceException; +import akka.javasdk.annotations.GrpcEndpoint; +import akka.javasdk.client.ComponentClient; +import customer.api.proto.*; +import customer.application.CustomerByEmailView; +import customer.application.CustomerByNameView; +import customer.application.CustomerEntity; +import io.grpc.Status; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.concurrent.CompletionStage; + +@GrpcEndpoint +public class CustomerGrpcEndpointImpl implements CustomerGrpcEndpoint { + + private static final Logger log = LoggerFactory.getLogger(CustomerGrpcEndpointImpl.class); + + private final ComponentClient componentClient; + + public CustomerGrpcEndpointImpl(ComponentClient componentClient) { + this.componentClient = componentClient; + } + + @Override + public CompletionStage createCustomer(CreateCustomerRequest in) { + log.info("gRPC request to create customer: {}", in); + if (in.getCustomerId().isBlank()) + throw new IllegalArgumentException("Customer id must not be empty"); + + return componentClient.forEventSourcedEntity(in.getCustomerId()) + .method(CustomerEntity::create) + .invokeAsync(apiToDomain(in.getCustomer())) + .thenApply(__ -> CreateCustomerResponse.getDefaultInstance()); + } + + @Override + public CompletionStage getCustomer(GetCustomerRequest in) { + return componentClient.forEventSourcedEntity(in.getCustomerId()) + .method(CustomerEntity::getCustomer) + .invokeAsync() + .thenApply(this::domainToApi) + .exceptionally(ex -> { + if (ex.getMessage().contains("No customer found for id")) throw new GrpcServiceException(Status.NOT_FOUND); + else throw new RuntimeException(ex); + }); + } + + @Override + public CompletionStage changeName(ChangeNameRequest in) { + log.info("gRPC request to change customer [{}] name: {}", in.getCustomerId(), in.getNewName()); + return componentClient.forEventSourcedEntity(in.getCustomerId()) + .method(CustomerEntity::changeName) + .invokeAsync(in.getNewName()) + .thenApply(__ -> ChangeNameResponse.getDefaultInstance()); + } + + @Override + public CompletionStage changeAddress(ChangeAddressRequest in) { + log.info("gRPC request to change customer [{}] address: {}", in.getCustomerId(), in.getNewAddress()); + return componentClient.forEventSourcedEntity(in.getCustomerId()) + .method(CustomerEntity::changeAddress) + .invokeAsync(apiToDomain(in.getNewAddress())) + .thenApply(__ -> ChangeAddressResponse.getDefaultInstance()); + } + + // The two methods below are not necessarily realistic since we have the full result in one response, + // but provides examples of streaming a response + @Override + public CompletionStage customerByName(CustomerByNameRequest in) { + return componentClient.forView() + .method(CustomerByNameView::getCustomers) + .invokeAsync(name) + .thenApply(viewCustomerList -> { + var apiCustomers = viewCustomerList.customers().stream().map(this::domainToApi).toList(); + + return CustomerList.newBuilder().addAllCustomers(apiCustomers).build(); + }); + } + + @Override + public CompletionStage customerByEmail(CustomerByEmailRequest in) { + return componentClient.forView() + .method(CustomerByEmailView::getCustomers) + .invokeAsync(name) + .thenApply(viewCustomerList -> { + var apiCustomers = viewCustomerList.customers().stream().map(this::domainToApi).toList(); + + return CustomerList.newBuilder().addAllCustomers(apiCustomers).build(); + }); + } + + + // Conversions between the public gRPC API protobuf messages and the internal + // Java domain classes. + private customer.domain.Customer apiToDomain(Customer protoCustomer) { + return new customer.domain.Customer( + protoCustomer.getEmail(), + protoCustomer.getName(), + apiToDomain(protoCustomer.getAddress()) + ); + } + + private customer.domain.Address apiToDomain(Address protoAddress) { + if (protoAddress == null) return null; + else { + return new customer.domain.Address( + protoAddress.getStreet(), + protoAddress.getCity() + ); + } + } + + private Customer domainToApi(customer.domain.Customer domainCustomer) { + return Customer.newBuilder() + .setName(domainCustomer.name()) + .setEmail(domainCustomer.email()) + .setAddress(domainToApi(domainCustomer.address())) + .build(); + } + + private Address domainToApi(customer.domain.Address domainAddress) { + if (domainAddress == null) return null; + else { + return Address.newBuilder() + .setCity(domainAddress.city()) + .setStreet(domainAddress.street()) + .build(); + } + } + + private Customer domainToApi(customer.domain.CustomerRow domainRow) { + return Customer.newBuilder() + .setName(domainRow.name()) + .setEmail(domainRow.email()) + .setAddress(domainToApi(domainRow.address())) + .build(); + } +} diff --git a/samples/event-sourced-customer-registry/src/main/proto/customer/api/customer_grpc_endpoint.proto b/samples/event-sourced-customer-registry/src/main/proto/customer/api/customer_grpc_endpoint.proto new file mode 100644 index 000000000..7c2142971 --- /dev/null +++ b/samples/event-sourced-customer-registry/src/main/proto/customer/api/customer_grpc_endpoint.proto @@ -0,0 +1,81 @@ +syntax = "proto3"; + +option java_multiple_files = true; +option java_package = "customer.api.proto"; + +package com.example; + +message Address { + string street = 1; + string city = 2; +} + +message Customer { + string email = 1; + string name = 2; + Address address = 3; +} + +message CreateCustomerRequest { + string customer_id = 1; + Customer customer = 2; +} +message CreateCustomerResponse {} + +message GetCustomerRequest { + string customer_id = 1; +} + +message ChangeNameRequest { + string customer_id = 1; + string new_name = 2; +} +message ChangeNameResponse {} + +message ChangeAddressRequest { + string customer_id = 1; + Address new_address = 2; +} +message ChangeAddressResponse {} + +message CustomerByNameRequest { + string name = 1; +} + +message CustomerByEmailRequest { + string email = 1; +} + +message CustomerList { + repeated Customer customers = 1; +} + +message StreamCustomersRequest { +} + + +service CustomerGrpcEndpoint { + + rpc CreateCustomer (CreateCustomerRequest) returns (CreateCustomerResponse) {} + + rpc GetCustomer (GetCustomerRequest) returns (Customer) {} + + rpc ChangeName (ChangeNameRequest) returns (ChangeNameResponse) {} + + rpc ChangeAddress (ChangeAddressRequest) returns (ChangeAddressResponse) {} + + rpc CustomerByName (CustomerByNameRequest) returns (CustomerList) {} + + rpc CustomerByEmail (CustomerByEmailRequest) returns (CustomerList) {} + +} + +// The request message containing the user's name. +message HelloRequest { + string name = 1; +} + +// The response message containing the greetings +message HelloReply { + string message = 1; +} From 7e534150839567fd3275f4426931d925780b2f18 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johan=20Andr=C3=A9n?= Date: Thu, 23 Jan 2025 17:01:04 +0100 Subject: [PATCH 2/4] README for the sample and some fixes --- .../event-sourced-customer-registry/README.md | 77 ++++++++++++++++++- .../api/CustomerGrpcEndpointImpl.java | 4 +- .../customer/api/customer_grpc_endpoint.proto | 2 +- 3 files changed, 79 insertions(+), 4 deletions(-) diff --git a/samples/event-sourced-customer-registry/README.md b/samples/event-sourced-customer-registry/README.md index e36e0172f..aef8d8bab 100644 --- a/samples/event-sourced-customer-registry/README.md +++ b/samples/event-sourced-customer-registry/README.md @@ -36,7 +36,7 @@ To start your service locally, run: mvn compile exec:java ``` -## Steps +## Steps to interact with the HTTP endpoint ### 1. Create a new customer @@ -92,6 +92,81 @@ curl -i localhost:9000/customer/one/address \ --data '{"street":"Newstreet 25","city":"Newcity"}' ``` +## Steps to interact with the gRPC endpoint + +Requires the command line tool [grpcurl](https://github.com/fullstorydev/grpcurl) + +### 1. Inspect what services are available using gRPC reflection + +```shell +grpcurl --plaintext localhost:9000 list +``` + +Or a more detailed listing of each service and its methods: + +```shell +grpcurl --plaintext localhost:9000 describe +``` + +### 2. Create a new customer + +```shell +grpcurl --plaintext \ + -d '{"customer_id": "one", "customer": {"name": "Grpc Testsson", "email":"grpc@example.com", "address": {"street":"Example Street", "city": "Sample Town"}}}' \ + localhost:9000 customer.api.CustomerGrpcEndpoint/CreateCustomer +``` + +### 3. Retrieve customer information + +To retrieve details of a specific customer: + +```shell +grpcurl --plaintext \ + -d '{"customer_id": "one"}' \ + localhost:9000 customer.api.CustomerGrpcEndpoint/GetCustomer +``` + +### 4. Query customers by email + +To find a customer using their email address: + +```shell +grpcurl --plaintext \ + -d '{"email": "grpc@example.com"}' \ + localhost:9000 customer.api.CustomerGrpcEndpoint/CustomerByEmail +``` + +### 5. Query customers by name + +To search for a customer by their name: + +```shell +grpcurl --plaintext \ + -d '{"name": "Grpc Testsson"}' \ + localhost:9000 customer.api.CustomerGrpcEndpoint/CustomerByName +``` + +### 6. Update customer name + +To change a customer's name: + +```shell +grpcurl --plaintext \ + -d '{"customer_id": "one", "new_name": "Grpc Testsson 2"}' \ + localhost:9000 customer.api.CustomerGrpcEndpoint/ChangeName +``` + +### 7. Update customer address + +To modify a customer's address: + +```shell +grpcurl --plaintext \ + -d '{"customer_id": "one", "new_address": {"street":"Upper Example Lane", "city": "Sample Town"}}' \ + localhost:9000 customer.api.CustomerGrpcEndpoint/ChangeAddress +``` + + ## Troubleshooting If you encounter issues, ensure that: diff --git a/samples/event-sourced-customer-registry/src/main/java/customer/api/CustomerGrpcEndpointImpl.java b/samples/event-sourced-customer-registry/src/main/java/customer/api/CustomerGrpcEndpointImpl.java index a0485a3d2..8aec36d27 100644 --- a/samples/event-sourced-customer-registry/src/main/java/customer/api/CustomerGrpcEndpointImpl.java +++ b/samples/event-sourced-customer-registry/src/main/java/customer/api/CustomerGrpcEndpointImpl.java @@ -72,7 +72,7 @@ public CompletionStage changeAddress(ChangeAddressRequest public CompletionStage customerByName(CustomerByNameRequest in) { return componentClient.forView() .method(CustomerByNameView::getCustomers) - .invokeAsync(name) + .invokeAsync(in.getName()) .thenApply(viewCustomerList -> { var apiCustomers = viewCustomerList.customers().stream().map(this::domainToApi).toList(); @@ -84,7 +84,7 @@ public CompletionStage customerByName(CustomerByNameRequest in) { public CompletionStage customerByEmail(CustomerByEmailRequest in) { return componentClient.forView() .method(CustomerByEmailView::getCustomers) - .invokeAsync(name) + .invokeAsync(in.getEmail()) .thenApply(viewCustomerList -> { var apiCustomers = viewCustomerList.customers().stream().map(this::domainToApi).toList(); diff --git a/samples/event-sourced-customer-registry/src/main/proto/customer/api/customer_grpc_endpoint.proto b/samples/event-sourced-customer-registry/src/main/proto/customer/api/customer_grpc_endpoint.proto index 7c2142971..26a30567a 100644 --- a/samples/event-sourced-customer-registry/src/main/proto/customer/api/customer_grpc_endpoint.proto +++ b/samples/event-sourced-customer-registry/src/main/proto/customer/api/customer_grpc_endpoint.proto @@ -3,7 +3,7 @@ syntax = "proto3"; option java_multiple_files = true; option java_package = "customer.api.proto"; -package com.example; +package customer.api; message Address { string street = 1; From 0ff98ca4315f415b55cb643468a77a493396fa19 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johan=20Andr=C3=A9n?= Date: Thu, 23 Jan 2025 17:18:43 +0100 Subject: [PATCH 3/4] Basic docs --- .../modules/java/pages/grpc-endpoints.adoc | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 docs/src/modules/java/pages/grpc-endpoints.adoc diff --git a/docs/src/modules/java/pages/grpc-endpoints.adoc b/docs/src/modules/java/pages/grpc-endpoints.adoc new file mode 100644 index 000000000..f42b14d95 --- /dev/null +++ b/docs/src/modules/java/pages/grpc-endpoints.adoc @@ -0,0 +1,27 @@ += Designing gRPC Endpoints + +include::ROOT:partial$include.adoc[] + +gRPC Endpoint components make it possible to conveniently define public APIs accepting and responding in protobuf, +a binary, typed protocol which is designed to handle evolution of a service over time. + +== Basics == +To define a gRPC Endpoint component, you start by defining a `.proto` file that defines the service and its messages +in `src/main/proto` of the project. + +[source,protobuf] +.{sample-base-url}/doc-snippets/src/main/proto/com/example/example_grpc_endpoint.proto +---- +include::example$doc-snippets/src/main/proto/com/example/example_grpc_endpoint.proto[] +---- + +When compiling the project a Java interface for the service is generated `com.example.proto.ExampleGrpcEndpoint`. Define a class implementing this interface in the `api` package of your project +and annotate the class with `@GrpcEndpoint`: + +[source,java] +.{sample-base-url}/doc-snippets/src/main/java/ +---- +include::example$doc-snippets/src/main/java/com/example/api/ExampleGrpcEndpointImpl.java[] +---- + + From f22fce779d887a553494eb79c3b1fa8b0a21a9a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johan=20Andr=C3=A9n?= Date: Thu, 23 Jan 2025 17:19:51 +0100 Subject: [PATCH 4/4] add doc page to index --- docs/src/modules/java/pages/index.adoc | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/src/modules/java/pages/index.adoc b/docs/src/modules/java/pages/index.adoc index 4deac6050..5d2045fbe 100644 --- a/docs/src/modules/java/pages/index.adoc +++ b/docs/src/modules/java/pages/index.adoc @@ -30,6 +30,7 @@ On the other hand, if you would rather spend some time exploring our documentati * xref:event-sourced-entities.adoc[Event Sourced Entities] * xref:key-value-entities.adoc[Key Value Entities] * xref:http-endpoints.adoc[HTTP Endpoints] +* xref:grpc-endpoints.adoc[gRPC Endpoints] * xref:views.adoc[Views] * xref:workflows.adoc[Workflows] * xref:timed-actions.adoc[Timed Actions]