Skip to content

Commit

Permalink
Derivation of DynamoFormat is not automatic anymore
Browse files Browse the repository at this point in the history
Auto derivation of case classes now requires `import com.gu.scanamo.auto._`
and doesn't come automatically as soon as DynamoFormat is in scope
The goal is to keep providing the automatic derivation of DynamoFormat
for case classes without forcing it on scanamo users
  • Loading branch information
TBonnin committed Dec 19, 2018
1 parent 154a11c commit 8daa15a
Show file tree
Hide file tree
Showing 17 changed files with 93 additions and 14 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ Note: the `LocalDynamoDB` object is provided by the `scanamo-testkit` package.
```scala
scala> import com.gu.scanamo._
scala> import com.gu.scanamo.syntax._
scala> import com.gu.scanamo.auto._

scala> val client = LocalDynamoDB.client()
scala> import com.amazonaws.services.dynamodbv2.model.ScalarAttributeType._
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import com.amazonaws.services.dynamodbv2.model.ScalarAttributeType._
import com.gu.scanamo.ops.ScanamoOps
import com.gu.scanamo.query._
import com.gu.scanamo.syntax._
import com.gu.scanamo.auto._
import org.scalatest.concurrent.ScalaFutures
import org.scalatest.time.{Millis, Seconds, Span}
import org.scalatest.{BeforeAndAfterAll, FunSpecLike, Matchers}
Expand Down
1 change: 1 addition & 0 deletions cats/src/test/scala/com/gu/scanamo/ScanamoCatsSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import com.gu.scanamo.error.DynamoReadError
import com.gu.scanamo.ops.ScanamoOps
import com.gu.scanamo.query._
import com.gu.scanamo.syntax._
import com.gu.scanamo.auto._

class ScanamoCatsSpec extends FunSpec with Matchers {

Expand Down
19 changes: 19 additions & 0 deletions docs/src/main/tut/dynamo-format.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,25 @@ Scanamo also supports automatically deriving formats for case classes and
sealed trait families where all the contained types have a defined or derivable
`DynamoFormat`.

### Automatic Derivation

Scanamo can automatically derive `DynamoFormat` for case classes (as long as all its members can also be derived). Ex:

```tut:silent
import com.gu.scanamo.auto._
case class Farm(animals: List[String])
case class Farmer(name: String, age: Long, farm: Farm)
val table = Table[Farmer]("farmer")
table.putAll(
Set(
Farmer("McDonald", 156L, Farm(List("sheep", "cow"))),
Farmer("Boggis", 43L, Farm(List("chicken")))
)
)
```

### Custom Formats

It's also possible to define a serialisation format for types which Scanamo
Expand Down
25 changes: 15 additions & 10 deletions formats/src/main/scala/com/gu/scanamo/DerivedDynamoFormat.scala
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import cats.data.{NonEmptyList, Validated}
import cats.syntax.either._
import com.amazonaws.services.dynamodbv2.model.AttributeValue
import com.gu.scanamo.error._
import com.gu.scanamo.export.Exported
import shapeless._
import shapeless.labelled._

Expand Down Expand Up @@ -95,18 +96,22 @@ trait DerivedDynamoFormat {
implicit def genericProduct[T: NotSymbol, R](
implicit gen: LabelledGeneric.Aux[T, R],
formatR: Lazy[ValidConstructedDynamoFormat[R]]
): DynamoFormat[T] =
new DynamoFormat[T] {
def read(av: AttributeValue): Either[DynamoReadError, T] = formatR.value.read(av).map(gen.from).toEither
def write(t: T): AttributeValue = formatR.value.write(gen.to(t))
}
): Exported[DynamoFormat[T]] =
Exported(
new DynamoFormat[T] {
def read(av: AttributeValue): Either[DynamoReadError, T] = formatR.value.read(av).map(gen.from).toEither
def write(t: T): AttributeValue = formatR.value.write(gen.to(t))
}
)

implicit def genericCoProduct[T, R](
implicit gen: LabelledGeneric.Aux[T, R],
formatR: Lazy[CoProductDynamoFormat[R]]
): DynamoFormat[T] =
new DynamoFormat[T] {
def read(av: AttributeValue): Either[DynamoReadError, T] = formatR.value.read(av).map(gen.from)
def write(t: T): AttributeValue = formatR.value.write(gen.to(t))
}
): Exported[DynamoFormat[T]] =
Exported(
new DynamoFormat[T] {
def read(av: AttributeValue): Either[DynamoReadError, T] = formatR.value.read(av).map(gen.from)
def write(t: T): AttributeValue = formatR.value.write(gen.to(t))
}
)
}
4 changes: 2 additions & 2 deletions formats/src/main/scala/com/gu/scanamo/DynamoFormat.scala
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,9 @@ import scala.reflect.ClassTag
* Right(List(Some(1), None, Some(3)))
* }}}
*
* Also supports automatic derivation for case classes
*
* {{{
* >>> import com.gu.scanamo.auto._
* >>>
* >>> case class Farm(animals: List[String])
* >>> case class Farmer(name: String, age: Long, farm: Farm)
* >>> val farmerF = DynamoFormat[Farmer]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ package com.gu.scanamo

import com.amazonaws.services.dynamodbv2.model.AttributeValue
import com.gu.scanamo.error.{DynamoReadError, TypeCoercionError}
import shapeless.labelled.{field, FieldType}
import com.gu.scanamo.export.Exported
import shapeless.labelled.{FieldType, field}
import shapeless.{:+:, CNil, Coproduct, HNil, Inl, Inr, LabelledGeneric, Witness}

abstract class EnumerationDynamoFormat[T] extends DynamoFormat[T]
Expand All @@ -26,7 +27,7 @@ abstract class EnumerationDynamoFormat[T] extends DynamoFormat[T]
* Zebra
* }}}
*/
trait EnumDynamoFormat extends DerivedDynamoFormat {
trait EnumDynamoFormat extends LowPriorityDynamoFormat {
implicit val enumDynamoFormatCNil: EnumerationDynamoFormat[CNil] = new EnumerationDynamoFormat[CNil] {
override def read(av: AttributeValue): Either[DynamoReadError, CNil] = Left(
TypeCoercionError(new Exception(s"$av is not a recognised member of the Enumeration"))
Expand Down Expand Up @@ -61,3 +62,8 @@ trait EnumDynamoFormat extends DerivedDynamoFormat {
override def write(t: A): AttributeValue = genericFormat.write(gen.to(t))
}
}

trait LowPriorityDynamoFormat {
implicit def dynamoFormat[T](implicit exported: Exported[DynamoFormat[T]]): DynamoFormat[T] =
exported.instance
}
9 changes: 9 additions & 0 deletions formats/src/main/scala/com/gu/scanamo/auto.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.gu.scanamo

/**
* Fully automatic format derivation.
*
* Importing the contents of this package object provides [[com.gu.scanamo.DynamoFormat]]
* instances for case classes (if all members have instances)
*/
package object auto extends DerivedDynamoFormat
3 changes: 3 additions & 0 deletions formats/src/main/scala/com/gu/scanamo/export/Exported.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package com.gu.scanamo.export

final case class Exported[T](instance: T) extends AnyVal
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import com.amazonaws.services.dynamodbv2.model.ScalarAttributeType._
import org.scalacheck._
import org.scalatest._
import org.scalatest.prop.GeneratorDrivenPropertyChecks
import com.gu.scanamo.auto._

class DynamoFormatTest extends FunSpec with Matchers with GeneratorDrivenPropertyChecks {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import com.gu.scanamo.error.DynamoReadError
import com.gu.scanamo.ops.ScanamoOps
import com.gu.scanamo.query._
import com.gu.scanamo.syntax._
import com.gu.scanamo.auto._
import cats.implicits._
import scalaz.zio.{IO, RTS}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import com.gu.scanamo.error.DynamoReadError
import com.gu.scanamo.ops.ScanamoOps
import com.gu.scanamo.query._
import com.gu.scanamo.syntax._
import com.gu.scanamo.auto._
import scalaz.ioeffect.RTS
import scalaz._
import Scalaz._
Expand Down
2 changes: 2 additions & 0 deletions scanamo/src/main/scala/com/gu/scanamo/Scanamo.scala
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ object Scanamo {
* provided synchronously
*
* {{{
* >>> import com.gu.scanamo.auto._
*
* >>> case class Transport(mode: String, line: String)
* >>> val transport = Table[Transport]("transport")
*
Expand Down
4 changes: 4 additions & 0 deletions scanamo/src/main/scala/com/gu/scanamo/SecondaryIndex.scala
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ sealed abstract class SecondaryIndex[V] {
* >>> import com.amazonaws.services.dynamodbv2.model.ScalarAttributeType._
*
* >>> import com.gu.scanamo.syntax._
* >>> import com.gu.scanamo.auto._
*
* >>> LocalDynamoDB.withRandomTableWithSecondaryIndex(client)('name -> S)('antagonist -> S) { (t, i) =>
* ... val table = Table[Bear](t)
Expand All @@ -53,6 +54,7 @@ sealed abstract class SecondaryIndex[V] {
* >>> import com.amazonaws.services.dynamodbv2.model.ScalarAttributeType._
*
* >>> import com.gu.scanamo.syntax._
* >>> import com.gu.scanamo.auto._
*
* >>> LocalDynamoDB.withRandomTableWithSecondaryIndex(client)('organisation -> S, 'repository -> S)('language -> S, 'license -> S) { (t, i) =>
* ... val githubProjects = Table[GithubProject](t)
Expand Down Expand Up @@ -81,6 +83,7 @@ sealed abstract class SecondaryIndex[V] {
* >>> val client = LocalDynamoDB.client()
* >>> import com.amazonaws.services.dynamodbv2.model.ScalarAttributeType._
* >>> import com.gu.scanamo.syntax._
* >>> import com.gu.scanamo.auto._
*
* >>> LocalDynamoDB.withRandomTableWithSecondaryIndex(client)(
* ... 'mode -> S, 'line -> S)('mode -> S, 'colour -> S
Expand Down Expand Up @@ -114,6 +117,7 @@ sealed abstract class SecondaryIndex[V] {
* >>> val client = LocalDynamoDB.client()
* >>> import com.amazonaws.services.dynamodbv2.model.ScalarAttributeType._
* >>> import com.gu.scanamo.syntax._
* >>> import com.gu.scanamo.auto._
*
* >>> LocalDynamoDB.withRandomTableWithSecondaryIndex(client)(
* ... 'mode -> S, 'line -> S)('mode -> S, 'colour -> S
Expand Down
23 changes: 23 additions & 0 deletions scanamo/src/main/scala/com/gu/scanamo/Table.scala
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import scala.collection.JavaConverters._
*
* >>> LocalDynamoDB.withRandomTable(client)('mode -> S, 'line -> S) { t =>
* ... import com.gu.scanamo.syntax._
* ... import com.gu.scanamo.auto._
* ... val transport = Table[Transport](t)
* ... val operations = for {
* ... _ <- transport.putAll(Set(
Expand Down Expand Up @@ -50,6 +51,8 @@ case class Table[V: DynamoFormat](name: String) {
*
* >>> import com.amazonaws.services.dynamodbv2.model.ScalarAttributeType._
* >>> import com.gu.scanamo.syntax._
* >>> import com.gu.scanamo.auto._
*
* >>> val client = LocalDynamoDB.client()
*
* >>> val dataSet = Set(
Expand Down Expand Up @@ -79,6 +82,7 @@ case class Table[V: DynamoFormat](name: String) {
* >>> val client = LocalDynamoDB.client()
* >>> import com.amazonaws.services.dynamodbv2.model.ScalarAttributeType._
* >>> import com.gu.scanamo.syntax._
* >>> import com.gu.scanamo.auto._
*
* >>> LocalDynamoDB.withRandomTableWithSecondaryIndex(client)('mode -> S, 'line -> S)('colour -> S) { (t, i) =>
* ... val transport = Table[Transport](t)
Expand All @@ -97,6 +101,8 @@ case class Table[V: DynamoFormat](name: String) {
* {{{
* >>> case class GithubProject(organisation: String, repository: String, language: String, license: String)
*
* >>> import com.gu.scanamo.auto._
*
* >>> LocalDynamoDB.withRandomTableWithSecondaryIndex(client)('organisation -> S, 'repository -> S)('language -> S, 'license -> S) { (t, i) =>
* ... val githubProjects = Table[GithubProject](t)
* ... val operations = for {
Expand Down Expand Up @@ -129,6 +135,7 @@ case class Table[V: DynamoFormat](name: String) {
*
* >>> LocalDynamoDB.withRandomTable(client)('location -> S) { t =>
* ... import com.gu.scanamo.syntax._
* ... import com.gu.scanamo.auto._
* ... val forecast = Table[Forecast](t)
* ... val operations = for {
* ... _ <- forecast.put(Forecast("London", "Rain"))
Expand All @@ -146,6 +153,7 @@ case class Table[V: DynamoFormat](name: String) {
*
* >>> LocalDynamoDB.withRandomTable(client)('name -> S) { t =>
* ... import com.gu.scanamo.syntax._
* ... import com.gu.scanamo.auto._
* ... val characters = Table[Character](t)
* ... val operations = for {
* ... _ <- characters.put(Character("The Doctor", List("Ecclestone", "Tennant", "Smith")))
Expand All @@ -163,6 +171,7 @@ case class Table[V: DynamoFormat](name: String) {
* {{{
* >>> LocalDynamoDB.withRandomTable(client)('name -> S) { t =>
* ... import com.gu.scanamo.syntax._
* ... import com.gu.scanamo.auto._
* ... val characters = Table[Character](t)
* ... val operations = for {
* ... _ <- characters.update('name -> "James Bond", append('actors -> "Craig"))
Expand All @@ -180,6 +189,7 @@ case class Table[V: DynamoFormat](name: String) {
*
* >>> LocalDynamoDB.withRandomTable(client)('kind -> S) { t =>
* ... import com.gu.scanamo.syntax._
* ... import com.gu.scanamo.auto._
* ... val fruits = Table[Fruit](t)
* ... val operations = for {
* ... _ <- fruits.put(Fruit("watermelon", List("USA")))
Expand All @@ -198,6 +208,7 @@ case class Table[V: DynamoFormat](name: String) {
*
* >>> LocalDynamoDB.withRandomTable(client)('name -> S) { t =>
* ... import com.gu.scanamo.syntax._
* ... import com.gu.scanamo.auto._
* ... val foos = Table[Foo](t)
* ... val operations = for {
* ... _ <- foos.put(Foo("x", 0, List("First")))
Expand All @@ -215,6 +226,7 @@ case class Table[V: DynamoFormat](name: String) {
*
* >>> LocalDynamoDB.withRandomTable(client)('name -> S) { t =>
* ... import com.gu.scanamo.syntax._
* ... import com.gu.scanamo.auto._
* ... val bars = Table[Bar](t)
* ... val operations = for {
* ... _ <- bars.put(Bar("x", 1L, Set("First")))
Expand All @@ -235,6 +247,7 @@ case class Table[V: DynamoFormat](name: String) {
*
* >>> LocalDynamoDB.withRandomTable(client)('id -> S) { t =>
* ... import com.gu.scanamo.syntax._
* ... import com.gu.scanamo.auto._
* ... val outers = Table[Outer](t)
* ... val id = java.util.UUID.fromString("a8345373-9a93-43be-9bcd-e3682c9197f4")
* ... val operations = for {
Expand All @@ -254,6 +267,7 @@ case class Table[V: DynamoFormat](name: String) {
*
* >>> LocalDynamoDB.withRandomTable(client)('id -> S) { t =>
* ... import com.gu.scanamo.syntax._
* ... import com.gu.scanamo.auto._
* ... val things = Table[Thing](t)
* ... val operations = for {
* ... _ <- things.put(Thing("a1", 3, None))
Expand All @@ -277,6 +291,7 @@ case class Table[V: DynamoFormat](name: String) {
*
* >>> LocalDynamoDB.withRandomTable(client)('mode -> S, 'line -> S) { t =>
* ... import com.gu.scanamo.syntax._
* ... import com.gu.scanamo.auto._
* ... val transport = Table[Transport](t)
* ... val operations = for {
* ... _ <- transport.putAll(Set(
Expand Down Expand Up @@ -305,6 +320,7 @@ case class Table[V: DynamoFormat](name: String) {
* >>> val client = LocalDynamoDB.client()
* >>> val (get, scan, query) = LocalDynamoDB.withRandomTable(client)('country -> S, 'name -> S) { t =>
* ... import com.gu.scanamo.syntax._
* ... import com.gu.scanamo.auto._
* ... val cityTable = Table[City](t)
* ... val ops = for {
* ... putRes <- cityTable.putAll(Set(
Expand Down Expand Up @@ -335,6 +351,7 @@ case class Table[V: DynamoFormat](name: String) {
* >>> case class Farmer(name: String, age: Long, farm: Farm)
*
* >>> import com.gu.scanamo.syntax._
* >>> import com.gu.scanamo.auto._
* >>> import com.gu.scanamo.query._
* >>> import com.amazonaws.services.dynamodbv2.model.ScalarAttributeType._
* >>> val client = LocalDynamoDB.client()
Expand Down Expand Up @@ -491,6 +508,7 @@ case class Table[V: DynamoFormat](name: String) {
*
* {{{
* >>> import com.gu.scanamo.syntax._
* >>> import com.gu.scanamo.auto._
* >>> import com.gu.scanamo.query._
* >>> import com.amazonaws.services.dynamodbv2.model.ScalarAttributeType._
* >>> val client = LocalDynamoDB.client()
Expand Down Expand Up @@ -547,6 +565,7 @@ case class Table[V: DynamoFormat](name: String) {
* >>> import com.amazonaws.services.dynamodbv2.model.ScalarAttributeType._
*
* >>> LocalDynamoDB.withRandomTable(client)('name -> S) { t =>
* ... import com.gu.scanamo.auto._
* ... val table = Table[Bear](t)
* ... val ops = for {
* ... _ <- table.put(Bear("Pooh", "honey"))
Expand Down Expand Up @@ -577,6 +596,7 @@ case class Table[V: DynamoFormat](name: String) {
* >>> import com.gu.scanamo.error._
* >>> import com.gu.scanamo.ops._
* >>> import com.gu.scanamo.syntax._
* >>> import com.gu.scanamo.auto._
* >>> import com.gu.scanamo.query._
* >>> import com.amazonaws.services.dynamodbv2.model.ScalarAttributeType._
*
Expand Down Expand Up @@ -609,6 +629,7 @@ case class Table[V: DynamoFormat](name: String) {
* >>> val client = LocalDynamoDB.client()
*
* >>> import com.gu.scanamo.syntax._
* >>> import com.gu.scanamo.auto._
* >>> import com.amazonaws.services.dynamodbv2.model.ScalarAttributeType._
*
* >>> LocalDynamoDB.withRandomTable(client)('mode -> S, 'line -> S) { t =>
Expand Down Expand Up @@ -645,6 +666,7 @@ case class Table[V: DynamoFormat](name: String) {
* >>> import com.gu.scanamo.error._
* >>> import com.gu.scanamo.ops._
* >>> import com.gu.scanamo.syntax._
* >>> import com.gu.scanamo.auto._
* >>> import com.gu.scanamo.query._
* >>> import com.amazonaws.services.dynamodbv2.model.ScalarAttributeType._
*
Expand Down Expand Up @@ -681,6 +703,7 @@ case class Table[V: DynamoFormat](name: String) {
* >>> import com.amazonaws.services.dynamodbv2.model.ScalarAttributeType._
*
* >>> import com.gu.scanamo.syntax._
* >>> import com.gu.scanamo.auto._
*
* >>> LocalDynamoDB.withRandomTable(client)('name -> S) { t =>
* ... val table = Table[Bear](t)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import org.scalatest.{BeforeAndAfterAll, FunSpec, Matchers}
import com.amazonaws.services.dynamodbv2.model.ScalarAttributeType._
import com.gu.scanamo.query._
import com.gu.scanamo.syntax._
import com.gu.scanamo.auto._

class ScanamoAsyncTest extends FunSpec with Matchers with BeforeAndAfterAll with ScalaFutures {
implicit val defaultPatience =
Expand Down
1 change: 1 addition & 0 deletions scanamo/src/test/scala/com/gu/scanamo/ScanamoTest.scala
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import com.gu.scanamo.error.DynamoReadError
import com.gu.scanamo.ops.ScanamoOps
import com.gu.scanamo.query._
import com.gu.scanamo.syntax._
import com.gu.scanamo.auto._

class ScanamoTest extends org.scalatest.FunSpec with org.scalatest.Matchers {

Expand Down

0 comments on commit 8daa15a

Please sign in to comment.