Skip to content

Commit

Permalink
Merge pull request #2016 from lightbend/workflows-proto-api
Browse files Browse the repository at this point in the history
workflows in proto sdks
  • Loading branch information
aludwiko authored Feb 7, 2024
2 parents 7ba46a8 + 77fcbdc commit 8cbf249
Show file tree
Hide file tree
Showing 383 changed files with 9,753 additions and 1,548 deletions.
6 changes: 6 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -589,6 +589,9 @@ jobs:
- { sample: java-spring-transfer-workflow, it: true }
- { sample: java-spring-transfer-workflow-compensation, it: true }

- { sample: java-protobuf-transfer-workflow, it: true }
- { sample: java-protobuf-transfer-workflow-compensation, it: true }

steps:
- name: Checkout
# v3.1.0
Expand Down Expand Up @@ -698,6 +701,9 @@ jobs:

- { sample: scala-protobuf-web-resources, test: true }

- { sample: scala-protobuf-transfer-workflow, test: true }
- { sample: scala-protobuf-transfer-workflow-compensation, test: true }

steps:
- name: Checkout
# v3.1.0
Expand Down
1 change: 1 addition & 0 deletions codegen/core/src/main/scala/kalix/codegen/Log.scala
Original file line number Diff line number Diff line change
Expand Up @@ -21,5 +21,6 @@ trait Log {
// so there debug/info messages are either silent or println'ed, and
// all other problems should be fatal.
def debug(message: String): Unit
def warn(message: String): Unit
def info(message: String): Unit
}
99 changes: 71 additions & 28 deletions codegen/core/src/main/scala/kalix/codegen/ModelBuilder.scala
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@
package kalix.codegen

import java.util.Locale

import scala.jdk.CollectionConverters._

import com.google.common.base.CaseFormat
import com.google.protobuf.Descriptors
import com.google.protobuf.Descriptors.ServiceDescriptor
Expand All @@ -27,6 +29,7 @@ import kalix.ReplicatedEntityDef
import kalix.ServiceOptions.ServiceType
import kalix.ValueEntityDef
import kalix.View
import kalix.WorkflowDef

/**
* Builds a model of entities and their properties from a protobuf descriptor
Expand All @@ -52,36 +55,37 @@ object ModelBuilder {
def fromService(service: Service): Model =
Model.empty.addService(service)

def fromEntity(entity: Entity): Model =
def fromEntity(entity: StatefulComponent): Model =
Model.empty.addEntity(entity)
}

/**
* The Kalix service definitions and entities that could be extracted from a protobuf descriptor
*/
case class Model(services: Map[String, Service], entities: Map[String, Entity]) {
def lookupEntity(service: EntityService): Entity = {
entities.getOrElse(
case class Model(services: Map[String, Service], statefulComponents: Map[String, StatefulComponent]) {
def lookupEntity(service: EntityService): StatefulComponent = {
statefulComponents.getOrElse(
service.componentFullName,
throw new IllegalArgumentException(
"Service [" + service.messageType.fullyQualifiedProtoName + "] refers to entity [" + service.componentFullName +
s"], but no entity configuration is found for that component name. Entities: [${entities.keySet.mkString(", ")}]"))
s"Service [${service.messageType.fullyQualifiedProtoName}] refers to stateful component [${service.componentFullName}" +
s"], but no component configuration is found for that component name. Components: [${statefulComponents.keySet
.mkString(", ")}]"))
}

def addService(service: Service): Model =
copy(services + (service.messageType.fullyQualifiedProtoName -> service), entities)
copy(services + (service.messageType.fullyQualifiedProtoName -> service), statefulComponents)

def addEntity(entity: Entity): Model =
copy(services, entities + (entity.messageType.fullyQualifiedProtoName -> entity))
def addEntity(entity: StatefulComponent): Model =
copy(services, statefulComponents + (entity.messageType.fullyQualifiedProtoName -> entity))

def ++(model: Model): Model =
Model(services ++ model.services, entities ++ model.entities)
Model(services ++ model.services, statefulComponents ++ model.statefulComponents)
}

/**
* An entity represents the primary model object and is conceptually equivalent to a class, or a type of state.
*/
sealed abstract class Entity(val messageType: ProtoMessageType, val entityType: String) {
sealed abstract class StatefulComponent(val messageType: ProtoMessageType, val typeId: String) {
val abstractEntityName = "Abstract" + messageType.name
val routerName = messageType.name + "Router"
val providerName = messageType.name + "Provider"
Expand All @@ -94,25 +98,28 @@ object ModelBuilder {
*/
case class EventSourcedEntity(
override val messageType: ProtoMessageType,
override val entityType: String,
override val typeId: String,
state: State,
events: Iterable[Event])
extends Entity(messageType, entityType)
extends StatefulComponent(messageType, typeId)

case class WorkflowComponent(override val messageType: ProtoMessageType, override val typeId: String, state: State)
extends StatefulComponent(messageType, typeId)

/**
* A type of Entity that stores its current state directly.
*/
case class ValueEntity(override val messageType: ProtoMessageType, override val entityType: String, state: State)
extends Entity(messageType, entityType)
case class ValueEntity(override val messageType: ProtoMessageType, override val typeId: String, state: State)
extends StatefulComponent(messageType, typeId)

/**
* A type of Entity that replicates its current state using CRDTs.
*/
case class ReplicatedEntity(
override val messageType: ProtoMessageType,
override val entityType: String,
override val typeId: String,
data: ReplicatedData)
extends Entity(messageType, entityType)
extends StatefulComponent(messageType, typeId)

/**
* The underlying replicated data type for a Replicated Entity.
Expand Down Expand Up @@ -214,7 +221,7 @@ object ModelBuilder {
* A Service backed by Kalix; either an Action, View or Entity
*/
sealed abstract class Service(val messageType: ProtoMessageType, val commands: Iterable[Command]) {
lazy val commandTypes =
lazy val commandTypes: Iterable[ProtoMessageType] =
commands.flatMap { cmd =>
cmd.inputType :: cmd.outputType :: Nil
}
Expand Down Expand Up @@ -632,6 +639,13 @@ object ModelBuilder {
.fromService(EntityService(serviceName, commands, componentFullName))
.addEntity(extractEventSourcedEntity(serviceDescriptor, entityDef, additionalDescriptors))

case CodegenOptions.CodegenCase.WORKFLOW =>
val workflowDef = codegenOptions.getWorkflow
val componentFullName = resolveFullComponentName(workflowDef.getName, serviceName)
Model
.fromService(EntityService(serviceName, commands, componentFullName))
.addEntity(extractWorkflowComponent(serviceDescriptor, workflowDef, additionalDescriptors))

case CodegenOptions.CodegenCase.REPLICATED_ENTITY =>
val entityDef = codegenOptions.getReplicatedEntity
val componentFullName = resolveFullComponentName(entityDef.getName, serviceName)
Expand All @@ -648,7 +662,7 @@ object ModelBuilder {
* otherwise, we need to resolve the entity name.
*/
private def resolveFullComponentName(optionalName: String, serviceName: ProtoMessageType) = {
val messageType = defineEntityMessageType(optionalName, serviceName)
val messageType = defineStatefulComponentMessageType(optionalName, serviceName)
resolveFullName(messageType.parent.protoPackage, messageType.name)
}

Expand All @@ -670,15 +684,18 @@ object ModelBuilder {
ProtoMessageType(resolvedName, resolvedName, packageNaming, descOpt)
}

private def defineEntityMessageType(optionalName: String, serviceName: ProtoMessageType) =
private def defineStatefulComponentMessageType(
optionalName: String,
serviceName: ProtoMessageType,
postfix: String = "Entity") =
buildUserDefinedMessageType(optionalName, serviceName)
.getOrElse {
// when an entity name is not explicitly defined, we need to fabricate a unique name
// that doesn't conflict with the service name (since we do generate a grpc service for it)
// therefore we append 'Entity' to the name
serviceName
.deriveName(_ + "Entity")
.copy(protoName = serviceName.protoName + "Entity")
.deriveName(_ + postfix)
.copy(protoName = serviceName.protoName + postfix)
}

private def extractEventSourcedEntity(
Expand All @@ -690,14 +707,29 @@ object ModelBuilder {

val protoPackageName = serviceProtoDescriptor.getFile.getPackage

val typeId = getTypeId(entityDef.getEntityType, entityDef.getTypeId)
EventSourcedEntity(
defineEntityMessageType(entityDef.getName, messageExtractor(serviceProtoDescriptor)),
entityDef.getEntityType,
defineStatefulComponentMessageType(entityDef.getName, messageExtractor(serviceProtoDescriptor)),
typeId,
State(resolveMessageType(entityDef.getState, protoPackageName, additionalDescriptors)),
entityDef.getEventsList.asScala.map { event =>
Event(resolveMessageType(event, protoPackageName, additionalDescriptors))
})
}

private def extractWorkflowComponent(
serviceProtoDescriptor: ServiceDescriptor,
workflowDef: WorkflowDef,
additionalDescriptors: Seq[Descriptors.FileDescriptor])(implicit
log: Log,
messageExtractor: ProtoMessageTypeExtractor): WorkflowComponent = {

val protoPackageName = serviceProtoDescriptor.getFile.getPackage

WorkflowComponent(
defineStatefulComponentMessageType(workflowDef.getName, messageExtractor(serviceProtoDescriptor), "Workflow"),
workflowDef.getTypeId,
State(resolveMessageType(workflowDef.getState, protoPackageName, additionalDescriptors)))
}

private def extractValueEntity(
Expand All @@ -707,10 +739,11 @@ object ModelBuilder {
log: Log,
messageExtractor: ProtoMessageTypeExtractor): ValueEntity = {

val typeId = getTypeId(entityDef.getEntityType, entityDef.getTypeId)
val protoPackageName = serviceProtoDescriptor.getFile.getPackage
ValueEntity(
defineEntityMessageType(entityDef.getName, messageExtractor(serviceProtoDescriptor)),
entityDef.getEntityType,
defineStatefulComponentMessageType(entityDef.getName, messageExtractor(serviceProtoDescriptor)),
typeId,
State(resolveMessageType(entityDef.getState, protoPackageName, additionalDescriptors)))
}

Expand Down Expand Up @@ -769,12 +802,22 @@ object ModelBuilder {
throw new IllegalArgumentException("Replicated data type not set")
}

val typeId = getTypeId(entityDef.getEntityType, entityDef.getTypeId)
ReplicatedEntity(
defineEntityMessageType(entityDef.getName, messageExtractor(serviceProtoDescriptor)),
entityDef.getEntityType,
defineStatefulComponentMessageType(entityDef.getName, messageExtractor(serviceProtoDescriptor)),
typeId,
dataType)
}

private def getTypeId(entityType: String, typeId: String)(implicit log: Log): String = {
if (entityType != "") {
log.warn(s"""Using 'entity_type: "$entityType"' is deprecated, replace it with 'type_id: "$entityType"'""")
entityType
} else {
typeId
}
}

private def modelFromServiceOptions(serviceDescriptor: Descriptors.ServiceDescriptor)(implicit
log: Log,
messageExtractor: ProtoMessageTypeExtractor): Model = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ object SourceGeneratorUtils {
|// As long as this file exists it will not be overwritten: you can maintain it yourself,
|// or delete it so it is regenerated as needed.""".stripMargin

def unmanagedComment(service: Either[ModelBuilder.Service, ModelBuilder.Entity]) = {
def unmanagedComment(service: Either[ModelBuilder.Service, ModelBuilder.StatefulComponent]) = {

val (kind, messageType) = service match {
case Left(serv: ModelBuilder.ActionService) => ("Action Service", serv.messageType)
Expand All @@ -56,6 +56,7 @@ object SourceGeneratorUtils {
case Right(ent: ModelBuilder.EventSourcedEntity) => ("Event Sourced Entity Service", ent.messageType)
case Right(ent: ModelBuilder.ValueEntity) => ("Value Entity Service", ent.messageType)
case Right(ent: ModelBuilder.ReplicatedEntity) => ("Replicated Entity Service", ent.messageType)
case Right(ent: ModelBuilder.WorkflowComponent) => ("Workflow Service", ent.messageType)
}
val fileName = messageType.parent.protoFileName
s"""// This class was initially generated based on the .proto definition by Kalix tooling.
Expand Down Expand Up @@ -150,11 +151,11 @@ object SourceGeneratorUtils {
/**
* Given a Service and an Entity, return all MessageType for all possible messages:
* - commands for all cases
* - state for Value Entities
* - state for Value Entities and Workflows
* - state and events for Event Sourced Entities
* - Value and eventually Key types for Replicated Entities (when applicable)
*/
def allRelevantMessageTypes(service: ModelBuilder.EntityService, entity: ModelBuilder.Entity) = {
def allRelevantMessageTypes(service: ModelBuilder.EntityService, entity: ModelBuilder.StatefulComponent) = {

val allCommands = service.commands.toSeq
.flatMap(command => Seq(command.inputType, command.outputType))
Expand All @@ -181,6 +182,8 @@ object SourceGeneratorUtils {
case ReplicatedMultiMap(_, MessageTypeArgument(valueFqn)) => Seq(valueFqn)
case _ => Seq.empty
}
case va: ModelBuilder.WorkflowComponent =>
Seq(va.state.messageType)
}
allCommands ++ entitySpecificMessages
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,24 +23,24 @@ import "google/api/annotations.proto";
option java_outer_classname = "ShoppingCartApi";

message AddLineItem {
string cart_id = 1 [(kalix.field).entity_key = true];
string cart_id = 1 [(kalix.field).id = true];
string product_id = 2;
string name = 3;
int32 quantity = 4;
}

message RemoveLineItem {
string cart_id = 1 [(kalix.field).entity_key = true];
string cart_id = 1 [(kalix.field).id = true];
string product_id = 2;
string name = 3;
}

message GetShoppingCart {
string cart_id = 1 [(kalix.field).entity_key = true];
string cart_id = 1 [(kalix.field).id = true];
}

message RemoveShoppingCart {
string cart_id = 1 [(kalix.field).entity_key = true];
string cart_id = 1 [(kalix.field).id = true];
}

message LineItem {
Expand All @@ -57,7 +57,7 @@ service ShoppingCartService {
option (kalix.codegen) = {
value_entity: {
name: ".domain.ShoppingCart"
entity_type: "shopping-cart"
type_id: "shopping-cart"
state: ".domain.Cart"
}
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ abstract class ExampleSuiteBase extends munit.FunSuite {

implicit val codegenLog = new kalix.codegen.Log {
override def debug(message: String): Unit = println(s"[DEBUG] $message")
override def warn(message: String): Unit = println(s"[WARNING] $message")
override def info(message: String): Unit = println(s"[INFO] $message")
}
val testsDir = BuildInfo.test_resourceDirectory / "tests"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,17 +17,12 @@
package kalix.codegen
package java

import _root_.java.nio.file.Files
import _root_.java.nio.file.Path

import com.google.common.base.Charsets

/**
* Responsible for generating Java source from an entity model
*/
object ActionServiceSourceGenerator {
import kalix.codegen.SourceGeneratorUtils._
import JavaGeneratorUtils._
import kalix.codegen.SourceGeneratorUtils._

/**
* Generate Java source from views where the target source and test source directories have no existing source.
Expand Down Expand Up @@ -304,14 +299,7 @@ object ActionServiceSourceGenerator {
val classNameAction = service.className
val protoName = service.messageType.protoName

val relevantDescriptors =
collectRelevantTypes(service.commandTypes, service.messageType)
.collect { case pmt: ProtoMessageType =>
s"${pmt.parent.javaOuterClassname}.getDescriptor()"
}

val descriptors =
(relevantDescriptors :+ s"${service.messageType.parent.javaOuterClassname}.getDescriptor()").distinct.sorted
val descriptors = AdditionalDescriptors.collectServiceDescriptors(service)

implicit val imports: Imports = generateImports(
service.commandTypes ++ service.commandTypes.map(_.descriptorImport),
Expand Down
Loading

0 comments on commit 8cbf249

Please sign in to comment.