Skip to content

Commit 40fb5b3

Browse files
committed
polymorphism support POC
1 parent f0c0759 commit 40fb5b3

File tree

9 files changed

+359
-2
lines changed

9 files changed

+359
-2
lines changed

samples/shopping-cart-quickstart/pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
<parent>
66
<groupId>io.akka</groupId>
77
<artifactId>akka-javasdk-parent</artifactId>
8-
<version>3.0.2</version>
8+
<version>3.0.2-5-898ec6f9-dev-SNAPSHOT</version>
99
</parent>
1010

1111
<groupId>com.example</groupId>

samples/shopping-cart-quickstart/src/main/java/shoppingcart/api/ShoppingCartEndpoint.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
// For actual services meant for production this must be carefully considered, and often set more limited
2828
// tag::endpoint-component-interaction[]
2929
@Acl(allow = @Acl.Matcher(principal = Acl.Principal.INTERNET))
30-
@HttpEndpoint("/carts") // <1>
30+
@HttpEndpoint("/carts-old") // <1>
3131
public class ShoppingCartEndpoint {
3232

3333
private final ComponentClient componentClient;
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
// tag::top[]
2+
package shoppingcart.polymorphism.api;
3+
4+
import akka.http.javadsl.model.HttpResponse;
5+
import akka.javasdk.annotations.Acl;
6+
import akka.javasdk.annotations.http.Delete;
7+
import akka.javasdk.annotations.http.Get;
8+
import akka.javasdk.annotations.http.HttpEndpoint;
9+
import akka.javasdk.annotations.http.Post;
10+
import akka.javasdk.annotations.http.Put;
11+
import akka.javasdk.client.ComponentClient;
12+
import akka.javasdk.http.HttpResponses;
13+
import org.slf4j.Logger;
14+
import org.slf4j.LoggerFactory;
15+
import shoppingcart.domain.ShoppingCart.LineItem;
16+
import shoppingcart.polymorphism.application.ShoppingCartEntity;
17+
import shoppingcart.polymorphism.domain.ShoppingCart;
18+
import shoppingcart.polymorphism.domain.ShoppingCartCommand.CheckedOutShoppingCartCommand.Cancel;
19+
import shoppingcart.polymorphism.domain.ShoppingCartCommand.OpenShoppingCartCommand.AddItem;
20+
import shoppingcart.polymorphism.domain.ShoppingCartCommand.OpenShoppingCartCommand.Checkout;
21+
import shoppingcart.polymorphism.domain.ShoppingCartCommand.OpenShoppingCartCommand.RemoveItem;
22+
23+
import java.util.concurrent.CompletionStage;
24+
25+
// end::top[]
26+
27+
// tag::class[]
28+
29+
// Opened up for access from the public internet to make the sample service easy to try out.
30+
// For actual services meant for production this must be carefully considered, and often set more limited
31+
// tag::endpoint-component-interaction[]
32+
@Acl(allow = @Acl.Matcher(principal = Acl.Principal.INTERNET))
33+
@HttpEndpoint("/carts") // <1>
34+
public class ShoppingCartEndpoint {
35+
36+
private final ComponentClient componentClient;
37+
38+
private static final Logger logger = LoggerFactory.getLogger(ShoppingCartEndpoint.class);
39+
40+
public ShoppingCartEndpoint(ComponentClient componentClient) { // <2>
41+
this.componentClient = componentClient;
42+
}
43+
44+
// end::class[]
45+
46+
// tag::get[]
47+
@Get("/{cartId}") // <3>
48+
public CompletionStage<ShoppingCart> get(String cartId) {
49+
logger.info("Get cart id={}", cartId);
50+
return componentClient.forEventSourcedEntity(cartId) // <4>
51+
.method(ShoppingCartEntity::getCart)
52+
.invokeAsync(); // <5>
53+
}
54+
55+
// end::get[]
56+
57+
// tag::addItem[]
58+
@Put("/{cartId}/item") // <6>
59+
public CompletionStage<HttpResponse> addItem(String cartId, LineItem item) {
60+
logger.info("Adding item to cart id={} item={}", cartId, item);
61+
return componentClient.forEventSourcedEntity(cartId)
62+
.method(ShoppingCartEntity::handleCommand)
63+
.invokeAsync(new AddItem(item))
64+
.thenApply(__ -> HttpResponses.ok()); // <7>
65+
}
66+
// end::endpoint-component-interaction[]
67+
68+
// end::addItem[]
69+
70+
@Delete("/{cartId}/item/{productId}")
71+
public CompletionStage<HttpResponse> removeItem(String cartId, String productId) {
72+
logger.info("Removing item from cart id={} item={}", cartId, productId);
73+
return componentClient.forEventSourcedEntity(cartId)
74+
.method(ShoppingCartEntity::handleCommand)
75+
.invokeAsync(new RemoveItem(productId))
76+
.thenApply(__ -> HttpResponses.ok());
77+
}
78+
79+
@Post("/{cartId}/checkout")
80+
public CompletionStage<HttpResponse> checkout(String cartId) {
81+
logger.info("Checkout cart id={}", cartId);
82+
return componentClient.forEventSourcedEntity(cartId)
83+
.method(ShoppingCartEntity::handleCommand)
84+
.invokeAsync(new Checkout())
85+
.thenApply(__ -> HttpResponses.ok());
86+
}
87+
88+
@Post("/{cartId}/cancel")
89+
public CompletionStage<HttpResponse> cancel(String cartId) {
90+
logger.info("Checkout cart id={}", cartId);
91+
return componentClient.forEventSourcedEntity(cartId)
92+
.method(ShoppingCartEntity::handleCommand)
93+
.invokeAsync(new Cancel())
94+
.thenApply(__ -> HttpResponses.ok());
95+
}
96+
97+
// tag::class[]
98+
}
99+
// end::class[]
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
// tag::top[]
2+
package shoppingcart.polymorphism.application;
3+
4+
import akka.Done;
5+
import akka.javasdk.annotations.ComponentId;
6+
import akka.javasdk.eventsourcedentity.EventSourcedEntity;
7+
import akka.javasdk.eventsourcedentity.EventSourcedEntityContext;
8+
import org.slf4j.Logger;
9+
import org.slf4j.LoggerFactory;
10+
import shoppingcart.polymorphism.domain.OpenShoppingCart;
11+
import shoppingcart.polymorphism.domain.ShoppingCart;
12+
import shoppingcart.polymorphism.domain.ShoppingCartCommand;
13+
import shoppingcart.polymorphism.domain.ShoppingCartEvent;
14+
15+
import java.util.Collections;
16+
17+
18+
@ComponentId("shopping-cart-poly")
19+
public class ShoppingCartEntity extends EventSourcedEntity<ShoppingCart, ShoppingCartEvent> { // <1>
20+
21+
private final String entityId;
22+
23+
private static final Logger logger = LoggerFactory.getLogger(ShoppingCartEntity.class);
24+
25+
public ShoppingCartEntity(EventSourcedEntityContext context) {
26+
this.entityId = context.entityId(); // <1>
27+
}
28+
29+
@Override
30+
public ShoppingCart emptyState() { // <2>
31+
return new OpenShoppingCart(entityId, Collections.emptyList());
32+
}
33+
34+
public Effect<Done> handleCommand(ShoppingCartCommand command) {
35+
//todo some basic validation
36+
var event = currentState().handleCommand(command);
37+
38+
return effects().persist(event).thenReply(__ -> Done.done());
39+
}
40+
41+
public ReadOnlyEffect<ShoppingCart> getCart() {
42+
return effects().reply(currentState()); // <3>
43+
}
44+
45+
46+
@Override
47+
public ShoppingCart applyEvent(ShoppingCartEvent event) {
48+
return currentState().applyEvent(event);
49+
}
50+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
package shoppingcart.polymorphism.domain;
2+
3+
import shoppingcart.domain.ShoppingCart.LineItem;
4+
import shoppingcart.polymorphism.domain.ShoppingCartCommand.CheckedOutShoppingCartCommand;
5+
import shoppingcart.polymorphism.domain.ShoppingCartCommand.CheckedOutShoppingCartCommand.Cancel;
6+
import shoppingcart.polymorphism.domain.ShoppingCartCommand.OpenShoppingCartCommand;
7+
import shoppingcart.polymorphism.domain.ShoppingCartEvent.CheckedOutShoppingCartEvent;
8+
import shoppingcart.polymorphism.domain.ShoppingCartEvent.CheckedOutShoppingCartEvent.Cancelled;
9+
import shoppingcart.polymorphism.domain.ShoppingCartEvent.OpenShoppingCartEvent;
10+
11+
import java.util.List;
12+
13+
public record CheckedOutShoppingCart(String cartId, List<LineItem> items, Boolean cancelled) implements ShoppingCart {
14+
15+
@Override
16+
public ShoppingCartEvent handleCommand(ShoppingCartCommand command) {
17+
return switch (command) {
18+
case OpenShoppingCartCommand __ -> throw new IllegalStateException("Cannot handle command on a checked out cart");
19+
case CheckedOutShoppingCartCommand checkedOutCommand -> switch (checkedOutCommand) {
20+
case Cancel cancel -> new Cancelled();
21+
};
22+
};
23+
}
24+
25+
@Override
26+
public ShoppingCart applyEvent(ShoppingCartEvent event) {
27+
return switch (event) {
28+
case OpenShoppingCartEvent __ -> {
29+
throw new IllegalStateException("Cannot apply event on a checkout out cart");
30+
}
31+
case CheckedOutShoppingCartEvent checkedOutEvent -> switch (checkedOutEvent) {
32+
case Cancelled cancelled -> new CheckedOutShoppingCart(cartId, items, true);
33+
};
34+
};
35+
}
36+
}
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
package shoppingcart.polymorphism.domain;
2+
3+
import shoppingcart.domain.ShoppingCart.LineItem;
4+
import shoppingcart.polymorphism.domain.ShoppingCartCommand.CheckedOutShoppingCartCommand;
5+
import shoppingcart.polymorphism.domain.ShoppingCartCommand.OpenShoppingCartCommand;
6+
import shoppingcart.polymorphism.domain.ShoppingCartCommand.OpenShoppingCartCommand.AddItem;
7+
import shoppingcart.polymorphism.domain.ShoppingCartCommand.OpenShoppingCartCommand.Checkout;
8+
import shoppingcart.polymorphism.domain.ShoppingCartCommand.OpenShoppingCartCommand.RemoveItem;
9+
import shoppingcart.polymorphism.domain.ShoppingCartEvent.CheckedOutShoppingCartEvent;
10+
import shoppingcart.polymorphism.domain.ShoppingCartEvent.OpenShoppingCartEvent;
11+
import shoppingcart.polymorphism.domain.ShoppingCartEvent.OpenShoppingCartEvent.CheckedOut;
12+
import shoppingcart.polymorphism.domain.ShoppingCartEvent.OpenShoppingCartEvent.ItemAdded;
13+
import shoppingcart.polymorphism.domain.ShoppingCartEvent.OpenShoppingCartEvent.ItemRemoved;
14+
15+
import java.util.Comparator;
16+
import java.util.List;
17+
import java.util.Optional;
18+
import java.util.function.Predicate;
19+
import java.util.stream.Collectors;
20+
21+
public record OpenShoppingCart(String cartId, List<LineItem> items) implements ShoppingCart {
22+
23+
@Override
24+
public ShoppingCartEvent handleCommand(ShoppingCartCommand command) {
25+
return switch (command) {
26+
case CheckedOutShoppingCartCommand __ -> throw new IllegalStateException("Cannot handle command on an open cart");
27+
case OpenShoppingCartCommand openCommand -> switch (openCommand) {
28+
case AddItem addItem -> new ItemAdded(addItem.item());
29+
case RemoveItem removeItem -> new ItemRemoved(removeItem.productId());
30+
case Checkout checkout -> new CheckedOut();
31+
};
32+
};
33+
}
34+
35+
@Override
36+
public ShoppingCart applyEvent(ShoppingCartEvent event) {
37+
return switch (event) {
38+
case CheckedOutShoppingCartEvent __ -> {
39+
throw new IllegalStateException("Cannot apply event on an open cart");
40+
}
41+
case OpenShoppingCartEvent openEvent -> switch (openEvent) {
42+
case CheckedOut checkedOut -> onCheckedOut();
43+
case ItemAdded itemAdded -> onItemAdded(itemAdded);
44+
case ItemRemoved itemRemoved -> onItemRemoved(itemRemoved);
45+
};
46+
};
47+
}
48+
49+
50+
public OpenShoppingCart onItemAdded(ItemAdded itemAdded) {
51+
var item = itemAdded.item();
52+
var lineItem = updateItem(item); // <1>
53+
List<LineItem> lineItems = removeItemByProductId(item.productId()); // <2>
54+
lineItems.add(lineItem); // <3>
55+
lineItems.sort(Comparator.comparing(LineItem::productId));
56+
return new OpenShoppingCart(cartId, lineItems); // <4>
57+
}
58+
59+
private LineItem updateItem(LineItem item) {
60+
return findItemByProductId(item.productId())
61+
.map(li -> li.withQuantity(li.quantity() + item.quantity()))
62+
.orElse(item);
63+
}
64+
65+
private List<LineItem> removeItemByProductId(String productId) {
66+
return items().stream()
67+
.filter(lineItem -> !lineItem.productId().equals(productId))
68+
.collect(Collectors.toList());
69+
}
70+
71+
public Optional<LineItem> findItemByProductId(String productId) {
72+
Predicate<LineItem> lineItemExists =
73+
lineItem -> lineItem.productId().equals(productId);
74+
return items.stream().filter(lineItemExists).findFirst();
75+
}
76+
77+
public OpenShoppingCart onItemRemoved(ItemRemoved itemRemoved) {
78+
List<LineItem> updatedItems =
79+
removeItemByProductId(itemRemoved.productId());
80+
updatedItems.sort(Comparator.comparing(LineItem::productId));
81+
return new OpenShoppingCart(cartId, updatedItems);
82+
}
83+
84+
public ShoppingCart onCheckedOut() {
85+
return new CheckedOutShoppingCart(cartId, items, false);
86+
}
87+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package shoppingcart.polymorphism.domain;
2+
3+
import com.fasterxml.jackson.annotation.JsonSubTypes;
4+
import com.fasterxml.jackson.annotation.JsonTypeInfo;
5+
6+
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "@type")
7+
@JsonSubTypes({
8+
@JsonSubTypes.Type(value = OpenShoppingCart.class, name = "open"),
9+
@JsonSubTypes.Type(value = CheckedOutShoppingCart.class, name = "checked-out")})
10+
public sealed interface ShoppingCart permits OpenShoppingCart, CheckedOutShoppingCart {
11+
12+
ShoppingCartEvent handleCommand(ShoppingCartCommand command);
13+
14+
ShoppingCart applyEvent(ShoppingCartEvent event);
15+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
package shoppingcart.polymorphism.domain;
2+
3+
import com.fasterxml.jackson.annotation.JsonSubTypes;
4+
import com.fasterxml.jackson.annotation.JsonTypeInfo;
5+
import shoppingcart.domain.ShoppingCart;
6+
7+
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "@type")
8+
@JsonSubTypes({
9+
@JsonSubTypes.Type(value = ShoppingCartCommand.OpenShoppingCartCommand.class, name = "open"),
10+
@JsonSubTypes.Type(value = ShoppingCartCommand.CheckedOutShoppingCartCommand.class, name = "checked-out")})
11+
public sealed interface ShoppingCartCommand {
12+
13+
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "@type")
14+
@JsonSubTypes({
15+
@JsonSubTypes.Type(value = OpenShoppingCartCommand.AddItem.class, name = "a"),
16+
@JsonSubTypes.Type(value = OpenShoppingCartCommand.RemoveItem.class, name = "r"),
17+
@JsonSubTypes.Type(value = OpenShoppingCartCommand.Checkout.class, name = "c")})
18+
sealed interface OpenShoppingCartCommand extends ShoppingCartCommand {
19+
record AddItem(ShoppingCart.LineItem item) implements OpenShoppingCartCommand {
20+
}
21+
22+
record RemoveItem(String productId) implements OpenShoppingCartCommand {
23+
}
24+
25+
record Checkout() implements OpenShoppingCartCommand {
26+
}
27+
}
28+
29+
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "@type")
30+
@JsonSubTypes({
31+
@JsonSubTypes.Type(value = CheckedOutShoppingCartCommand.Cancel.class, name = "cc")})
32+
sealed interface CheckedOutShoppingCartCommand extends ShoppingCartCommand {
33+
34+
record Cancel() implements CheckedOutShoppingCartCommand {
35+
}
36+
}
37+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
package shoppingcart.polymorphism.domain;
2+
3+
import akka.javasdk.annotations.TypeName;
4+
import com.fasterxml.jackson.annotation.JsonSubTypes;
5+
import com.fasterxml.jackson.annotation.JsonTypeInfo;
6+
import shoppingcart.domain.ShoppingCart;
7+
8+
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "@type")
9+
@JsonSubTypes({
10+
@JsonSubTypes.Type(value = ShoppingCartEvent.OpenShoppingCartEvent.class, name = "open"),
11+
@JsonSubTypes.Type(value = ShoppingCartEvent.CheckedOutShoppingCartEvent.class, name = "checked-out")})
12+
public sealed interface ShoppingCartEvent {
13+
14+
sealed interface OpenShoppingCartEvent extends ShoppingCartEvent {
15+
@TypeName("item-added") // <2>
16+
record ItemAdded(ShoppingCart.LineItem item) implements OpenShoppingCartEvent {
17+
}
18+
19+
@TypeName("item-removed")
20+
record ItemRemoved(String productId) implements OpenShoppingCartEvent {
21+
}
22+
23+
@TypeName("checked-out")
24+
record CheckedOut() implements OpenShoppingCartEvent {
25+
}
26+
}
27+
28+
sealed interface CheckedOutShoppingCartEvent extends ShoppingCartEvent {
29+
@TypeName("cancelled")
30+
record Cancelled() implements CheckedOutShoppingCartEvent {
31+
}
32+
}
33+
}

0 commit comments

Comments
 (0)