Cloudstate Dart library

Usage

A simple usage example:

Create new Dart project.

Add dependencies in pubspec.yml:

name: shopping_cart
version: 0.5.1
description: A Cloudstate Dart ShoppingCart Example.
author: Adriano Santos <sleipnir@bsd.com.br>

environment:
  sdk: '>=2.7.0 <3.0.0'

dependencies:
  cloudstate: ^0.5.1

dev_dependencies:
  pedantic: ^1.8.0
  build_runner: ^1.5.2
  build_test: ^0.10.8
  build_web_compilers: ^2.1.1
  mockito: ^4.1.0
  test: ^1.6.4

Write Protofiles:

// File => protos/shoppingcart.proto

// This is the public API offered by the shopping cart entity.
syntax = "proto3";

package com.example.shoppingcart;

import "google/protobuf/empty.proto";
import "cloudstate/entity_key.proto";
import "cloudstate/eventing.proto";
import "google/api/annotations.proto";
import "google/api/http.proto";
import "google/api/httpbody.proto";

message AddLineItem {
    string user_id = 1 [(.cloudstate.entity_key) = true];
    string product_id = 2;
    string name = 3;
    int32 quantity = 4;
}

message RemoveLineItem {
    string user_id = 1 [(.cloudstate.entity_key) = true];
    string product_id = 2;
}

message GetShoppingCart {
    string user_id = 1 [(.cloudstate.entity_key) = true];
}

message LineItem {
    string product_id = 1;
    string name = 2;
    int32 quantity = 3;
}

message Cart {
    repeated LineItem items = 1;
}

service ShoppingCart {
    rpc AddItem(AddLineItem) returns (google.protobuf.Empty) {
        option (google.api.http) = {
            post: "/cart/{user_id}/items/add",
            body: "*",
        };
        option (.cloudstate.eventing).in = "items";
    }

    rpc RemoveItem(RemoveLineItem) returns (google.protobuf.Empty) {
        option (google.api.http).post = "/cart/{user_id}/items/{product_id}/remove";
    }

    rpc GetCart(GetShoppingCart) returns (Cart) {
        option (google.api.http) = {
          get: "/carts/{user_id}",
          additional_bindings: {
            get: "/carts/{user_id}/items",
            response_body: "items"
          }
        };
    }
}

// File => protos/persistence/domain.proto
// These are the messages that get persisted - the events, plus the current state (Cart) for snapshots.
syntax = "proto3";

package com.example.shoppingcart.persistence;

message LineItem {
    string productId = 1;
    string name = 2;
    int32 quantity = 3;
}

// The item added event.
message ItemAdded {
    LineItem item = 1;
}

// The item removed event.
message ItemRemoved {
    string productId = 1;
}

// The shopping cart state.
message Cart {
    repeated LineItem items = 1;
}

Compiling your proto files:

[sleipnir@sleipnir example]# protoc --include_imports \
     --descriptor_set_out=user-function.desc \
     -I protos/persistence/domain.proto protos/shoppingcart.proto \
     --dart_out=grpc:lib/src/generated

Write file => lib/eventsourced_entity.dart:


import 'package:cloudstate/cloudstate.dart';

import 'generated/google/protobuf/empty.pb.dart';
import 'generated/persistence/domain.pb.dart' as Domain;
import 'generated/shoppingcart.pb.dart' as Shoppingcart;

@EventSourcedEntity()
class ShoppingCartEntity {
  final Map<String, Shoppingcart.LineItem> _cart = {};
  
  String entityId;
  Context context;
  
  ShoppingCartEntity.create(@EntityId() String entityId, Context context) {
    this.entityId = entityId;
    this.context = context;
  }

  @Snapshot()
  Domain.Cart snapshot() {
    return Domain.Cart.create()
        ..items.addAll(
            _cart.values.map((e) => convertShoppingItem(e))
            .toList());
  }

  @SnapshotHandler()
  void handleSnapshot(Domain.Cart cart) {
    _cart.clear();
    for (var item in cart.items) {
      _cart[item.productId] =  convert(item);
    }

  }

  @EventHandler()
  void itemAdded(Domain.ItemAdded itemAdded) {
    var item = _cart[itemAdded.item.productId];
    if (item == null) {
      item = convert(itemAdded.item);
    } else {
      item =
          item..quantity = item.quantity + itemAdded.item.quantity;
    }
    _cart[item.productId] = item;
  }

  @EventHandler()
  void itemRemoved(Domain.ItemRemoved itemRemoved) {
    _cart.remove(itemRemoved.productId);
  }

  @CommandHandler()
  Shoppingcart.Cart getCart() {
    return Shoppingcart.Cart.create()
        ..items.addAll(_cart.values);
  }

  @CommandHandler()
  Empty addItem(Shoppingcart.AddLineItem item, CommandContext ctx) {
    if (item.quantity <= 0) {
      ctx.fail('Cannot add negative quantity of to item ${item.productId}');
    }

    var lineIem = Domain.LineItem.create()
      ..productId = item.productId
      ..name = item.name
      ..quantity = item.quantity;

    ctx.emit(Domain.ItemAdded.create()..item = lineIem);
    return Empty.getDefault();
  }

  @CommandHandler()
  Empty removeItem(Shoppingcart.RemoveLineItem item, CommandContext ctx) {
    if (!_cart.containsKey(item.productId)) {
      ctx.fail('Cannot remove item ${item.productId} because it is not in the cart.');
    }
    ctx.emit(Domain.ItemRemoved.create()..productId = item.productId);
    return Empty.getDefault();
  }

  Shoppingcart.LineItem convert(Domain.LineItem item) {
    return Shoppingcart.LineItem.create()
        ..productId = item.productId
        ..name = item.name
        ..quantity = item.quantity;
  }

  Domain.LineItem convertShoppingItem(Shoppingcart.LineItem item) {
    return Domain.LineItem.create()
        ..productId = item.productId
        ..name = item.name
        ..quantity = item.quantity;
  }

}

Note: You are not required to create a constructor, but it is useful if you want to access the entity id or the EventSourcedCreatedContext. In this case, the class is only allowed to have only one constructor, which can be a normal constructor or a Named Constructor. Don't forget to annotate the entity parameter with the @EntityId annotation

Write file => bin/shopping_cart.dart:


import 'package:cloudstate/cloudstate.dart';
import 'package:shopping_cart/src/eventsourced_entity.dart';

void main() {
  Cloudstate()
    ..port = 8080
    ..address = 'localhost'
    ..registerEventSourcedEntity('com.example.shoppingcart.ShoppingCart', ShoppingCartEntity)
    ..start();
}

Build and run on docker:

docker build -t sleipnir/cloudstate-dart-shoppingcart:0.5.1

docker run --rm -p 8080:8080 -p 8181:8181 --name dart-shoppingcartsleipnir/cloudstate-dart-shoppingcart:0.5.1

Libraries

cloudstate
More dartdocs go here.