Skip to content

Commit

Permalink
feat: generic components, handlers, and contexts (#333)
Browse files Browse the repository at this point in the history
* feat: generic components, handlers, and contexts
* update TCK with typed state
* update codegen to use typed components
* update framework version to 1.0.6
* update javascript samples
* update typescript samples
  • Loading branch information
pvlugter authored May 19, 2022
1 parent 50563b8 commit bb0e289
Show file tree
Hide file tree
Showing 86 changed files with 2,030 additions and 2,111 deletions.
21 changes: 10 additions & 11 deletions codegen/core/src/main/scala/io/kalix/codegen/ModelBuilder.scala
Original file line number Diff line number Diff line change
Expand Up @@ -47,23 +47,26 @@ object ModelBuilder {
/**
* An entity represents the primary model object and is conceptually equivalent to a class, or a type of state.
*/
sealed abstract class Entity(val fqn: FullyQualifiedName, val entityType: String)
sealed abstract class Entity(val fqn: FullyQualifiedName, val entityType: String, val state: State)

/**
* A type of Entity that stores its state using a journal of events, and restores its state by replaying that journal.
*/
case class EventSourcedEntity(
override val fqn: FullyQualifiedName,
override val entityType: String,
state: Option[State],
override val state: State,
events: Iterable[Event])
extends Entity(fqn, entityType)
extends Entity(fqn, entityType, state)

/**
* A type of Entity that stores its state using a journal of events, and restores its state by replaying that journal.
*/
case class ValueEntity(override val fqn: FullyQualifiedName, override val entityType: String, state: State)
extends Entity(fqn, entityType)
case class ValueEntity(
override val fqn: FullyQualifiedName,
override val entityType: String,
override val state: State)
extends Entity(fqn, entityType, state)

/**
* A Service backed by Kalix; either an Action, View or Entity
Expand Down Expand Up @@ -283,9 +286,7 @@ object ModelBuilder {
EventSourcedEntity(
defineEntityFullyQualifiedName(entityDef.getName, serviceName),
entityDef.getEntityType,
Option(entityDef.getState)
.filter(_.nonEmpty)
.map(name => State(resolveFullyQualifiedMessageType(name, pkg, additionalDescriptors))),
State(resolveFullyQualifiedMessageType(entityDef.getState, pkg, additionalDescriptors)),
entityDef.getEventsList.asScala
.map(event => Event(resolveFullyQualifiedMessageType(event, pkg, additionalDescriptors))))
}
Expand Down Expand Up @@ -436,9 +437,7 @@ object ModelBuilder {
EventSourcedEntity(
FullyQualifiedName(name, protoReference),
entityDef.getEntityType,
Option(entityDef.getState)
.filter(_.nonEmpty)
.map(name => State(FullyQualifiedName(name, protoReference))),
State(FullyQualifiedName(entityDef.getState, protoReference)),
entityDef.getEventsList.asScala
.map(event => Event(FullyQualifiedName(event, protoReference)))))
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ abstract class ModelBuilderSuite(val config: ModelBuilderSuite.Config) extends m
ModelBuilder.EventSourcedEntity(
FullyQualifiedName("ShoppingCart", derivedEntityPackage),
"shopping-cart",
Some(ModelBuilder.State(FullyQualifiedName("Cart", domainPackage))),
ModelBuilder.State(FullyQualifiedName("Cart", domainPackage)),
List(
ModelBuilder.Event(FullyQualifiedName("ItemAdded", domainPackage)),
ModelBuilder.Event(FullyQualifiedName("ItemRemoved", domainPackage))))
Expand Down Expand Up @@ -398,7 +398,7 @@ class ModelBuilderWithCodegenAnnotationSuite extends ModelBuilderSuite(ModelBuil
// this is the name as defined in the proto file
FullyQualifiedName("ShoppingCartServiceEntity", shoppingCartPackage),
"shopping-cart",
Some(ModelBuilder.State(FullyQualifiedName("Cart", domainPackage))),
ModelBuilder.State(FullyQualifiedName("Cart", domainPackage)),
List(
ModelBuilder.Event(FullyQualifiedName("ItemAdded", domainPackage)),
ModelBuilder.Event(FullyQualifiedName("ItemRemoved", domainPackage))))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,9 +86,9 @@ object ActionServiceSourceGenerator {
dquotes(service.fqn.fullName) <> comma <> line <>
braces(nest(line <>
ssep(
(if (sourceDirectory != protobufSourceDirectory)
List("includeDirs" <> colon <+> brackets(dquotes(protobufSourceDirectory.toString)))
else List.empty) ++ List("serializeFallbackToJson" <> colon <+> "true"),
if (sourceDirectory != protobufSourceDirectory)
List("includeDirs" <> colon <+> brackets(dquotes(protobufSourceDirectory.toString)))
else List.empty,
comma <> line)) <> line)) <> line) <> semi <> line <>
line <>
"action.commandHandlers" <+> equal <+> braces(
Expand All @@ -101,43 +101,49 @@ object ActionServiceSourceGenerator {
},
comma <> line)) <> line) <> semi <> line <>
line <>
"export default action;")
"export default action;" <> line)
}

private[codegen] def typedefSource(service: ModelBuilder.ActionService): Document =
pretty(
managedCodeComment <> line <> line <>
"import" <+> braces(nest(line <>
"TypedAction" <> comma <> line <>
"ActionCommandContext" <> comma <> line <>
"StreamedInCommandContext" <> comma <> line <>
"StreamedOutCommandContext") <> line) <+> "from" <+> dquotes("../kalix") <> semi <> line <>
"import" <+> ProtoNs <+> "from" <+> dquotes("./proto") <> semi <> line <>
"import" <+> braces(space <> "Action, CommandReply" <> space) <+> "from" <+> dquotes(
"@kalix-io/kalix-javascript-sdk") <> semi <> line <>
"import * as" <+> ProtoNs <+> "from" <+> dquotes("./proto") <> semi <> line <>
line <>
"export type CommandHandlers" <+> equal <+> braces(nest(line <>
ssep(
service.commands.toSeq.map { command =>
command.fqn.name <> colon <+> parens(nest(line <>
(if (command.streamedInput) emptyDoc
else {
"request" <> colon <+> typeReference(command.inputType) <> comma <> line
}) <>
"ctx" <> colon <+> ssep(
Seq(text("ActionCommandContext")) ++
Seq("StreamedInCommandContext" <> angles(typeReference(command.inputType)))
.filter(_ => command.streamedInput)
++
Seq("StreamedOutCommandContext" <> angles(typeReference(command.outputType)))
.filter(_ => command.streamedOutput),
" & ")) <> line) <+> "=>" <+>
ssep(
(if (command.streamedOutput) Seq(text("void"))
else Seq(typeReference(command.outputType), text("void"))).flatMap(returnType =>
Seq(returnType, "Promise" <> angles(returnType))),
" | ") <> semi
},
line)) <> line) <> semi <> line <>
apiTypes(service) <>
line <>
"export declare namespace" <+> service.fqn.name <+> braces(
nest(line <>
"type CommandHandlers" <+> equal <+> braces(nest(line <>
ssep(
service.commands.toSeq.map { command =>
command.fqn.name <> colon <+> parens(nest(line <>
(if (command.streamedInput) emptyDoc
else {
"command" <> colon <+> apiClass(command.inputType) <> comma <> line
}) <>
"ctx" <> colon <+> (
if (command.streamedInput && command.streamedOutput)
"Action.StreamedCommandContext" <> angles(
apiClass(command.inputType) <> comma <+> apiInterface(command.outputType))
else if (command.streamedInput)
"Action.StreamedInCommandContext" <> angles(
apiClass(command.inputType) <> comma <+> apiInterface(command.outputType))
else if (command.streamedOutput)
"Action.StreamedOutCommandContext" <> angles(apiInterface(command.outputType))
else "Action.UnaryCommandContext" <> angles(apiInterface(command.outputType))
) <> line)) <+> "=>" <+>
ssep(
if (command.streamedOutput) Seq(text("void"))
else
Seq(
"CommandReply" <> angles(apiInterface(command.outputType)),
"Promise" <> angles("CommandReply" <> angles(apiInterface(command.outputType)))),
" | ") <> semi
},
line)) <> line) <> semi) <> line) <> line <>
line <>
"export type" <+> service.fqn.name <+> equal <+>
"TypedAction" <> angles("CommandHandlers") <> semi <> line)
"export declare type" <+> service.fqn.name <+> equal <+>
"Action" <> angles(s"${service.fqn.name}.CommandHandlers") <> semi <> line)
}
Original file line number Diff line number Diff line change
Expand Up @@ -108,25 +108,14 @@ object EntityServiceSourceGenerator {
}
pretty(
initialisedCodeComment <> line <> line <>
"import" <+> "kalix" <+> "from" <+> dquotes("@kalix-io/kalix-javascript-sdk") <> semi <> line <>
"const" <+> entityType <+> equal <+> "kalix." <> entityType
<> semi <> line <>
"import" <+> braces(space <> entityType <> comma <+> "Reply" <> space) <+> "from" <+> dquotes(
"@kalix-io/kalix-javascript-sdk") <> semi <> line <>
line <>
blockComment(Seq[Doc](
"Type definitions.",
"These types have been generated based on your proto source.",
"A TypeScript aware editor such as VS Code will be able to leverage them to provide hinting and validation.",
emptyDoc,
"State; the serialisable and persistable state of the entity",
typedef("import" <> parens(dquotes(typedefPath)) <> dot <> "State", "State"),
emptyDoc) ++ (entity match {
case _: ModelBuilder.EventSourcedEntity =>
Seq[Doc](
"Event; the union of all possible event types",
typedef("import" <> parens(dquotes(typedefPath)) <> dot <> "Event", "Event"),
emptyDoc)
case _ => Seq.empty
}) ++ Seq[Doc](
emptyDoc) ++ Seq[Doc](
service.fqn.name <> semi <+> "a strongly typed extension of" <+> entityType <+> "derived from your proto source",
typedef("import" <> parens(dquotes(typedefPath)) <> dot <> service.fqn.name, service.fqn.name)): _*) <> line <>
line <>
Expand All @@ -140,12 +129,16 @@ object EntityServiceSourceGenerator {
dquotes(entity.entityType) <> comma <> line <>
braces(nest(line <>
ssep(
(if (sourceDirectory != protobufSourceDirectory)
List("includeDirs" <> colon <+> brackets(dquotes(protobufSourceDirectory.toString)))
else List.empty) ++ List("serializeFallbackToJson" <> colon <+> "true"),
if (sourceDirectory != protobufSourceDirectory)
List("includeDirs" <> colon <+> brackets(dquotes(protobufSourceDirectory.toString)))
else List.empty,
comma <> line)) <> line)) <> line) <> semi <> line <>
line <>
"entity.setInitial" <> parens("entityId => " <> parens("{}")) <> semi <> line <>
"const" <+> entity.state.fqn.name <+> equal <+> "entity.lookupType" <> parens(
dquotes(entity.state.fqn.fullName)) <> semi <> line <>
line <>
"entity.setInitial" <> parens(
"entityId => " <> entity.state.fqn.name <> ".create" <> parens("{}")) <> semi <> line <>
line <>
(entity match {
case ModelBuilder.EventSourcedEntity(_, _, _, events) =>
Expand All @@ -156,7 +149,7 @@ object EntityServiceSourceGenerator {
ssep(
service.commands.toSeq.map { command =>
command.fqn.name <> parens("command, state, ctx") <+> braces(nest(line <>
"return ctx.fail(\"The command handler for `" <> command.fqn.name <> "` is not implemented, yet\")" <> semi) <> line)
"return Reply.failure(\"The command handler for `" <> command.fqn.name <> "` is not implemented, yet\")" <> semi) <> line)
},
comma <> line)) <> line) <> comma <>
line <>
Expand All @@ -174,73 +167,93 @@ object EntityServiceSourceGenerator {
ssep(
service.commands.toSeq.map { command =>
command.fqn.name <> parens("command, state, ctx") <+> braces(nest(line <>
"return ctx.fail(\"The command handler for `" <> command.fqn.name <> "` is not implemented, yet\")" <> semi) <> line)
"return Reply.failure(\"The command handler for `" <> command.fqn.name <> "` is not implemented, yet\")" <> semi) <> line)
},
comma <> line)) <> line)) <> semi
}) <> line <>
line <>
"export default entity;")
"export default entity;" <> line)
}

private[codegen] def typedefSource(service: ModelBuilder.Service, entity: ModelBuilder.Entity): Document =
pretty(
managedCodeComment <> line <> line <>
"import" <+> braces(nest(line <> (entity match {
case _: ModelBuilder.EventSourcedEntity =>
"TypedEventSourcedEntity" <> comma <> line <>
"EventSourcedCommandContext"
case _: ModelBuilder.ValueEntity =>
"TypedValueEntity" <> comma <> line <>
"ValueEntityCommandContext"
})) <> line) <+> "from" <+> dquotes("../kalix") <> semi <> line <>
"import" <+> ProtoNs <+> "from" <+> dquotes("./proto") <> semi <> line <>
"import" <+> braces(space <> (entity match {
case _: ModelBuilder.EventSourcedEntity => "EventSourcedEntity"
case _: ModelBuilder.ValueEntity => "ValueEntity"
}) <+> comma <+> "CommandReply" <> space) <+> "from" <+> dquotes(
"@kalix-io/kalix-javascript-sdk") <> semi <> line <>
"import * as" <+> ProtoNs <+> "from" <+> dquotes("./proto") <> semi <> line <>
line <>
(entity match {
case ModelBuilder.EventSourcedEntity(_, _, state, events) =>
"export type State" <+> equal <+> state
.map(state => typeReference(state.fqn))
.getOrElse(text("unknown")) <> semi <> line <>
"export type Event" <+> equal <> typeUnion(events.toSeq.map(_.fqn)) <> semi
case ModelBuilder.ValueEntity(_, _, state) =>
"export type State" <+> equal <+> typeReference(state.fqn) <> semi
}) <> line <>
"export type Command" <+> equal <> typeUnion(service.commands.toSeq.map(_.inputType)) <> semi <> line <>
apiTypes(service) <>
line <>
(entity match {
case ModelBuilder.EventSourcedEntity(_, _, _, events) =>
"export type EventHandlers" <+> equal <+> braces(nest(line <>
domainTypes(entity) <>
line <>
"export declare namespace" <+> service.fqn.name <+> braces(
nest(
line <>
"type" <+> "State" <+> equal <+> domainType(entity.state.fqn) <> semi <> line <>
line <>
(entity match {
case ModelBuilder.EventSourcedEntity(_, _, _, events) =>
"type Events" <+> equal <> typeUnion(events.toSeq.map(event => domainType(event.fqn))) <> semi <> line <>
line <>
"type EventHandlers" <+> equal <+> braces(nest(line <>
ssep(
events.toSeq.map { event =>
event.fqn.name <> colon <+> parens(nest(line <>
"event" <> colon <+> domainType(event.fqn) <> comma <> line <>
"state" <> colon <+> "State") <> line) <+> "=>" <+> "State" <> semi
},
line)) <> line) <> semi <> line <> line
case _: ModelBuilder.ValueEntity => emptyDoc
}) <>
"type CommandContext" <+> equal <+> (entity match {
case _: ModelBuilder.EventSourcedEntity => "EventSourcedEntity.CommandContext<Events>"
case _: ModelBuilder.ValueEntity => "ValueEntity.CommandContext<State>"
}) <> semi <> line <>
line <>
"type CommandHandlers" <+> equal <+> braces(nest(line <>
ssep(
events.toSeq.map { event =>
event.fqn.name <> colon <+> parens(nest(line <>
"event" <> colon <+> typeReference(event.fqn) <> comma <> line <>
"state" <> colon <+> "State") <> line) <+> "=>" <+> "State" <> semi
service.commands.toSeq.map { command =>
command.fqn.name <> colon <+> parens(nest(line <>
"command" <> colon <+> apiClass(command.inputType) <> comma <> line <>
"state" <> colon <+> "State" <> comma <> line <>
"ctx" <> colon <+> "CommandContext") <> line) <+> "=>" <+> "CommandReply" <> angles(
apiInterface(command.outputType)) <> semi
},
line)) <> line) <> semi <> line <>
line
case _: ModelBuilder.ValueEntity => emptyDoc
}) <>
"export type CommandHandlers" <+> equal <+> braces(nest(line <>
ssep(
service.commands.toSeq.map { command =>
command.fqn.name <> colon <+> parens(nest(line <>
"command" <> colon <+> typeReference(command.inputType) <> comma <> line <>
"state" <> colon <+> "State" <> comma <> line <>
"ctx" <> colon <+> (entity match {
case _: ModelBuilder.EventSourcedEntity => "EventSourcedCommandContext<Event>"
case _: ModelBuilder.ValueEntity => "ValueEntityCommandContext<State>"
})) <> line) <+> "=>" <+> typeReference(command.outputType) <> semi
},
line)) <> line) <> semi <> line <>
line)) <> line) <> semi) <> line) <> line <>
line <>
"export type" <+> service.fqn.name <+> equal <+> (entity match {
"export declare type" <+> service.fqn.name <+> equal <+> (entity match {
case _: ModelBuilder.EventSourcedEntity =>
"TypedEventSourcedEntity" <> angles(nest(line <>
ssep(Seq("State", "EventHandlers", "CommandHandlers"), comma <> line)) <> line)
"EventSourcedEntity" <> angles(
nest(line <>
ssep(
Seq(
s"${service.fqn.name}.State",
s"${service.fqn.name}.Events",
s"${service.fqn.name}.CommandHandlers",
s"${service.fqn.name}.EventHandlers"),
comma <> line)) <> line)
case _: ModelBuilder.ValueEntity =>
"TypedValueEntity" <> angles(nest(line <>
ssep(Seq("State", "CommandHandlers"), comma <> line)) <> line)
"ValueEntity" <> angles(nest(line <>
ssep(Seq(s"${service.fqn.name}.State", s"${service.fqn.name}.CommandHandlers"), comma <> line)) <> line)
}) <> semi <> line)

private[codegen] def domainTypes(entity: ModelBuilder.Entity): Doc = {
"export declare namespace domain" <+> braces(
nest(line <>
"type" <+> entity.state.fqn.name <+> equal <+> messageType(entity.state.fqn) <> semi <>
(entity match {
case ModelBuilder.EventSourcedEntity(_, _, _, events) =>
line <>
ssep(
events.toSeq.map(event => line <> "type" <+> event.fqn.name <+> equal <+> messageType(event.fqn) <> semi),
line)
case _: ModelBuilder.ValueEntity => emptyDoc
})) <> line) <> line
}

private[codegen] def testSource(
service: ModelBuilder.Service,
entity: ModelBuilder.Entity,
Expand Down
Loading

0 comments on commit bb0e289

Please sign in to comment.