Skip to content
This repository has been archived by the owner on Mar 16, 2022. It is now read-only.

scala support #116

Merged
merged 10 commits into from
Oct 9, 2019
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
92 changes: 91 additions & 1 deletion build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -86,10 +86,13 @@ headerSources in Compile ++= {
}

lazy val root = (project in file("."))
// Don't forget to add your sbt module here!
// A missing module here can lead to failing Travis test results
.aggregate(`proxy-core`,
`proxy-cassandra`,
`proxy-postgres`,
`java-support`,
`scala-support`,
`java-shopping-cart`,
`akka-client`,
operator,
Expand Down Expand Up @@ -630,6 +633,70 @@ lazy val `java-support` = (project in file("java-support"))
)
)

lazy val `scala-support` = (project in file("scala-support"))
Copy link
Contributor

Choose a reason for hiding this comment

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

Try addding this project to aggregate:

.aggregate(`proxy-core`,
             `proxy-cassandra`,
             `proxy-postgres`,
             `java-support`,
             `scala-support`, <--- HERE
             `java-shopping-cart`,
             `akka-client`,
             operator,
             `tck`,
             docs)
  .settings(common)

.enablePlugins(AkkaGrpcPlugin, BuildInfoPlugin)
.settings(
name := "cloudstate-scala-support",
common,
crossPaths := false,
publishMavenStyle := true,
publishTo := sonatypePublishTo.value,
buildInfoKeys := Seq[BuildInfoKey](name, version),
buildInfoPackage := "io.cloudstate.scalasupport",
// Generate javadocs by just including non generated Java sources
sourceDirectories in (Compile, doc) := Seq((javaSource in Compile).value),
sources in (Compile, doc) := {
val javaSourceDir = (javaSource in Compile).value.getAbsolutePath
(sources in (Compile, doc)).value.filter(_.getAbsolutePath.startsWith(javaSourceDir))
},
// javadoc (I think java 9 onwards) refuses to compile javadocs if it can't compile the entire source path.
// but since we have java files depending on Scala files, we need to include ourselves on the classpath.
dependencyClasspath in (Compile, doc) := (fullClasspath in Compile).value,
javacOptions in (Compile, doc) ++= Seq(
"-overview",
((javaSource in Compile).value / "overview.html").getAbsolutePath,
"-notimestamp",
"-doctitle",
"CloudState Scala Support"
),
libraryDependencies ++= Seq(
// Remove these explicit gRPC/netty dependencies once akka-grpc 0.7.1 is released and we've upgraded to using that
"io.grpc" % "grpc-core" % GrpcJavaVersion,
"io.grpc" % "grpc-netty-shaded" % GrpcJavaVersion,
"com.typesafe.akka" %% "akka-stream" % AkkaVersion,
"com.typesafe.akka" %% "akka-slf4j" % AkkaVersion,
"com.typesafe.akka" %% "akka-discovery" % AkkaVersion,
"com.typesafe.akka" %% "akka-http" % AkkaHttpVersion,
"com.typesafe.akka" %% "akka-http-spray-json" % AkkaHttpVersion,
"com.typesafe.akka" %% "akka-http-core" % AkkaHttpVersion,
"com.typesafe.akka" %% "akka-http2-support" % AkkaHttpVersion,
"com.google.protobuf" % "protobuf-java" % ProtobufVersion % "protobuf",
"com.google.protobuf" % "protobuf-java-util" % ProtobufVersion,
"org.scalatest" %% "scalatest" % ScalaTestVersion % Test,
"com.typesafe.akka" %% "akka-testkit" % AkkaVersion % Test,
"com.typesafe.akka" %% "akka-stream-testkit" % AkkaVersion % Test,
"com.typesafe.akka" %% "akka-http-testkit" % AkkaHttpVersion % Test,
"com.thesamet.scalapb" %% "scalapb-runtime" % scalapb.compiler.Version.scalapbVersion % "protobuf",
"org.slf4j" % "slf4j-simple" % "1.7.26",
"com.fasterxml.jackson.core" % "jackson-databind" % "2.9.9.3"
),
javacOptions in Compile ++= Seq("-encoding", "UTF-8"),
Copy link
Contributor

Choose a reason for hiding this comment

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

Move this to the other javacOptiuons above?

akkaGrpcGeneratedSources in Compile := Seq(AkkaGrpc.Server),
akkaGrpcGeneratedLanguages in Compile := Seq(AkkaGrpc.Scala), // FIXME should be Java, but here be dragons

// Work around for https://github.com/akka/akka-grpc/pull/673
(PB.targets in Compile) := {
val old = (PB.targets in Compile).value
val ct = crossTarget.value

old.map(_.copy(outputPath = ct / "akka-grpc" / "main"))
},
PB.protoSources in Compile ++= {
val baseDir = (baseDirectory in ThisBuild).value / "protocols"
Seq(baseDir / "protocol", baseDir / "frontend")
}
)

lazy val `java-shopping-cart` = (project in file("samples/java-shopping-cart"))
.dependsOn(`java-support`)
.enablePlugins(AkkaGrpcPlugin, AssemblyPlugin)
Expand Down Expand Up @@ -659,6 +726,29 @@ lazy val `java-shopping-cart` = (project in file("samples/java-shopping-cart"))
}
)

lazy val `scala-shopping-cart` = (project in file("samples/scala-shopping-cart"))
.dependsOn(`scala-support`)
.enablePlugins(AkkaGrpcPlugin)
.settings(
name := "scala-shopping-cart",
PB.generate in Compile := (PB.generate in Compile).dependsOn(PB.generate in (`scala-support`, Compile)).value,
PB.protoSources in Compile ++= {
val baseDir = (baseDirectory in ThisBuild).value / "protocols"
Seq(baseDir / "frontend", baseDir / "example")
Copy link
Contributor

Choose a reason for hiding this comment

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

@janory Have you tried

Suggested change
Seq(baseDir / "frontend", baseDir / "example")
Seq(baseDir / "example")

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@viktorklang
I've checked out the branch & run the TCK on a different computer and it worked well, so I will cleanup the project and try it again. It is definitely a local issue.

The TCK is almost complete, the only issue is this:

[info] - must verify that the HTTP API of ShoppingCart protocol works *** FAILED ***
"...ems":[{"productId":"[B14623482","name":"Basic","quantity":1},{"productId":"A14362347","name":"Deluxe","quantity":7]}]}" did not equal "...ems":[{"productId":"[A14362347","name":"Deluxe","quantity":7},{"productId":"B14623482","name":"Basic","quantity":1]}]}" 

It is the same content, but the order is different.
I will check the implementation and fix the order, although maybe we can relax the TCK's check to ignore the order and focus only the content, because my guess is, that the order here is not important.

Copy link
Contributor

@marcellanz marcellanz Oct 2, 2019

Choose a reason for hiding this comment

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

@janory I had the excactly same "issue" in the Go Support first :-), had the items in a list that is not ordered.

Copy link
Contributor Author

@janory janory Oct 2, 2019

Choose a reason for hiding this comment

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

@marcellanz :)
It's a one liner fix, but I am just not sure, if we really need to enforce the order here.

Copy link
Contributor

@marcellanz marcellanz Oct 2, 2019

Choose a reason for hiding this comment

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

Had the same thought, yes.
There are other implicit requirements by the shopping cart tck example. Adding negative amounts of line-items, or better not allowing it, as an example. As the shopping cart is used to verify the spec through the TCK, we perhaps might document these requirements within the shoppingcart.proto file.

Copy link
Contributor

Choose a reason for hiding this comment

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

The reason for checking the response as a String was to avoid having parsing influencing things needlessly. I'm very much open for a better way of verifying the responses :)

},
mainClass in assembly := Some("io.cloudstate.samples.shoppingcart.Main"),
assemblyJarName in assembly := "scala-shopping-cart.jar",
test in assembly := {},
// logLevel in assembly := Level.Debug,
assemblyMergeStrategy in assembly := {
/*ADD CUSTOMIZATIONS HERE*/
//case PathList("META-INF", "io.netty.versions.properties") => MergeStrategy.last
case x =>
val oldStrategy = (assemblyMergeStrategy in assembly).value
oldStrategy(x)
}
)

lazy val `akka-client` = (project in file("samples/akka-client"))
.enablePlugins(AkkaGrpcPlugin)
.settings(
Expand Down Expand Up @@ -720,7 +810,7 @@ lazy val `tck` = (project in file("tck"))
fork in test := true,
parallelExecution in IntegrationTest := false,
executeTests in IntegrationTest := (executeTests in IntegrationTest)
.dependsOn(`proxy-core` / assembly, `java-shopping-cart` / assembly)
.dependsOn(`proxy-core` / assembly, `java-shopping-cart` / assembly, `scala-shopping-cart` / assembly)
.value
)

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package io.cloudstate.samples.shoppingcart

import com.example.shoppingcart.ShoppingcartProto
import io.cloudstate.javasupport._

object Main extends App {
new CloudState()
.registerEventSourcedEntity(
classOf[ShoppingCartEntity],
ShoppingcartProto.javaDescriptor.findServiceByName("ShoppingCart"),
com.example.shoppingcart.persistence.DomainProto.javaDescriptor
)
.start
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package io.cloudstate.samples.shoppingcart

import com.example.shoppingcart.persistence.{
Cart => DCart,
ItemAdded => DItemAdded,
ItemRemoved => DItemRemoved,
LineItem => DLineItem
}
import com.example.shoppingcart.{
AddLineItem => SAddLineItem,
Cart => SCart,
LineItem => SLineItem,
RemoveLineItem => SRemoveLineItem
}
import com.google.protobuf.Empty
import io.cloudstate.javasupport.EntityId
import io.cloudstate.javasupport.eventsourced._

import scala.collection.mutable

/** An event sourced entity. */
@EventSourcedEntity
class ShoppingCartEntity(@EntityId val entityId: String) {

private val cart = mutable.LinkedHashMap.empty[String, SLineItem]

@Snapshot
def snapshot: DCart =
DCart(cart.values.map(convert).toSeq)

@SnapshotHandler
def handleSnapshot(cart: DCart): Unit = {
this.cart.clear()
cart.items.foreach { item =>
this.cart.put(item.productId, convert(item))
}
}

@EventHandler
def itemAdded(itemAdded: DItemAdded): Unit = {
val item = cart
.get(itemAdded.getItem.productId)
.fold(convert(itemAdded.getItem))(
item => item.copy(quantity = item.quantity + itemAdded.item.fold(0)(_.quantity))
)
cart.put(item.productId, item)
}

@EventHandler
def itemRemoved(itemRemoved: DItemRemoved): Unit = cart.remove(itemRemoved.productId)

@CommandHandler
def getCart: SCart = SCart(cart.values.toSeq)

@CommandHandler
def addItem(item: SAddLineItem, ctx: CommandContext): Empty = {
if (item.quantity <= 0) ctx.fail("Cannot add negative quantity of to item" + item.productId)
ctx.emit(
DItemAdded(Some(DLineItem(item.productId, item.name, item.quantity)))
)
Empty.getDefaultInstance
}

@CommandHandler
def removeItem(item: SRemoveLineItem, ctx: CommandContext): Empty = {
if (!cart.contains(item.productId)) {
ctx.fail("Cannot remove item " + item.productId + " because it is not in the cart.")
}
ctx.emit(DItemRemoved(item.productId))
Empty.getDefaultInstance
}

private def convert(item: DLineItem) = SLineItem(item.productId, item.name, item.quantity)

private def convert(item: SLineItem) = DLineItem(item.productId, item.name, item.quantity)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package io.cloudstate.javasupport;

/**
* Context that provides client actions, which include failing and forwarding.
*
* <p>These contexts are typically made available in response to commands.
*/
public interface ClientActionContext extends Context {
/**
* Fail the command with the given message.
*
* @param errorMessage The error message to send to the client.
*/
RuntimeException fail(String errorMessage);

/**
* Instruct the proxy to forward handling of this command to another entity served by this
* stateful function.
*
* <p>The command will be forwarded after successful completion of handling this command,
* including any persistence that this command does.
*
* <p>{@link ServiceCall} instances can be created using the {@link ServiceCallFactory} obtained
* from any (including this) contexts {@link Context#serviceCallFactory()} method.
*
* @param to The service call to forward command processing to.
*/
void forward(ServiceCall to);
}