Skip to content
This repository was archived by the owner on Jun 13, 2023. It is now read-only.

Commit 98a124c

Browse files
authored
Scala 3.0.0 (#263)
* Fix dynamodb putItem for annotated case classes * Support Scala 3 final in all modules
1 parent 3542e91 commit 98a124c

File tree

8 files changed

+155
-111
lines changed

8 files changed

+155
-111
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -231,7 +231,7 @@ val googlers: Seq[Item] = table.scan(Seq("Company" -> cond.eq("Google")))
231231
table.destroy()
232232
```
233233

234-
PUT method with case class usage
234+
PUT method with case class usage (@hashPK and @rangePK annotations are not currently available in Scala 3)
235235

236236
```scala
237237
import awscala._, dynamodbv2._

build.sbt

Lines changed: 14 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,14 @@
11
import xerial.sbt.Sonatype.autoImport._
22

33
val scala213 = "2.13.4"
4-
val scala3 = "3.0.0-RC3"
4+
val scala3 = "3.0.0"
55

66
lazy val commonSettings = Seq(
77
organization := "com.github.seratch",
88
name := "awscala",
99
version := "0.9.1",
1010
scalaVersion := scala213,
11-
// TODO: set scala3 for all projects
12-
crossScalaVersions := Seq(scala213),
11+
crossScalaVersions := Seq(scala213, scala3),
1312
sbtPlugin := false,
1413
transitiveClassifiers in Global := Seq(Artifact.SourceClassifier),
1514
scalacOptions ++= Seq("-unchecked", "-deprecation", "-feature"),
@@ -74,10 +73,10 @@ lazy val core = project
7473
"com.amazonaws" % "aws-java-sdk-core" % awsJavaSdkVersion,
7574
"joda-time" % "joda-time" % "2.10.10",
7675
"org.joda" % "joda-convert" % "2.2.1",
77-
"org.scala-lang.modules" %% "scala-collection-compat" % "2.4.3",
76+
"org.scala-lang.modules" %% "scala-collection-compat" % "2.4.4",
7877
"org.bouncycastle" % "bcprov-jdk16" % "1.46" % "provided",
7978
"ch.qos.logback" % "logback-classic" % "1.2.3" % "test",
80-
"org.scalatest" %% "scalatest" % "3.2.8" % "test",
79+
"org.scalatest" %% "scalatest" % "3.2.9" % "test",
8180
) ++ {scalaVersion.value.head match {
8281
case '2' => Seq("org.scala-lang" % "scala-reflect" % scalaVersion.value)
8382
case _ => Seq()
@@ -87,19 +86,19 @@ lazy val core = project
8786
lazy val ec2 = awsProject("ec2")
8887
.settings(
8988
libraryDependencies ++= Seq(
90-
("com.decodified" %% "scala-ssh" % "0.11.0" % "provided").cross(CrossVersion.for3Use2_13)
89+
"com.decodified" %% "scala-ssh" % "0.11.1" % "provided"
9190
)
9291
)
9392

94-
lazy val iam = awsProject("iam").settings(crossScalaVersions += scala3)
93+
lazy val iam = awsProject("iam")
9594
lazy val dynamodb = awsProject("dynamodb").settings(dynamoTestSettings)
96-
lazy val emr = awsProject("emr").settings(crossScalaVersions += scala3).dependsOn(ec2 % "test")
97-
lazy val redshift = awsProject("redshift").settings(crossScalaVersions += scala3)
98-
lazy val s3 = awsProject("s3").settings(crossScalaVersions += scala3)
99-
lazy val simpledb = awsProject("simpledb").settings(crossScalaVersions += scala3)
100-
lazy val sqs = awsProject("sqs").settings(crossScalaVersions += scala3)
101-
lazy val sts = awsProject("sts").settings(crossScalaVersions += scala3)
102-
lazy val stepfunctions = awsProject("stepfunctions").settings(crossScalaVersions += scala3).dependsOn(iam % "test")
95+
lazy val emr = awsProject("emr").dependsOn(ec2 % "test")
96+
lazy val redshift = awsProject("redshift")
97+
lazy val s3 = awsProject("s3")
98+
lazy val simpledb = awsProject("simpledb")
99+
lazy val sqs = awsProject("sqs")
100+
lazy val sts = awsProject("sts")
101+
lazy val stepfunctions = awsProject("stepfunctions").dependsOn(iam % "test")
103102

104103
def awsProject(service: String) = {
105104
Project
@@ -110,7 +109,7 @@ def awsProject(service: String) = {
110109
libraryDependencies ++= Seq(
111110
"com.amazonaws" % s"aws-java-sdk-$service" % awsJavaSdkVersion,
112111
"ch.qos.logback" % "logback-classic" % "1.2.3" % "test",
113-
"org.scalatest" %% "scalatest" % "3.2.8" % "test"
112+
"org.scalatest" %% "scalatest" % "3.2.9" % "test"
114113
)
115114
)
116115
.dependsOn(core)
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
package awscala.dynamodbv2
2+
3+
import DynamoDB.SimplePk
4+
5+
import scala.reflect.runtime.{ universe => u }
6+
import scala.reflect.runtime.universe.termNames
7+
8+
import scala.collection.mutable.ListBuffer
9+
import scala.annotation.StaticAnnotation
10+
11+
class hashPK extends StaticAnnotation
12+
class rangePK extends StaticAnnotation
13+
14+
private[dynamodbv2] trait TableCompat { self: Table =>
15+
16+
def putItem[T: u.TypeTag](entity: T)(implicit dynamoDB: DynamoDB): Unit = {
17+
val constructorArgs: Seq[AnnotatedConstructorArgMeta] = extractAnnotatedConstructorArgs(entity)
18+
val getterCallResults: Seq[(String, AnyRef)] = extractGetterNameAndValue(entity)
19+
20+
var maybeHashPK: Option[Any] = None
21+
var maybeRangePK: Option[Any] = None
22+
val attributes: ListBuffer[(String, AnyRef)] = ListBuffer()
23+
for (nameAndValue <- getterCallResults) {
24+
val (name, value) = nameAndValue
25+
constructorArgs.find { arg => name == arg.name } match {
26+
case Some(arg) if arg.annotationNames.exists(_.contains("hashPK")) => maybeHashPK = Some(value)
27+
case Some(arg) if arg.annotationNames.exists(_.contains("rangePK")) => maybeRangePK = Some(value)
28+
case _ => attributes += nameAndValue
29+
}
30+
}
31+
(maybeHashPK, maybeRangePK) match {
32+
case (Some(hashPK), Some(rangePK)) =>
33+
dynamoDB.put(this, hashPK, rangePK, attributes.toSeq: _*)
34+
case (Some(hashPK), None) =>
35+
dynamoDB.put(this, hashPK, attributes.toSeq: _*)
36+
case _ =>
37+
throw new Exception(s"Primary key is not defined for ${entity.getClass.getName} (constructor args are $constructorArgs)")
38+
}
39+
}
40+
41+
def putItem(hashPK: Any, attributes: SimplePk*)(implicit dynamoDB: DynamoDB): Unit = {
42+
dynamoDB.put(this, hashPK, attributes: _*)
43+
}
44+
def putItem(hashPK: Any, rangePK: Any, attributes: SimplePk*)(implicit dynamoDB: DynamoDB): Unit = {
45+
dynamoDB.put(this, hashPK, rangePK, attributes: _*)
46+
}
47+
48+
private case class AnnotatedConstructorArgMeta(name: String, annotationNames: Seq[String])
49+
50+
private def extractAnnotatedConstructorArgs[T: u.TypeTag](entity: T): Seq[AnnotatedConstructorArgMeta] = {
51+
u.typeOf[entity.type].decl(termNames.CONSTRUCTOR).asMethod.paramLists.flatten
52+
.collect({
53+
case t if t != null && t.annotations.nonEmpty =>
54+
Some(AnnotatedConstructorArgMeta(
55+
name = t.name.toString,
56+
// FIXME: should we use canonical name?
57+
annotationNames = t.annotations.map(_.toString)))
58+
case _ => None
59+
}).flatten
60+
}
61+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package awscala.dynamodbv2
2+
3+
import DynamoDB.SimplePk
4+
5+
private[dynamodbv2] trait TableCompat { self: Table =>
6+
def putItem(hashPK: Any, attributes: SimplePk*)(implicit dynamoDB: DynamoDB): Unit = {
7+
dynamoDB.put(this, hashPK, attributes: _*)
8+
}
9+
def putItem(hashPK: Any, rangePK: Any, attributes: SimplePk*)(implicit dynamoDB: DynamoDB): Unit = {
10+
dynamoDB.put(this, hashPK, rangePK, attributes: _*)
11+
}
12+
}

dynamodb/src/main/scala/awscala/dynamodbv2/ResultPager.scala

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -82,9 +82,10 @@ sealed trait ResultPager[TReq, TRes] extends Iterator[Item] {
8282
} else if (lastKey == null) {
8383
false
8484
} else {
85-
do {
85+
while ({
8686
nextPage(withExclusiveStartKey(request, lastKey))
87-
} while (lastKey != null && items.isEmpty) // there are potentially more matching data, but this page didn't contain any
87+
lastKey != null && items.isEmpty // there are potentially more matching data, but this page didn't contain any
88+
}) {}
8889
items.nonEmpty
8990
}
9091
}

dynamodb/src/main/scala/awscala/dynamodbv2/Table.scala

Lines changed: 2 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,7 @@ import DynamoDB.{ CompositePk, SimplePk }
55
import java.lang.reflect.Modifier
66
import com.amazonaws.services.{ dynamodbv2 => aws }
77

8-
import scala.annotation.StaticAnnotation
98
import scala.collection.mutable.ListBuffer
10-
import scala.reflect.runtime.{ universe => u }
11-
import scala.reflect.runtime.universe.termNames
129

1310
object Table {
1411

@@ -25,9 +22,6 @@ object Table {
2522
globalSecondaryIndexes, provisionedThroughput, Option(billingMode))
2623
}
2724

28-
class hashPK extends StaticAnnotation
29-
class rangePK extends StaticAnnotation
30-
3125
case class Table(
3226
name: String,
3327
hashPK: String,
@@ -36,7 +30,7 @@ case class Table(
3630
localSecondaryIndexes: Seq[LocalSecondaryIndex] = Nil,
3731
globalSecondaryIndexes: Seq[GlobalSecondaryIndex] = Nil,
3832
provisionedThroughput: Option[ProvisionedThroughput] = None,
39-
billingMode: Option[aws.model.BillingMode] = None) {
33+
billingMode: Option[aws.model.BillingMode] = None) extends TableCompat {
4034

4135
// ------------------------------------------
4236
// Items
@@ -74,46 +68,7 @@ case class Table(
7468
dynamoDB.put(this, hashPK, rangePK, attrs: _*)
7569
}
7670

77-
def putItem[T: u.TypeTag](entity: T)(implicit dynamoDB: DynamoDB): Unit = {
78-
val constructorArgs: Seq[AnnotatedConstructorArgMeta] = extractAnnotatedConstructorArgs(entity)
79-
val getterCallResults: Seq[(String, AnyRef)] = extractGetterNameAndValue(entity)
80-
81-
var maybeHashPK: Option[Any] = None
82-
var maybeRangePK: Option[Any] = None
83-
val attributes: ListBuffer[(String, AnyRef)] = ListBuffer()
84-
for (arg <- constructorArgs) {
85-
getterCallResults.find { case (name, _) => name == arg.name } match {
86-
case Some((_, value)) if arg.annotationNames.contains("hashPK") => maybeHashPK = Some(value)
87-
case Some((_, value)) if arg.annotationNames.contains("rangePK") => maybeRangePK = Some(value)
88-
case Some(nameAndValue) => attributes += nameAndValue
89-
case _ => // noop
90-
}
91-
}
92-
(maybeHashPK, maybeRangePK) match {
93-
case (Some(hashPK), Some(rangePK)) =>
94-
dynamoDB.put(this, hashPK, rangePK, attributes.toSeq: _*)
95-
case (Some(hashPK), None) =>
96-
dynamoDB.put(this, hashPK, attributes.toSeq: _*)
97-
case _ =>
98-
throw new Exception(s"Primary key is not defined for ${entity.getClass.getName}")
99-
}
100-
}
101-
102-
private case class AnnotatedConstructorArgMeta(name: String, annotationNames: Seq[String])
103-
104-
private def extractAnnotatedConstructorArgs[T: u.TypeTag](entity: T): Seq[AnnotatedConstructorArgMeta] = {
105-
u.typeOf[entity.type].decl(termNames.CONSTRUCTOR).asMethod.paramLists.flatten
106-
.collect({
107-
case t if t != null && t.annotations.nonEmpty =>
108-
Some(AnnotatedConstructorArgMeta(
109-
name = t.name.toString,
110-
// FIXME: should we use canonical name?
111-
annotationNames = t.annotations.map(_.toString)))
112-
case _ => None
113-
}).flatten
114-
}
115-
116-
private def extractGetterNameAndValue(obj: Any): Seq[(String, AnyRef)] = {
71+
protected def extractGetterNameAndValue(obj: Any): Seq[(String, AnyRef)] = {
11772
val clazz = obj.getClass
11873
val privateInstanceFieldNames: Seq[String] = clazz.getDeclaredFields
11974
.filter(f =>
@@ -132,14 +87,6 @@ case class Table(
13287

13388
getterMethodNames.map(name => (name, clazz.getDeclaredMethod(name).invoke(obj)))
13489
}
135-
136-
def putItem(hashPK: Any, attributes: SimplePk*)(implicit dynamoDB: DynamoDB): Unit = {
137-
dynamoDB.put(this, hashPK, attributes: _*)
138-
}
139-
def putItem(hashPK: Any, rangePK: Any, attributes: SimplePk*)(implicit dynamoDB: DynamoDB): Unit = {
140-
dynamoDB.put(this, hashPK, rangePK, attributes: _*)
141-
}
142-
14390
def delete(hashPK: Any)(implicit dynamoDB: DynamoDB): Unit = deleteItem(hashPK)
14491
def delete(hashPK: Any, rangePK: Any)(implicit dynamoDB: DynamoDB): Unit = deleteItem(hashPK, rangePK)
14592

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
package awscala
2+
3+
import java.util.Date
4+
5+
import awscala.dynamodbv2._
6+
import com.amazonaws.services.dynamodbv2.model.{ ProvisionedThroughputDescription, TableDescription, TableStatus }
7+
import com.amazonaws.services.dynamodbv2.util.TableUtils
8+
import com.amazonaws.services.{ dynamodbv2 => aws }
9+
import org.scalatest._
10+
import org.slf4j._
11+
12+
import scala.util.Try
13+
import org.scalatest.flatspec.AnyFlatSpec
14+
import org.scalatest.matchers.should.Matchers
15+
class DynamoDBV2AnnotationsSpec extends AnyFlatSpec with Matchers {
16+
17+
behavior of "DynamoDB"
18+
19+
val log: Logger = LoggerFactory.getLogger(this.getClass)
20+
21+
case class TestMember(
22+
@hashPK val id: Int,
23+
@rangePK val Country: String,
24+
val Company: String,
25+
val Name: String,
26+
val Age: Int)
27+
it should "allows using case class with annotation in put method" in {
28+
implicit val dynamoDB: DynamoDB = DynamoDB.local()
29+
val tableName = s"Members_${System.currentTimeMillis}"
30+
val createdTableMeta: TableMeta = dynamoDB.createTable(
31+
name = tableName,
32+
hashPK = "Id" -> AttributeType.Number,
33+
rangePK = "Country" -> AttributeType.String,
34+
otherAttributes = Seq(
35+
"Company" -> AttributeType.String),
36+
indexes = Seq(
37+
LocalSecondaryIndex(
38+
name = "CompanyIndex",
39+
keySchema = Seq(KeySchema("Id", KeyType.Hash), KeySchema("Company", KeyType.Range)),
40+
projection = Projection(ProjectionType.Include, Seq("Company")))))
41+
log.info(s"Created Table: $createdTableMeta")
42+
43+
println(s"Waiting for DynamoDB table activation...")
44+
TableUtils.waitUntilActive(dynamoDB, createdTableMeta.name)
45+
println("")
46+
println(s"Created DynamoDB table has been activated.")
47+
48+
val members: Table = dynamoDB.table(tableName).get
49+
val member = TestMember(1, "PL", "DataMass", "Alex", 29)
50+
println(members)
51+
members.putItem(member)
52+
53+
println(members)
54+
println(members.get(1, "PL"))
55+
println(members.get(1, "PL").get.attributes.find(_.name == "Name").get.value)
56+
57+
members.get(1, "PL").get.attributes.find(_.name == "Name").get.value.s.get should equal("Alex")
58+
members.get(1, "PL").get.attributes.find(_.name == "Company").get.value.s.get should equal("DataMass")
59+
members.get(1, "PL").get.attributes.find(_.name == "Country").get.value.s.get should equal("PL")
60+
members.destroy()
61+
}
62+
}

dynamodb/src/test/scala/awscala/DynamoDBV2Spec.scala

Lines changed: 0 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -103,44 +103,6 @@ class DynamoDBV2Spec extends AnyFlatSpec with Matchers {
103103
members.destroy()
104104
}
105105

106-
case class TestMember(
107-
@hashPK id: Int,
108-
@rangePK Country: String,
109-
Company: String,
110-
Name: String,
111-
Age: Int)
112-
it should "allows using case class with annotation in put method" in {
113-
implicit val dynamoDB: DynamoDB = DynamoDB.local()
114-
val tableName = s"Members_${System.currentTimeMillis}"
115-
val createdTableMeta: TableMeta = dynamoDB.createTable(
116-
name = tableName,
117-
hashPK = "Id" -> AttributeType.Number,
118-
rangePK = "Country" -> AttributeType.String,
119-
otherAttributes = Seq(
120-
"Company" -> AttributeType.String),
121-
indexes = Seq(
122-
LocalSecondaryIndex(
123-
name = "CompanyIndex",
124-
keySchema = Seq(KeySchema("Id", KeyType.Hash), KeySchema("Company", KeyType.Range)),
125-
projection = Projection(ProjectionType.Include, Seq("Company")))))
126-
log.info(s"Created Table: $createdTableMeta")
127-
128-
println(s"Waiting for DynamoDB table activation...")
129-
TableUtils.waitUntilActive(dynamoDB, createdTableMeta.name)
130-
println("")
131-
println(s"Created DynamoDB table has been activated.")
132-
133-
val members: Table = dynamoDB.table(tableName).get
134-
val member = TestMember(1, "PL", "DataMass", "Alex", 29)
135-
136-
members.putItem(member)
137-
138-
members.get(1, "PL").get.attributes.find(_.name == "Name").get.value.s.get should equal("Alex")
139-
members.get(1, "PL").get.attributes.find(_.name == "Company").get.value.s.get should equal("DataMass")
140-
members.get(1, "PL").get.attributes.find(_.name == "Country").get.value.s.get should equal("PL")
141-
members.destroy()
142-
}
143-
144106
it should "provide cool APIs for Hash/Range PK tables" in {
145107
implicit val dynamoDB: DynamoDB = DynamoDB.local()
146108

0 commit comments

Comments
 (0)