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

feat: generic components, handlers, and contexts #333

Merged
merged 6 commits into from
May 19, 2022
Merged
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
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