Skip to content

Commit c4ee2d3

Browse files
author
Georgi Krastev
authored
Allow customized encoding / decoding on a field basis (#206)
Also add `withDefault` decoder combinator.
1 parent 0d5354e commit c4ee2d3

File tree

6 files changed

+66
-20
lines changed

6 files changed

+66
-20
lines changed

build.sbt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,5 +109,5 @@ lazy val sonatypeSettings = {
109109
}
110110

111111
lazy val mimaSettings = Seq(
112-
mimaPreviousArtifacts := Set("io.moia" %% "scynamo" % "0.7.0")
112+
mimaPreviousArtifacts := Set.empty
113113
)

src/main/scala/scynamo/ScynamoDecoder.scala

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,20 @@
11
package scynamo
22

3-
import java.time.Instant
4-
import java.util.UUID
5-
import java.util.concurrent.TimeUnit
63
import cats.data.{EitherNec, NonEmptyChain}
74
import cats.syntax.either._
85
import cats.syntax.parallel._
96
import cats.{Monad, SemigroupK}
107
import scynamo.StackFrame.Index
118
import scynamo.generic.auto.AutoDerivationUnlocked
129
import scynamo.generic.{GenericScynamoDecoder, SemiautoDerivationDecoder}
13-
import shapeless.{tag, Lazy}
10+
import shapeless.labelled.{field, FieldType}
1411
import shapeless.tag.@@
12+
import shapeless.{tag, Lazy}
1513
import software.amazon.awssdk.services.dynamodb.model.AttributeValue
1614

15+
import java.time.Instant
16+
import java.util.UUID
17+
import java.util.concurrent.TimeUnit
1718
import scala.annotation.tailrec
1819
import scala.collection.compat._
1920
import scala.concurrent.duration.{Duration, FiniteDuration}
@@ -41,7 +42,7 @@ object StackFrame {
4142
case class Custom(name: String) extends StackFrame
4243
}
4344

44-
trait ScynamoDecoder[A] extends ScynamoDecoderFunctions {
45+
trait ScynamoDecoder[A] extends ScynamoDecoderFunctions { self =>
4546
def decode(attributeValue: AttributeValue): EitherNec[ScynamoDecodeError, A]
4647

4748
def map[B](f: A => B): ScynamoDecoder[B] =
@@ -57,6 +58,11 @@ trait ScynamoDecoder[A] extends ScynamoDecoderFunctions {
5758
value => f(decode(value))
5859

5960
def defaultValue: Option[A] = None
61+
62+
def withDefault(value: A): ScynamoDecoder[A] = new ScynamoDecoder[A] {
63+
override def decode(attributeValue: AttributeValue) = self.decode(attributeValue)
64+
override val defaultValue = Some(value)
65+
}
6066
}
6167

6268
object ScynamoDecoder extends DefaultScynamoDecoderInstances {
@@ -167,7 +173,16 @@ trait DefaultScynamoDecoderInstances extends ScynamoDecoderFunctions with Scynam
167173
.map(_.toMap)
168174
}
169175

170-
implicit val attributeValueDecoder: ScynamoDecoder[AttributeValue] = attributeValue => Right(attributeValue)
176+
implicit val attributeValueDecoder: ScynamoDecoder[AttributeValue] =
177+
attributeValue => Right(attributeValue)
178+
179+
implicit def fieldDecoder[K, V](implicit V: Lazy[ScynamoDecoder[V]]): ScynamoDecoder[FieldType[K, V]] =
180+
new ScynamoDecoder[FieldType[K, V]] {
181+
override def decode(attributeValue: AttributeValue) =
182+
V.value.decode(attributeValue).map(field[K][V])
183+
override lazy val defaultValue =
184+
V.value.defaultValue.map(field[K][V])
185+
}
171186
}
172187

173188
trait ScynamoIterableDecoder extends LowestPrioAutoDecoder {

src/main/scala/scynamo/ScynamoEncoder.scala

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import scynamo.StackFrame.{Index, MapKey}
77
import scynamo.generic.auto.AutoDerivationUnlocked
88
import scynamo.generic.{GenericScynamoEncoder, SemiautoDerivationEncoder}
99
import shapeless._
10+
import shapeless.labelled.FieldType
1011
import shapeless.tag.@@
1112
import software.amazon.awssdk.services.dynamodb.model.AttributeValue
1213

@@ -115,6 +116,9 @@ trait DefaultScynamoEncoderInstances extends ScynamoIterableEncoder {
115116
case Left(value) => Left(value)
116117
case Right(value) => ScynamoEncoder[A].encode(value)
117118
}
119+
120+
implicit def fieldEncoder[K, V](implicit V: Lazy[ScynamoEncoder[V]]): ScynamoEncoder[FieldType[K, V]] =
121+
field => V.value.encode(field)
118122
}
119123

120124
trait ScynamoIterableEncoder extends LowestPrioAutoEncoder {

src/main/scala/scynamo/generic/ShapelessScynamoDecoder.scala

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ import shapeless._
99
import shapeless.labelled._
1010
import software.amazon.awssdk.services.dynamodb.model.AttributeValue
1111

12+
import java.util
13+
1214
trait ShapelessScynamoDecoder[Base, A] {
1315
def decodeMap(value: java.util.Map[String, AttributeValue]): EitherNec[ScynamoDecodeError, A]
1416
}
@@ -20,21 +22,21 @@ trait DecoderHListInstances extends ScynamoDecoderFunctions {
2022

2123
implicit def deriveHCons[Base, K <: Symbol, V, T <: HList](implicit
2224
key: Witness.Aux[K],
23-
sv: Lazy[ScynamoDecoder[V]],
25+
sv: ScynamoDecoder[FieldType[K, V]],
2426
st: ShapelessScynamoDecoder[Base, T],
2527
opts: ScynamoDerivationOpts[Base] = ScynamoDerivationOpts.default[Base]
2628
): ShapelessScynamoDecoder[Base, FieldType[K, V] :: T] =
2729
value => {
2830
val fieldName = opts.transform(key.value.name)
2931
val fieldAttrValue = Option(value.get(fieldName))
3032

31-
val decodedHead = (fieldAttrValue, sv.value.defaultValue) match {
32-
case (Some(field), _) => sv.value.decode(field).leftMap(x => x.map(_.push(Attr(fieldName))))
33+
val decodedHead = (fieldAttrValue, sv.defaultValue) match {
34+
case (Some(field), _) => sv.decode(field).leftMap(_.map(_.push(Attr(fieldName))))
3335
case (None, Some(default)) => Right(default)
3436
case (None, None) => Either.leftNec(ScynamoDecodeError.missingField(fieldName, value))
3537
}
3638

37-
(decodedHead.map(field[K](_)), st.decodeMap(value)).mapN(_ :: _)
39+
(decodedHead, st.decodeMap(value)).mapN(_ :: _)
3840
}
3941
}
4042

@@ -49,13 +51,12 @@ trait DecoderCoproductInstances extends ScynamoDecoderFunctions {
4951
sv: Lazy[ScynamoDecoder[V]],
5052
st: ShapelessScynamoDecoder[Base, T],
5153
opts: ScynamoSealedTraitOpts[Base] = ScynamoSealedTraitOpts.default[Base]
52-
): ShapelessScynamoDecoder[Base, FieldType[K, V] :+: T] =
53-
value => {
54-
if (value.containsKey(opts.discriminator))
55-
deriveCConsTagged[Base, K, V, T].decodeMap(value)
56-
else
57-
deriveCConsNested[Base, K, V, T].decodeMap(value)
58-
}
54+
): ShapelessScynamoDecoder[Base, FieldType[K, V] :+: T] = new ShapelessScynamoDecoder[Base, FieldType[K, V] :+: T] {
55+
lazy val tagged = deriveCConsTagged[Base, K, V, T]
56+
lazy val nested = deriveCConsNested[Base, K, V, T]
57+
override def decodeMap(value: util.Map[String, AttributeValue]) =
58+
(if (value.containsKey(opts.discriminator)) tagged else nested).decodeMap(value)
59+
}
5960

6061
def deriveCConsTagged[Base, K <: Symbol, V, T <: Coproduct](implicit
6162
key: Witness.Aux[K],

src/main/scala/scynamo/generic/ShapelessScynamoEncoder.scala

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,14 +23,14 @@ trait EncoderHListInstances {
2323

2424
implicit def deriveHCons[Base, K <: Symbol, V, T <: HList](implicit
2525
key: Witness.Aux[K],
26-
sv: Lazy[ScynamoEncoder[V]],
26+
sv: ScynamoEncoder[FieldType[K, V]],
2727
st: ShapelessScynamoEncoder[Base, T],
2828
opts: ScynamoDerivationOpts[Base] = ScynamoDerivationOpts.default[Base]
2929
): ShapelessScynamoEncoder[Base, FieldType[K, V] :: T] =
3030
value => {
3131
val fieldName = opts.transform(key.value.name)
3232

33-
val encodedHead = sv.value.encode(value.head).leftMap(x => x.map(_.push(Attr(fieldName))))
33+
val encodedHead = sv.encode(value.head).leftMap(_.map(_.push(Attr(fieldName))))
3434
val encodedTail = st.encodeMap(value.tail)
3535

3636
(encodedHead, encodedTail).parMapN { case (head, tail) =>

src/test/scala/scynamo/SemiautoDerivationTest.scala

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import scynamo.Mixed.{CaseClass, CaseObject}
66
import scynamo.generic.{ScynamoDerivationOpts, ScynamoSealedTraitOpts}
77
import scynamo.generic.semiauto._
88
import scynamo.syntax.all._
9+
import shapeless.Witness
10+
import shapeless.labelled.FieldType
911
import software.amazon.awssdk.services.dynamodb.model.AttributeValue
1012

1113
class SemiautoDerivationTest extends UnitTest {
@@ -113,6 +115,30 @@ class SemiautoDerivationTest extends UnitTest {
113115
|""".stripMargin shouldNot compile
114116
}
115117
}
118+
119+
"a custom field type encoder or decoder is provided" should {
120+
"override the default one" in {
121+
final case class Apple(variety: String, ripe: Boolean)
122+
object Apple {
123+
val variety = Witness(Symbol("variety"))
124+
val ripe = Witness(Symbol("ripe"))
125+
126+
implicit val varietyEncoder: ScynamoEncoder[FieldType[variety.T, String]] =
127+
ScynamoEncoder.fieldEncoder(ScynamoEncoder.stringEncoder.contramap[String](_.trim))
128+
129+
implicit val ripeDecoder: ScynamoDecoder[FieldType[ripe.T, Boolean]] =
130+
ScynamoDecoder.fieldDecoder(ScynamoDecoder.booleanDecoder.withDefault(true))
131+
132+
implicit val codec: ObjectScynamoCodec[Apple] =
133+
ObjectScynamoCodec.deriveScynamoCodec[Apple]
134+
}
135+
136+
val encoded = Apple("\tGranny Smith\n", ripe = false).encoded.flatMap(_.decode[Apple])
137+
val decoded = Map("variety" -> "Pink Lady".encodedUnsafe).encodedUnsafe.decode[Apple]
138+
encoded should ===(Right(Apple("Granny Smith", ripe = false)))
139+
decoded should ===(Right(Apple("Pink Lady", ripe = true)))
140+
}
141+
}
116142
}
117143

118144
sealed trait Shape

0 commit comments

Comments
 (0)