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(codegen-java): 'external' value entity state types #620

Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -314,66 +314,108 @@ object ModelBuilder {
*/
def introspectProtobufClasses(descriptors: Iterable[Descriptors.FileDescriptor])(implicit
log: Log,
fqnExtractor: FullyQualifiedNameExtractor): Model =
descriptors.foldLeft(Model(Map.empty, Map.empty)) { case (Model(existingServices, existingEntities), descriptor) =>
log.debug("Looking at descriptor " + descriptor.getName)
val services = for {
serviceDescriptor <- descriptor.getServices.asScala
options = serviceDescriptor
.getOptions()
.getExtension(com.akkaserverless.Annotations.service)
serviceType <- Option(options.getType())
serviceName = fqnExtractor(serviceDescriptor)

methods = serviceDescriptor.getMethods.asScala
commands = methods.map(Command.from)

service <- serviceType match {
case ServiceType.SERVICE_TYPE_ENTITY =>
Option(options.getComponent())
.filter(_.nonEmpty)
.map[Service] { componentName =>
val componentFullName =
resolveFullName(componentName, serviceDescriptor.getFile.getPackage)

EntityService(serviceName, commands, componentFullName)
fqnExtractor: FullyQualifiedNameExtractor): Model = {
val descriptorSeq = descriptors.toSeq
descriptorSeq.foldLeft(Model(Map.empty, Map.empty)) {
case (Model(existingServices, existingEntities), descriptor) =>
log.debug("Looking at descriptor " + descriptor.getName)
val services = for {
serviceDescriptor <- descriptor.getServices.asScala
options = serviceDescriptor
.getOptions()
.getExtension(com.akkaserverless.Annotations.service)
serviceType <- Option(options.getType())
serviceName = fqnExtractor(serviceDescriptor)

methods = serviceDescriptor.getMethods.asScala
commands = methods.map(Command.from)

service <- serviceType match {
case ServiceType.SERVICE_TYPE_ENTITY =>
Option(options.getComponent())
.filter(_.nonEmpty)
.map[Service] { componentName =>
val componentFullName =
resolveFullName(componentName, serviceDescriptor.getFile.getPackage)

EntityService(serviceName, commands, componentFullName)
}
case ServiceType.SERVICE_TYPE_ACTION =>
Some(ActionService(serviceName, commands))
case ServiceType.SERVICE_TYPE_VIEW =>
val methodDetails = methods.flatMap { method =>
Option(method.getOptions().getExtension(com.akkaserverless.Annotations.method).getView()).map(
viewOptions => (method, viewOptions))
}
case ServiceType.SERVICE_TYPE_ACTION =>
Some(ActionService(serviceName, commands))
case ServiceType.SERVICE_TYPE_VIEW =>
val methodDetails = methods.flatMap { method =>
Option(method.getOptions().getExtension(com.akkaserverless.Annotations.method).getView()).map(
viewOptions => (method, viewOptions))
}
val updates = methodDetails.collect {
case (method, viewOptions) if viewOptions.hasUpdate =>
Command.from(method)
}
Some(
ViewService(
serviceName,
commands,
viewId = serviceDescriptor.getName(),
updates = updates,
transformedUpdates = methodDetails
.collect {
case (method, viewOptions)
if viewOptions.hasUpdate && viewOptions
.getUpdate()
.getTransformUpdates() =>
Command.from(method)
}))
case _ => None
}
} yield serviceName.fullQualifiedName -> service

Model(
existingServices ++ services,
existingEntities ++
extractEventSourcedEntityDefinition(descriptor).map(entity => entity.fqn.fullQualifiedName -> entity) ++
extractValueEntityDefinition(descriptor).map(entity => entity.componentFullName -> entity) ++
extractReplicatedEntityDefinition(descriptor).map(entity => entity.fqn.fullQualifiedName -> entity))
val updates = methodDetails.collect {
case (method, viewOptions) if viewOptions.hasUpdate =>
Command.from(method)
}
Some(
ViewService(
serviceName,
commands,
viewId = serviceDescriptor.getName(),
updates = updates,
transformedUpdates = methodDetails
.collect {
case (method, viewOptions)
if viewOptions.hasUpdate && viewOptions
.getUpdate()
.getTransformUpdates() =>
Command.from(method)
}))
case _ => None
}
} yield serviceName.fullQualifiedName -> service

Model(
existingServices ++ services,
existingEntities ++
extractEventSourcedEntityDefinition(descriptor, descriptorSeq).map(entity =>
entity.fqn.fullQualifiedName -> entity) ++
extractValueEntityDefinition(descriptor, descriptorSeq).map(entity => entity.componentFullName -> entity) ++
extractReplicatedEntityDefinition(descriptor).map(entity => entity.fqn.fullQualifiedName -> entity))
}
}

/**
* @return
* the FQN for a proto 'message' (which are used not just for "messages", but also for state types etc)
*/
private def resolveFullyQualifiedMessageType(
name: String,
descriptor: Descriptors.FileDescriptor,
descriptors: Seq[Descriptors.FileDescriptor])(implicit
log: Log,
fqnExtractor: FullyQualifiedNameExtractor): FullyQualifiedName = {
// TODO this is used in the java tck as ValueEntity state type - I'm not sure we want to
// support this? In that case we should probably support all primitives?
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AnySupport does support primitives, wrapping them in an protobuf Any I think, but I also doubt we should support that. Another of those flexibilities just making what you actually can/should do hard to understand.

if (name == "String")
FullyQualifiedName.noDescriptor("String", PackageNaming("", "", "", javaMultipleFiles = true))
else {
val fullName = resolveFullName(name, descriptor.getPackage)
val protoPackage = fullName.split("\\.").init.mkString(".")
val protoName = fullName.split("\\.").last
// TODO we could also look at the imports in the proto file to support
// importing names from outside this file without using their fully qualified name.
descriptors
.filter(_.getPackage == protoPackage)
.flatMap(_.getMessageTypes.asScala)
.filter(_.getName == protoName) match {
case Nil =>
throw new IllegalStateException(
s"No descriptor found for [$fullName] (searched: [${descriptors.map(_.getFile.getName).mkString(", ")}])")
case Seq(descriptor) =>
fqnExtractor.apply(descriptor)
case matchingDescriptors =>
throw new IllegalStateException(
s"Multiple matching descriptors found for [$fullName] (searched: [${descriptors
.map(_.getFile.getName)
.mkString(", ")}], found in: ${matchingDescriptors.map(_.getFile.getName).mkString(", ")})")
}
}
}

/**
* Resolves the provided name relative to the provided package
Expand Down Expand Up @@ -402,7 +444,9 @@ object ModelBuilder {
* @return
* the event sourced entity
*/
private def extractEventSourcedEntityDefinition(descriptor: Descriptors.FileDescriptor)(implicit
private def extractEventSourcedEntityDefinition(
descriptor: Descriptors.FileDescriptor,
additionalDescriptors: Seq[Descriptors.FileDescriptor])(implicit
log: Log,
fqnExtractor: FullyQualifiedNameExtractor): Option[EventSourcedEntity] = {
val rawEntity =
Expand All @@ -417,10 +461,10 @@ object ModelBuilder {
EventSourcedEntity(
FullyQualifiedName(name, name, protoReference, fullQualifiedDescriptor),
rawEntity.getEntityType,
// FIXME this assumes the state is defined in the same proto file as the
// entity, which I don't think is necessarily true.
State(FullyQualifiedName(rawEntity.getState, rawEntity.getState, protoReference, fullQualifiedDescriptor)),
State(resolveFullyQualifiedMessageType(rawEntity.getState, descriptor, additionalDescriptors)),
rawEntity.getEventsList.asScala
// TODO this assumes events are defined in the same proto as the entity. To lift this restriction,
// use something like resolveFullyQualifiedMessageType above
.map(event => Event(FullyQualifiedName(event, event, protoReference, fullQualifiedDescriptor))))
}
}
Expand All @@ -431,29 +475,26 @@ object ModelBuilder {
* @param descriptor
* the file descriptor to extract from
*/
private def extractValueEntityDefinition(descriptor: Descriptors.FileDescriptor)(implicit
private def extractValueEntityDefinition(
descriptor: Descriptors.FileDescriptor,
descriptors: Seq[Descriptors.FileDescriptor])(implicit
log: Log,
fqnExtractor: FullyQualifiedNameExtractor): Option[ValueEntity] = {
val rawEntity =
descriptor.getOptions
.getExtension(com.akkaserverless.Annotations.file)
.getValueEntity

val protoReference = fqnExtractor.packageName(descriptor)

Option(rawEntity.getName).filter(_.nonEmpty).map { name =>
ValueEntity(
descriptor.getFile.getPackage + "." + name,
FullyQualifiedName(name, name, protoReference, Some(fqnExtractor.fileDescriptorObject(descriptor.getFile))),
FullyQualifiedName(
name,
name,
fqnExtractor.packageName(descriptor),
Some(fqnExtractor.fileDescriptorObject(descriptor.getFile))),
rawEntity.getEntityType,
// FIXME this assumes the state is defined in the same proto file as the
// entity, which I don't think is necessarily true.
State(
FullyQualifiedName(
rawEntity.getState,
rawEntity.getState,
protoReference,
Some(fqnExtractor.fileDescriptorObject(descriptor.getFile)))))
State(resolveFullyQualifiedMessageType(rawEntity.getState, descriptor, descriptors)))
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,9 @@ object SourceGeneratorUtils {
.filterNot { typ =>
typ.parent.javaPackage == packageName
}
.filterNot { typ =>
typ.parent.javaPackage.isEmpty
}
.filterNot { typ =>
packageImports.contains(typ.parent.javaPackage)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ object JavaGeneratorUtils {
else
s"${fqn.parent.javaPackage}.${fqn.parent.javaOuterClassname}"

if (imports.contains(fqn.fullQualifiedName)) fqn.name
if (fqn.parent.javaPackage.isEmpty) fqn.name
else if (imports.contains(fqn.fullQualifiedName)) fqn.name
else if (imports.currentPackage == directParent) fqn.name
else if (imports.contains(directParent) || imports.currentPackage == fqn.parent.javaPackage)
directParent.split("\\.").last + "." + fqn.name
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ object ValueEntitySourceGenerator {
}

implicit val imports = generateImports(
relevantTypes ++ relevantTypes.map(_.descriptorImport),
relevantTypes ++ relevantTypes.flatMap(_.descriptorObject),
packageName,
otherImports = Seq(
"com.akkaserverless.javasdk.valueentity.ValueEntityContext",
Expand All @@ -157,8 +157,9 @@ object ValueEntitySourceGenerator {

val descriptors =
(collectRelevantTypes(relevantTypes, service.fqn)
.flatMap(_.descriptorObject)
.map(d =>
s"${d.parent.javaOuterClassname}.getDescriptor()") :+ s"${service.fqn.parent.javaOuterClassname}.getDescriptor()").distinct.sorted
s"${d.name}.getDescriptor()") :+ s"${service.fqn.parent.javaOuterClassname}.getDescriptor()").distinct.sorted

s"""package $packageName;
|
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// Copyright 2021 Lightbend Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

syntax = "proto3";

package com.example.state_in_different_proto.domain;

import "akkaserverless/annotations.proto";

option java_outer_classname = "UserrDomain";

option (akkaserverless.file).value_entity = {
name: "Userr"
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I plan to use this as a testcase for the fix for #437, changing it back to User then ;)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yarr

entity_type: "user"
state: "com.example.state_in_different_proto.state.UserState"
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// Copyright 2021 Lightbend Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

syntax = "proto3";

package com.example.state_in_different_proto.state;

message UserState {
string name = 1;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
// Copyright 2021 Lightbend Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

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

import "google/protobuf/empty.proto";
import "akkaserverless/annotations.proto";

package com.example.state_in_different_proto;

option java_outer_classname = "UserrApi";

message CreateUser {
string user_id = 1 [(akkaserverless.field).entity_key = true];
string name = 2;
}


message GetUser {
string user_id = 1 [(akkaserverless.field).entity_key = true];
}

message CurrentUser {
int32 value = 1;
}

service UserService {
option (akkaserverless.service) = {
type : SERVICE_TYPE_ENTITY
component : "com.example.state_in_different_proto.domain.Userr"
};

rpc Create (CreateUser) returns (google.protobuf.Empty);
rpc GetCurrentUser (GetUser) returns (CurrentUser);
}