Skip to content

Commit

Permalink
Provide a simplified means of creating a DynamoFormat
Browse files Browse the repository at this point in the history
The common case is that there is some potential exception thrown when converting back from what was stored in Dynamo
  • Loading branch information
Philip Wills committed Apr 15, 2016
1 parent 082669c commit 0e4810d
Show file tree
Hide file tree
Showing 3 changed files with 65 additions and 11 deletions.
31 changes: 31 additions & 0 deletions README.md
Expand Up @@ -111,6 +111,37 @@ res1: List[cats.data.ValidatedNel[DynamoReadError, Free]] = List(Valid(Free(Mona

For more details see the [API docs](http://guardian.github.io/scanamo/latest/api/#com.gu.scanamo.Scanamo$)

### Custom Formats

To define a serialisation format for types which Scanamo doesn't already provide:

```scala
scala> import org.joda.time._

scala> import com.gu.scanamo._
scala> import com.gu.scanamo.syntax._

scala> case class Foo(dateTime: DateTime)

scala> val client = LocalDynamoDB.client()
scala> import com.amazonaws.services.dynamodbv2.model.ScalarAttributeType._
scala> val farmersTableResult = LocalDynamoDB.createTable(client)("foo")('dateTime -> S)

scala> implicit val jodaStringFormat = DynamoFormat.coercedXmap[DateTime, String, IllegalArgumentException](
| DateTime.parse(_).withZone(DateTimeZone.UTC)
| )(
| _.toString
| )
scala> val operations = for {
| _ <- ScanamoFree.put("foo")(Foo(new DateTime(0)))
| results <- ScanamoFree.scan[Foo]("foo")
| } yield results

scala> Scanamo.exec(client)(operations).toList
res1: List[cats.data.ValidatedNel[DynamoReadError, Foo]] = List(Valid(Foo(1970-01-01T00:00:00.000Z)))
```


License
-------

Expand Down
43 changes: 33 additions & 10 deletions src/main/scala/com/gu/scanamo/DynamoFormat.scala
@@ -1,5 +1,6 @@
package com.gu.scanamo

import cats.NotNull
import cats.data._
import cats.std.list._
import cats.std.map._
Expand All @@ -9,7 +10,9 @@ import com.amazonaws.services.dynamodbv2.model.AttributeValue
import shapeless._
import shapeless.labelled._
import simulacrum.typeclass

import collection.convert.decorateAll._
import scala.reflect.ClassTag

/**
* Type class for defining serialisation to and from
Expand Down Expand Up @@ -46,6 +49,8 @@ import collection.convert.decorateAll._
* >>> DynamoFormat[LargelyOptional].read(DynamoFormat[Map[String, String]].write(Map("b" -> "X")))
* Valid(LargelyOptional(None,Some(X)))
* }}}
*
* Custom formats can often be most easily defined using [[DynamoFormat.coercedXmap]] or [[DynamoFormat.xmap]]
*/
@typeclass trait DynamoFormat[T] {
def read(av: AttributeValue): ValidatedNel[DynamoReadError, T]
Expand Down Expand Up @@ -79,20 +84,35 @@ object DynamoFormat extends DerivedDynamoFormat {
* ... )
* >>> DynamoFormat[DateTime].read(new AttributeValue().withN("0"))
* Valid(1970-01-01T00:00:00.000Z)
* }}}
*/
def xmap[A, B](r: B => ValidatedNel[DynamoReadError, A])(w: A => B)(implicit f: DynamoFormat[B]) = new DynamoFormat[A] {
override def read(item: AttributeValue): ValidatedNel[DynamoReadError, A] = f.read(item).andThen(r)
override def write(t: A): AttributeValue = f.write(w(t))
}

/**
* Returns a [[DynamoFormat]] for the case where `A` can always be converted `B`,
* with `write`, but `read` may throw an exception for some value of `B`
*
* >>> val jodaStringFormat = DynamoFormat.xmap[LocalDate, String](
* ... s => Validated.valid(LocalDate.parse(s))
* {{{
* >>> import org.joda.time._
*
* >>> val jodaStringFormat = DynamoFormat.coercedXmap[LocalDate, String, IllegalArgumentException](
* ... LocalDate.parse
* ... )(
* ... _.toString
* ... )
* >>> jodaStringFormat.read(jodaStringFormat.write(new LocalDate(2007, 8, 18)))
* Valid(2007-08-18)
*
* >>> import com.amazonaws.services.dynamodbv2.model.AttributeValue
* >>> jodaStringFormat.read(new AttributeValue().withS("Togtogdenoggleplop"))
* Invalid(OneAnd(TypeCoercionError(java.lang.IllegalArgumentException: Invalid format: "Togtogdenoggleplop"),List()))
* }}}
*/
def xmap[A, B](r: B => ValidatedNel[DynamoReadError, A])(w: A => B)(implicit f: DynamoFormat[B]) = new DynamoFormat[A] {
override def read(item: AttributeValue): ValidatedNel[DynamoReadError, A] = f.read(item).andThen(r)
override def write(t: A): AttributeValue = f.write(w(t))
}
def coercedXmap[A, B, T >: scala.Null <: scala.Throwable](read: B => A)(write: A => B)(implicit f: DynamoFormat[B], T: ClassTag[T], NT: NotNull[T]) =
xmap(coerce[B, A, T](read))(write)

/**
* {{{
Expand All @@ -117,23 +137,26 @@ object DynamoFormat extends DerivedDynamoFormat {


private val numFormat = attribute(_.getN, "N")(_.withN)
private def coerce[N](f: String => N): String => ValidatedNel[DynamoReadError, N] = s =>
Validated.catchOnly[NumberFormatException](f(s)).leftMap(TypeCoercionError(_)).toValidatedNel
private def coerceNumber[N](f: String => N): String => ValidatedNel[DynamoReadError, N] =
coerce[String, N, NumberFormatException](f)

private def coerce[A, B, T >: scala.Null <: scala.Throwable](f: A => B)(implicit T: ClassTag[T], NT: NotNull[T]): A => ValidatedNel[DynamoReadError, B] = a =>
Validated.catchOnly[T](f(a)).leftMap(TypeCoercionError(_)).toValidatedNel

/**
* {{{
* prop> (l: Long) =>
* | DynamoFormat[Long].read(DynamoFormat[Long].write(l)) == cats.data.Validated.valid(l)
* }}}
*/
implicit val longFormat = xmap(coerce(_.toLong))(_.toString)(numFormat)
implicit val longFormat = xmap(coerceNumber(_.toLong))(_.toString)(numFormat)
/**
* {{{
* prop> (i: Int) =>
* | DynamoFormat[Int].read(DynamoFormat[Int].write(i)) == cats.data.Validated.valid(i)
* }}}
*/
implicit val intFormat = xmap(coerce(_.toInt))(_.toString)(numFormat)
implicit val intFormat = xmap(coerceNumber(_.toInt))(_.toString)(numFormat)


private val javaListFormat = attribute(_.getL, "L")(_.withL)
Expand Down
2 changes: 1 addition & 1 deletion src/main/scala/com/gu/scanamo/DynamoReadError.scala
Expand Up @@ -6,7 +6,7 @@ import cats.std.list._
sealed abstract class DynamoReadError
case class PropertyReadError(name: String, problem: NonEmptyList[DynamoReadError]) extends DynamoReadError
case class NoPropertyOfType(propertyType: String) extends DynamoReadError
case class TypeCoercionError(e: Exception) extends DynamoReadError
case class TypeCoercionError(t: Throwable) extends DynamoReadError
case object MissingProperty extends DynamoReadError

object DynamoReadError {
Expand Down

0 comments on commit 0e4810d

Please sign in to comment.