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

Simple made simpler #43

Draft
wants to merge 4 commits into
base: main
Choose a base branch
from
Draft
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
2 changes: 1 addition & 1 deletion .github/workflows/samples_event-sourcing-esdb-simple.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ jobs:
strategy:
# define the test matrix
matrix:
java-version: [17, 18]
java-version: [21]

steps:
- name: Check Out Repo
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,38 +3,44 @@
import io.eventdriven.ecommerce.api.requests.ShoppingCartsRequests;
import io.eventdriven.ecommerce.core.entities.CommandHandler;
import io.eventdriven.ecommerce.core.http.ETag;
import io.eventdriven.ecommerce.shoppingcarts.*;
import io.eventdriven.ecommerce.pricing.ProductPriceCalculator;
import io.eventdriven.ecommerce.shoppingcarts.ShoppingCart;
import io.eventdriven.ecommerce.shoppingcarts.ShoppingCartCommand;
import io.eventdriven.ecommerce.shoppingcarts.ShoppingCartEvent;
import io.eventdriven.ecommerce.shoppingcarts.productitems.PricedProductItem;
import io.eventdriven.ecommerce.shoppingcarts.productitems.ProductItem;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.enums.ParameterIn;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotNull;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;

import jakarta.validation.Valid;
import jakarta.validation.constraints.NotNull;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.UUID;

import static com.eventstore.dbclient.ExpectedRevision.noStream;
import static com.eventstore.dbclient.ExpectedRevision.expectedRevision;
import static com.eventstore.dbclient.ExpectedRevision.noStream;
import static io.eventdriven.ecommerce.shoppingcarts.ShoppingCartCommand.*;

@Validated
@RestController
@RequestMapping("api/shopping-carts")
class ShoppingCartsCommandController {
private final CommandHandler<ShoppingCart, ShoppingCartCommand, ShoppingCartEvent> store;
private final ProductPriceCalculator productPriceCalculator;

ShoppingCartsCommandController(
CommandHandler<ShoppingCart, ShoppingCartCommand, ShoppingCartEvent> store
CommandHandler<ShoppingCart, ShoppingCartCommand, ShoppingCartEvent> store,
ProductPriceCalculator productPriceCalculator
) {
this.store = store;
this.productPriceCalculator = productPriceCalculator;
}

@PostMapping
Expand All @@ -46,10 +52,7 @@ ResponseEntity<Void> open(

var result = store.handle(
cartId,
new OpenShoppingCart(
cartId,
request.clientId()
),
new Open(request.clientId()),
noStream()
);

Expand All @@ -68,15 +71,16 @@ ResponseEntity<Void> addProduct(
if (request.productItem() == null)
throw new IllegalArgumentException("Product Item has to be defined");

var productItem = productPriceCalculator.calculate(
new ProductItem(
request.productItem().productId(),
request.productItem().quantity()
)
);

var result = store.handle(
id,
new AddProductItemToShoppingCart(
id,
new ProductItem(
request.productItem().productId(),
request.productItem().quantity()
)
),
new AddProductItem(productItem),
expectedRevision(ifMatch.toLong())
);

Expand All @@ -94,18 +98,18 @@ ResponseEntity<Void> removeProduct(
@RequestParam @NotNull Double price,
@RequestHeader(name = HttpHeaders.IF_MATCH) @Parameter(in = ParameterIn.HEADER, required = true, schema = @Schema(type = "string")) @NotNull ETag ifMatch
) {

var productItem = new PricedProductItem(
new ProductItem(
productId,
quantity
),
price
);

var result = store.handle(
id,
new RemoveProductItemFromShoppingCart(
id,
new PricedProductItem(
new ProductItem(
productId,
quantity
),
price
)
),
new RemoveProductItem(productItem),
expectedRevision(ifMatch.toLong())
);

Expand All @@ -122,7 +126,7 @@ ResponseEntity<Void> confirmCart(
) {
var result = store.handle(
id,
new ConfirmShoppingCart(id),
new Confirm(),
expectedRevision(ifMatch.toLong())
);

Expand All @@ -139,7 +143,7 @@ ResponseEntity<Void> cancelCart(
) {
var result = store.handle(
id,
new CancelShoppingCart(id),
new Cancel(),
expectedRevision(ifMatch.toLong())
);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ public ETag handle(
EventSerializer.serialize(event)
).get();

return toETag(result.getNextExpectedRevision());
return ETag.weak(result.getNextExpectedRevision().toString());
} catch (Throwable e) {
throw new RuntimeException(e);
}
Expand Down Expand Up @@ -98,12 +98,4 @@ private Optional<List<Event>> getEvents(String streamId) {

return Optional.of(events);
}

//This ugly hack is needed as ESDB Java client from v4 doesn't allow to access or serialise version in an elegant manner
private ETag toETag(ExpectedRevision nextExpectedRevision) throws NoSuchFieldException, IllegalAccessException {
var field = nextExpectedRevision.getClass().getDeclaredField("version");
field.setAccessible(true);

return ETag.weak(field.get(nextExpectedRevision));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ public static <Event> Optional<EventEnvelope<Event>> of(final Class<Event> type,
new EventEnvelope<>(
eventData.get(),
new EventMetadata(
resolvedEvent.getEvent().getStreamId(),
resolvedEvent.getEvent().getEventId().toString(),
resolvedEvent.getEvent().getRevision(),
resolvedEvent.getEvent().getPosition().getCommitUnsigned(),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
package io.eventdriven.ecommerce.core.events;

public record EventMetadata(
String streamName,
String eventId,
long streamPosition,
long logPosition,
String eventType
) {
public String streamId(){
return streamName.substring(streamName.indexOf("-") + 1);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,101 +3,63 @@
import io.eventdriven.ecommerce.shoppingcarts.ShoppingCartEvent.*;
import io.eventdriven.ecommerce.shoppingcarts.productitems.ProductItems;

import java.time.OffsetDateTime;
import java.util.UUID;

sealed public interface ShoppingCart {
record Initial() implements ShoppingCart {
record Empty() implements ShoppingCart {
}

record Pending(
UUID id,
UUID clientId,
ProductItems productItems
) implements ShoppingCart {
}

record Confirmed(
UUID id,
UUID clientId,
ProductItems productItems,
OffsetDateTime confirmedAt
) implements ShoppingCart {
}

record Canceled(
UUID id,
UUID clientId,
ProductItems productItems,
OffsetDateTime canceledAt
) implements ShoppingCart {
}

default boolean isClosed() {
return this instanceof Confirmed || this instanceof Canceled;
record Closed() implements ShoppingCart {
}

static ShoppingCart when(ShoppingCart current, ShoppingCartEvent event) {
static ShoppingCart evolve(ShoppingCart state, ShoppingCartEvent event) {
return switch (event) {
case ShoppingCartOpened shoppingCartOpened: {
if (!(current instanceof Initial))
yield current;
case Opened ignore: {
if (!(state instanceof Empty))
yield state;

yield new Pending(
shoppingCartOpened.shoppingCartId(),
shoppingCartOpened.clientId(),
ProductItems.empty()
);
yield new Pending(ProductItems.empty());
}
case ProductItemAddedToShoppingCart productItemAddedToShoppingCart: {
if (!(current instanceof Pending pending))
yield current;
case ProductItemAdded(var productItem): {
if (!(state instanceof Pending pending))
yield state;

yield new Pending(
pending.id(),
pending.clientId(),
pending.productItems().add(productItemAddedToShoppingCart.productItem())
pending.productItems().with(productItem)
);
}
case ProductItemRemovedFromShoppingCart productItemRemovedFromShoppingCart: {
if (!(current instanceof Pending pending))
yield current;
case ProductItemRemoved(var productItem): {
if (!(state instanceof Pending pending))
yield state;

yield new Pending(
pending.id(),
pending.clientId(),
pending.productItems().remove(productItemRemovedFromShoppingCart.productItem())
pending.productItems().without(productItem)
);
}
case ShoppingCartConfirmed shoppingCartConfirmed: {
if (!(current instanceof Pending pending))
yield current;
case Confirmed ignore: {
if (!(state instanceof Pending))
yield state;

yield new Confirmed(
pending.id(),
pending.clientId(),
pending.productItems(),
shoppingCartConfirmed.confirmedAt()
);
yield new Closed();
}
case ShoppingCartCanceled shoppingCartCanceled: {
if (!(current instanceof Pending pending))
yield current;
case Canceled ignore: {
if (!(state instanceof Pending))
yield state;

yield new Canceled(
pending.id(),
pending.clientId(),
pending.productItems(),
shoppingCartCanceled.canceledAt()
);
yield new Closed();
}
case null:
throw new IllegalArgumentException("Event cannot be null!");
};
}

static ShoppingCart empty() {
return new Initial();
return new Empty();
}

static String mapToStreamId(UUID shoppingCartId) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,36 +1,28 @@
package io.eventdriven.ecommerce.shoppingcarts;

import io.eventdriven.ecommerce.shoppingcarts.productitems.PricedProductItem;
import io.eventdriven.ecommerce.shoppingcarts.productitems.ProductItem;

import java.util.UUID;

public sealed interface ShoppingCartCommand {
record OpenShoppingCart(
UUID shoppingCartId,
record Open(
UUID clientId
) implements ShoppingCartCommand {
}

record AddProductItemToShoppingCart(
UUID shoppingCartId,
ProductItem productItem
record AddProductItem(
PricedProductItem productItem
) implements ShoppingCartCommand {
}

record ConfirmShoppingCart(
UUID shoppingCartId
record RemoveProductItem(
PricedProductItem productItem
) implements ShoppingCartCommand {
}

record RemoveProductItemFromShoppingCart(
UUID shoppingCartId,
PricedProductItem productItem
) implements ShoppingCartCommand {
record Confirm() implements ShoppingCartCommand {
}

record CancelShoppingCart(
UUID shoppingCartId
) implements ShoppingCartCommand {
record Cancel() implements ShoppingCartCommand {
}
}
Loading
Loading