Skip to content

Commit

Permalink
Merge pull request #101 from guardian/update-attribute-to-attribute
Browse files Browse the repository at this point in the history
Support updating one attribute to the value of another
  • Loading branch information
Phil Wills committed Apr 10, 2017
2 parents cb73809 + c9e4556 commit 10a97ef
Show file tree
Hide file tree
Showing 7 changed files with 56 additions and 19 deletions.
3 changes: 2 additions & 1 deletion src/main/scala/com/gu/scanamo/DerivedDynamoFormat.scala
Expand Up @@ -11,6 +11,7 @@ import collection.JavaConverters._

trait DerivedDynamoFormat {
type ValidatedPropertiesError[T] = Validated[InvalidPropertiesError, T]
type NotSymbol[T] = |¬|[Symbol]#λ[T]

trait ConstructedDynamoFormat[T] {
def read(av: AttributeValue): Validated[InvalidPropertiesError, T]
Expand Down Expand Up @@ -85,7 +86,7 @@ trait DerivedDynamoFormat {
}
}

implicit def genericProduct[T, R](implicit gen: LabelledGeneric.Aux[T, R], formatR: Lazy[ConstructedDynamoFormat[R]]): DynamoFormat[T] =
implicit def genericProduct[T: NotSymbol, R](implicit gen: LabelledGeneric.Aux[T, R], formatR: Lazy[ConstructedDynamoFormat[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))
Expand Down
11 changes: 6 additions & 5 deletions src/main/scala/com/gu/scanamo/DynamoFormat.scala
@@ -1,20 +1,21 @@
package com.gu.scanamo

import java.nio.ByteBuffer
import java.util.UUID

import cats.NotNull
import cats.instances.either._
import cats.instances.list._
import cats.instances.map._
import cats.instances.vector._
import cats.instances.either._
import cats.syntax.traverse._
import cats.syntax.either._
import cats.syntax.traverse._
import com.amazonaws.services.dynamodbv2.model.AttributeValue
import com.gu.scanamo.error._
import simulacrum.typeclass

import collection.JavaConverters._
import scala.collection.JavaConverters._
import scala.reflect.ClassTag
import java.nio.ByteBuffer
import java.util.UUID

/**
* Type class for defining serialisation to and from
Expand Down
Expand Up @@ -57,5 +57,4 @@ trait EnumDynamoFormat extends DerivedDynamoFormat {
override def read(av: AttributeValue): Either[DynamoReadError, A] = genericFormat.read(av).right.map(gen.from)
override def write(t: A): AttributeValue = genericFormat.write(gen.to(t))
}

}
16 changes: 16 additions & 0 deletions src/main/scala/com/gu/scanamo/Table.scala
Expand Up @@ -203,6 +203,22 @@ case class Table[V: DynamoFormat](name: String) {
* ... }
* Right(Outer(a8345373-9a93-43be-9bcd-e3682c9197f4,Middle(x,1,Inner(beta),List(1, 3))))
* }}}
*
* It's possible to update one field to the value of another
* {{{
* >>> case class Thing(id: String, mandatory: Int, optional: Option[Int])
* >>> val things = Table[Thing]("things")
*
* >>> LocalDynamoDB.withTable(client)("things")('id -> S) {
* ... import com.gu.scanamo.syntax._
* ... val operations = for {
* ... _ <- things.put(Thing("a1", 3, None))
* ... updated <- things.update('id -> "a1", set('optional -> 'mandatory))
* ... } yield updated
* ... Scanamo.exec(client)(operations)
* ... }
* Right(Thing(a1,3,Some(3)))
* }}}
*/
def update(key: UniqueKey[_], expression: UpdateExpression): ScanamoOps[Either[DynamoReadError, V]] =
ScanamoFree.update[V](name)(key)(expression)
Expand Down
26 changes: 14 additions & 12 deletions src/main/scala/com/gu/scanamo/ops/ScanamoInterpreters.scala
Expand Up @@ -32,20 +32,22 @@ object ScanamoInterpreters {
)((cond, values) => cond.withExpressionAttributeValues(values.asJava))
)

def javaUpdateRequest(req: ScanamoUpdateRequest): UpdateItemRequest =
req.condition.foldLeft(
new UpdateItemRequest().withTableName(req.tableName).withKey(req.key.asJava)
.withUpdateExpression(req.updateExpression)
.withExpressionAttributeNames(req.attributeNames.asJava)
.withExpressionAttributeValues(req.attributeValues.asJava)
.withReturnValues(ReturnValue.ALL_NEW)
)((r, c) =>
c.attributeValues.foldLeft(
def javaUpdateRequest(req: ScanamoUpdateRequest): UpdateItemRequest = {
val reqWithoutValues =
req.condition.foldLeft(
new UpdateItemRequest().withTableName(req.tableName).withKey(req.key.asJava)
.withUpdateExpression(req.updateExpression)
.withExpressionAttributeNames(req.attributeNames.asJava)
.withReturnValues(ReturnValue.ALL_NEW)
)((r, c) =>
r.withConditionExpression(c.expression).withExpressionAttributeNames(
(c.attributeNames ++ req.attributeNames).asJava)
)((cond, values) => cond.withExpressionAttributeValues(
(values ++ req.attributeValues).asJava))
)
)

val attributeValues = req.condition.flatMap(_.attributeValues).foldLeft(req.attributeValues)(_ ++ _)
if (attributeValues.isEmpty) reqWithoutValues
else reqWithoutValues.withExpressionAttributeValues(attributeValues.asJava)
}

def id(client: AmazonDynamoDB) = new (ScanamoOpsA ~> Id) {
def apply[A](op: ScanamoOpsA[A]): Id[A] = op match {
Expand Down
3 changes: 3 additions & 0 deletions src/main/scala/com/gu/scanamo/package.scala
Expand Up @@ -49,6 +49,8 @@ package object scanamo {
def or[Y: ConditionExpression](y: Y) = OrCondition(x, y)
}

def set(fields: (Field, Field)): UpdateExpression =
UpdateExpression.setFromAttribute(fields)
def set[V: DynamoFormat](fieldValue: (Field, V)): UpdateExpression =
UpdateExpression.set(fieldValue)
def append[V: DynamoFormat](fieldValue: (Field, V)): UpdateExpression =
Expand All @@ -64,6 +66,7 @@ package object scanamo {

implicit def symbolField(s: Symbol): Field = Field.of(s)
implicit def symbolFieldValue[T](sv: (Symbol, T)): (Field, T) = Field.of(sv._1) -> sv._2
implicit def fieldField(ss: (Symbol, Symbol)): (Field, Field) = Field.of(ss._1) -> Field.of(ss._2)

implicit class AndUpdateExpression(x: UpdateExpression) {
def and(y: UpdateExpression): UpdateExpression = AndUpdate(x, y)
Expand Down
15 changes: 15 additions & 0 deletions src/main/scala/com/gu/scanamo/update/UpdateExpression.scala
Expand Up @@ -36,6 +36,10 @@ private[update] sealed trait LeafUpdateExpression {
object UpdateExpression {
def set[V: DynamoFormat](fieldValue: (Field, V)): UpdateExpression =
SetExpression(fieldValue._1, fieldValue._2)
def setFromAttribute(fields: (Field, Field)): UpdateExpression = {
val (to, from) = fields
SetExpression.fromAttribute(from, to)
}
def append[V: DynamoFormat](fieldValue: (Field, V)): UpdateExpression =
AppendExpression(fieldValue._1, fieldValue._2)
def prepend[V: DynamoFormat](fieldValue: (Field, V)): UpdateExpression =
Expand Down Expand Up @@ -96,6 +100,17 @@ object SetExpression {
format.write(value)
))
}
def fromAttribute(from: Field, to: Field): UpdateExpression =
SimpleUpdateExpression(new LeafUpdateExpression {
override def expression: String = s"#${to.placeholder} = #${from.placeholder}"

override def prefixKeys(prefix: String): LeafUpdateExpression = this

override val constantValue: Option[(String, AttributeValue)] = None
override val attributeNames: Map[String, String] = to.attributeNames ++ from.attributeNames
override val updateType: UpdateType = SET
override val attributeValue: Option[(String, AttributeValue)] = None
})
}

private[update] case class LeafAppendExpression (
Expand Down

0 comments on commit 10a97ef

Please sign in to comment.