From 59d24dd0bbc9210781c10c6e9ee8115076b99a10 Mon Sep 17 00:00:00 2001 From: Philip Wills Date: Mon, 25 Apr 2016 13:59:12 +0100 Subject: [PATCH 1/3] Introduce an abstraction for Table and Index --- README.md | 23 ++-- build.sbt | 2 + src/main/scala/com/gu/scanamo/Table.scala | 120 ++++++++++++++++++++ src/main/scala/com/gu/scanamo/package.scala | 10 +- 4 files changed, 145 insertions(+), 10 deletions(-) create mode 100644 src/main/scala/com/gu/scanamo/Table.scala diff --git a/README.md b/README.md index 132cc0a97..ca33ed9db 100644 --- a/README.md +++ b/README.md @@ -94,19 +94,24 @@ scala> import com.gu.scanamo.syntax._ scala> val client = LocalDynamoDB.client() scala> import com.amazonaws.services.dynamodbv2.model.ScalarAttributeType._ -scala> val farmersTableResult = LocalDynamoDB.createTable(client)("free")('name -> S) - -scala> case class Free(name: String, number: Int) +scala> val farmersTableResult = LocalDynamoDB.createTable(client)("winners")('name -> S) + +scala> case class LuckyWinner(name: String, shape: String) +scala> def temptWithGum(child: LuckyWinner): LuckyWinner = child match { + | case LuckyWinner("Violet", _) => LuckyWinner("Violet", "blueberry") + | case winner => winner + | } +scala> val luckyWinners = Table[LuckyWinner]("winners") scala> val operations = for { - | _ <- ScanamoFree.putAll("free")(List(Free("Monad", 1), Free("Applicative", 2), Free("Love", 3))) - | maybeMonad <- ScanamoFree.get[Free]("free")('name -> "Monad") - | monad = maybeMonad.flatMap(_.toOption).getOrElse(Free("oops", 9)) - | _ <- ScanamoFree.put("free")(monad.copy(number = monad.number * 10)) - | results <- ScanamoFree.getAll[Free]("free")('name -> List("Monad", "Applicative")) + | _ <- luckyWinners.putAll( + | List(LuckyWinner("Violet", "human"), LuckyWinner("Augustus", "human"), LuckyWinner("Charlie", "human"))) + | winners <- luckyWinners.scan() + | _ <- luckyWinners.putAll(winners.flatMap(_.toOption).map(temptWithGum).toList) + | results <- luckyWinners.getAll('name -> List("Charlie", "Violet")) | } yield results scala> Scanamo.exec(client)(operations).toList -res1: List[cats.data.Xor[error.DynamoReadError, Free]] = List(Right(Free(Monad,10)), Right(Free(Applicative,2))) +res1: List[cats.data.Xor[error.DynamoReadError, LuckyWinner]] = List(Right(LuckyWinner(Charlie,human)), Right(LuckyWinner(Violet,blueberry))) ``` For more details see the [API docs](http://guardian.github.io/scanamo/latest/api/#com.gu.scanamo.Scanamo$) diff --git a/build.sbt b/build.sbt index 1297fd0bd..e004e138b 100644 --- a/build.sbt +++ b/build.sbt @@ -27,6 +27,8 @@ scalacOptions := Seq( "-feature", "-unchecked", "-language:implicitConversions", + "-language:higherKinds", + "-language:existentials", "-Xfatal-warnings", "-Xlint", "-Yinline-warnings", diff --git a/src/main/scala/com/gu/scanamo/Table.scala b/src/main/scala/com/gu/scanamo/Table.scala new file mode 100644 index 000000000..245b9eeb0 --- /dev/null +++ b/src/main/scala/com/gu/scanamo/Table.scala @@ -0,0 +1,120 @@ +package com.gu.scanamo + +import cats.data.Xor +import com.gu.scanamo.error.DynamoReadError +import com.gu.scanamo.ops.ScanamoOps +import com.gu.scanamo.query.Query + +/** + * Represents a DynamoDB table that operations can be performed against + * + * {{{ + * >>> case class Transport(mode: String, line: String) + * >>> val transport = Table[Transport]("transport") + * + * >>> val client = LocalDynamoDB.client() + * >>> import com.amazonaws.services.dynamodbv2.model.ScalarAttributeType._ + * + * >>> LocalDynamoDB.withTable(client)("transport")('mode -> S, 'line -> S) { + * ... import com.gu.scanamo.syntax._ + * ... val operations = for { + * ... _ <- transport.putAll(List( + * ... Transport("Underground", "Circle"), + * ... Transport("Underground", "Metropolitan"), + * ... Transport("Underground", "Central"))) + * ... results <- transport.query('mode -> "Underground" and ('line beginsWith "C")) + * ... } yield results.toList + * ... Scanamo.exec(client)(operations) + * ... } + * List(Right(Transport(Underground,Central)), Right(Transport(Underground,Circle))) + * }}} + */ +case class Table[V: DynamoFormat](name: String) { + /** + * A secondary index on the table which can be scanned, or queried against + * + * {{{ + * >>> case class Transport(mode: String, line: String, colour: String) + * >>> val transport = Table[Transport]("transport") + * + * >>> val client = LocalDynamoDB.client() + * >>> import com.amazonaws.services.dynamodbv2.model.ScalarAttributeType._ + * >>> import com.gu.scanamo.syntax._ + * + * >>> LocalDynamoDB.withTableWithSecondaryIndex(client)("transport", "colour-index")('mode -> S, 'line -> S)('colour -> S) { + * ... val operations = for { + * ... _ <- transport.putAll(List( + * ... Transport("Underground", "Circle", "Yellow"), + * ... Transport("Underground", "Metropolitan", "Maroon"), + * ... Transport("Underground", "Central", "Red"))) + * ... maroonLine <- transport.index("colour-index").query('colour -> "Maroon") + * ... } yield maroonLine.toList + * ... Scanamo.exec(client)(operations) + * ... } + * List(Right(Transport(Underground,Metropolitan,Maroon))) + * }}} + */ + def index(indexName: String) = Index[V](name, indexName) +} + +case class Index[V: DynamoFormat](tableName: String, indexName: String) + +/* typeclass */trait Scannable[T[_], V] { + def scan(t: T[V])(): ScanamoOps[Stream[Xor[DynamoReadError, V]]] +} + +object Scannable { + def apply[T[_], V](implicit s: Scannable[T, V]) = s + + trait Ops[T[_], V] { + val instance: Scannable[T, V] + def self: T[V] + def scan() = instance.scan(self)() + } + + trait ToScannableOps { + implicit def scannableOps[T[_], V](t: T[V])(implicit s: Scannable[T, V]) = new Ops[T, V] { + val instance = s + val self = t + } + } + + implicit def tableScannable[V: DynamoFormat] = new Scannable[Table, V] { + override def scan(t: Table[V])(): ScanamoOps[Stream[Xor[DynamoReadError, V]]] = + ScanamoFree.scan[V](t.name) + } + implicit def indexScannable[V: DynamoFormat] = new Scannable[Index, V] { + override def scan(i: Index[V])(): ScanamoOps[Stream[Xor[DynamoReadError, V]]] = + ScanamoFree.scanIndex[V](i.tableName, i.indexName) + } +} + +/* typeclass */ trait Queryable[T[_], V] { + def query(t: T[V])(query: Query[_]): ScanamoOps[Stream[Xor[DynamoReadError, V]]] +} + +object Queryable { + def apply[T[_], V](implicit s: Queryable[T, V]) = s + + trait Ops[T[_], V] { + val instance: Queryable[T, V] + def self: T[V] + def query(query: Query[_]) = instance.query(self)(query) + } + + trait ToQueryableOps { + implicit def queryableOps[T[_], V](t: T[V])(implicit s: Queryable[T, V]) = new Ops[T, V] { + val instance = s + val self = t + } + } + + implicit def tableQueryable[V: DynamoFormat] = new Queryable[Table, V] { + override def query(t: Table[V])(query: Query[_]): ScanamoOps[Stream[Xor[DynamoReadError, V]]] = + ScanamoFree.query[V](t.name)(query) + } + implicit def indexQueryable[V: DynamoFormat] = new Queryable[Index, V] { + override def query(i: Index[V])(query: Query[_]): ScanamoOps[Stream[Xor[DynamoReadError, V]]] = + ScanamoFree.queryIndex[V](i.tableName, i.indexName)(query) + } +} diff --git a/src/main/scala/com/gu/scanamo/package.scala b/src/main/scala/com/gu/scanamo/package.scala index 5f5bcf3fd..e4e017d1d 100644 --- a/src/main/scala/com/gu/scanamo/package.scala +++ b/src/main/scala/com/gu/scanamo/package.scala @@ -4,7 +4,7 @@ import com.gu.scanamo.query._ package object scanamo { - object syntax { + object syntax extends Scannable.ToScannableOps with Queryable.ToQueryableOps { implicit class SymbolKeyCondition(s: Symbol) { def <[V: DynamoFormat](v: V) = KeyIs(s, LT, v) def >[V: DynamoFormat](v: V) = KeyIs(s, GT, v) @@ -35,5 +35,13 @@ package object scanamo { Query(KeyEquals(pair._1, pair._2)) implicit def toQuery[T: QueryableKeyCondition](t: T) = Query(t) + + implicit class TableOps[V: DynamoFormat](table: Table[V]) { + def put(v: V) = ScanamoFree.put(table.name)(v) + def putAll(vs: List[V]) = ScanamoFree.putAll(table.name)(vs) + def get(key: UniqueKey[_]) = ScanamoFree.get[V](table.name)(key) + def getAll(keys: UniqueKeys[_]) = ScanamoFree.getAll[V](table.name)(keys) + def delete(key: UniqueKey[_]) = ScanamoFree.delete(table.name)(key) + } } } From d728740d6be3a7f56086c71badf8b100f7897a65 Mon Sep 17 00:00:00 2001 From: Philip Wills Date: Mon, 25 Apr 2016 18:59:28 +0100 Subject: [PATCH 2/3] Remove unnecessary implicits --- src/main/scala/com/gu/scanamo/Table.scala | 10 ++++++++-- src/main/scala/com/gu/scanamo/package.scala | 8 -------- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/src/main/scala/com/gu/scanamo/Table.scala b/src/main/scala/com/gu/scanamo/Table.scala index 245b9eeb0..eb7a95687 100644 --- a/src/main/scala/com/gu/scanamo/Table.scala +++ b/src/main/scala/com/gu/scanamo/Table.scala @@ -3,7 +3,7 @@ package com.gu.scanamo import cats.data.Xor import com.gu.scanamo.error.DynamoReadError import com.gu.scanamo.ops.ScanamoOps -import com.gu.scanamo.query.Query +import com.gu.scanamo.query.{Query, UniqueKey, UniqueKeys} /** * Represents a DynamoDB table that operations can be performed against @@ -55,9 +55,15 @@ case class Table[V: DynamoFormat](name: String) { * }}} */ def index(indexName: String) = Index[V](name, indexName) + + def put(v: V) = ScanamoFree.put(name)(v) + def putAll(vs: List[V]) = ScanamoFree.putAll(name)(vs) + def get(key: UniqueKey[_]) = ScanamoFree.get[V](name)(key) + def getAll(keys: UniqueKeys[_]) = ScanamoFree.getAll[V](name)(keys) + def delete(key: UniqueKey[_]) = ScanamoFree.delete(name)(key) } -case class Index[V: DynamoFormat](tableName: String, indexName: String) +private[scanamo] case class Index[V: DynamoFormat](tableName: String, indexName: String) /* typeclass */trait Scannable[T[_], V] { def scan(t: T[V])(): ScanamoOps[Stream[Xor[DynamoReadError, V]]] diff --git a/src/main/scala/com/gu/scanamo/package.scala b/src/main/scala/com/gu/scanamo/package.scala index e4e017d1d..6e7568394 100644 --- a/src/main/scala/com/gu/scanamo/package.scala +++ b/src/main/scala/com/gu/scanamo/package.scala @@ -35,13 +35,5 @@ package object scanamo { Query(KeyEquals(pair._1, pair._2)) implicit def toQuery[T: QueryableKeyCondition](t: T) = Query(t) - - implicit class TableOps[V: DynamoFormat](table: Table[V]) { - def put(v: V) = ScanamoFree.put(table.name)(v) - def putAll(vs: List[V]) = ScanamoFree.putAll(table.name)(vs) - def get(key: UniqueKey[_]) = ScanamoFree.get[V](table.name)(key) - def getAll(keys: UniqueKeys[_]) = ScanamoFree.getAll[V](table.name)(keys) - def delete(key: UniqueKey[_]) = ScanamoFree.delete(table.name)(key) - } } } From d8db9895f1135fb11bddb58ea86a5251a3a3753a Mon Sep 17 00:00:00 2001 From: Philip Wills Date: Wed, 27 Apr 2016 18:39:32 +0100 Subject: [PATCH 3/3] Make README example clearer --- README.md | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index ca33ed9db..a70cba348 100644 --- a/README.md +++ b/README.md @@ -103,11 +103,13 @@ scala> def temptWithGum(child: LuckyWinner): LuckyWinner = child match { | } scala> val luckyWinners = Table[LuckyWinner]("winners") scala> val operations = for { - | _ <- luckyWinners.putAll( - | List(LuckyWinner("Violet", "human"), LuckyWinner("Augustus", "human"), LuckyWinner("Charlie", "human"))) - | winners <- luckyWinners.scan() - | _ <- luckyWinners.putAll(winners.flatMap(_.toOption).map(temptWithGum).toList) - | results <- luckyWinners.getAll('name -> List("Charlie", "Violet")) + | _ <- luckyWinners.putAll( + | List(LuckyWinner("Violet", "human"), LuckyWinner("Augustus", "human"), LuckyWinner("Charlie", "human"))) + | winners <- luckyWinners.scan() + | winnerList = winners.flatMap(_.toOption).toList + | temptedWinners = winnerList.map(temptWithGum) + | _ <- luckyWinners.putAll(temptedWinners) + | results <- luckyWinners.getAll('name -> List("Charlie", "Violet")) | } yield results scala> Scanamo.exec(client)(operations).toList