Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

doc: Sample and basic doc page #171

Merged
merged 4 commits into from
Jan 24, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions docs/src/modules/java/pages/grpc-endpoints.adoc
Original file line number Diff line number Diff line change
@@ -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[]
----


1 change: 1 addition & 0 deletions docs/src/modules/java/pages/index.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
@@ -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;

Expand Down
77 changes: 76 additions & 1 deletion samples/event-sourced-customer-registry/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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":"[email protected]", "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": "[email protected]"}' \
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:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,14 +76,14 @@ public CompletionStage<HttpResponse> changeAddress(String customerId, Address ne
}

@Get("/by-name/{name}")
public CompletionStage<CustomersList> userByName(String name) {
public CompletionStage<CustomersList> customerByName(String name) {
return componentClient.forView()
.method(CustomerByNameView::getCustomers)
.invokeAsync(name);
}

@Get("/by-email/{email}")
public CompletionStage<CustomersList> userByEmail(String email) {
public CompletionStage<CustomersList> customerByEmail(String email) {
return componentClient.forView()
.method(CustomerByEmailView::getCustomers)
.invokeAsync(email);
Expand Down
Original file line number Diff line number Diff line change
@@ -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<CreateCustomerResponse> 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<Customer> 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<ChangeNameResponse> 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<ChangeAddressResponse> 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<CustomerList> customerByName(CustomerByNameRequest in) {
return componentClient.forView()
.method(CustomerByNameView::getCustomers)
.invokeAsync(in.getName())
.thenApply(viewCustomerList -> {
var apiCustomers = viewCustomerList.customers().stream().map(this::domainToApi).toList();

return CustomerList.newBuilder().addAllCustomers(apiCustomers).build();
});
}

@Override
public CompletionStage<CustomerList> customerByEmail(CustomerByEmailRequest in) {
return componentClient.forView()
.method(CustomerByEmailView::getCustomers)
.invokeAsync(in.getEmail())
.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();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
syntax = "proto3";

option java_multiple_files = true;
option java_package = "customer.api.proto";

package customer.api;

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;
}
Loading