Skip to content

Commit f3d0eb5

Browse files
author
Georgi Krastev
authored
Add more Java time instances (#259)
* `Duration`, `LocalDate`, `LocalDateTime`, `ZonedDateTime` * Make `ScynamoDecoder` and `ScynamoEncoder` `instance` public * Add `ScynamoDecoder.emap` for partial mapping
1 parent 8c39587 commit f3d0eb5

File tree

5 files changed

+69
-43
lines changed

5 files changed

+69
-43
lines changed

src/main/scala/scynamo/ScynamoDecoder.scala

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

33
import cats.data.{Chain, EitherNec, NonEmptyChain}
4-
import cats.syntax.either._
4+
import cats.syntax.all._
55
import cats.{Monad, SemigroupK}
66
import scynamo.StackFrame.{Index, MapKey}
77
import scynamo.generic.auto.AutoDerivationUnlocked
88
import scynamo.generic.{GenericScynamoDecoder, SemiautoDerivationDecoder}
99
import scynamo.syntax.attributevalue._
10-
import scynamo.wrapper.YearMonthFormatter.yearMonthFormatter
10+
import scynamo.wrapper.DateTimeFormatters
1111
import shapeless.labelled.{field, FieldType}
1212
import shapeless.tag.@@
1313
import shapeless.{tag, Lazy}
1414
import software.amazon.awssdk.services.dynamodb.model.AttributeValue
1515

16-
import java.time.{Instant, YearMonth}
16+
import java.time._
1717
import java.util.UUID
18-
import java.util.concurrent.TimeUnit
1918
import scala.annotation.tailrec
2019
import scala.collection.compat._
2120
import scala.collection.immutable.Seq
@@ -43,6 +42,9 @@ trait ScynamoDecoder[A] extends ScynamoDecoderFunctions { self =>
4342
override def decode(attributeValue: AttributeValue): EitherNec[ScynamoDecodeError, A] = self.decode(attributeValue)
4443
override val defaultValue: Option[A] = Some(value)
4544
}
45+
46+
def emap[B](f: A => EitherNec[ScynamoDecodeError, B]): ScynamoDecoder[B] =
47+
ScynamoDecoder.instance(decode(_).flatMap(f))
4648
}
4749

4850
object ScynamoDecoder extends DefaultScynamoDecoderInstances {
@@ -51,8 +53,8 @@ object ScynamoDecoder extends DefaultScynamoDecoderInstances {
5153
def const[A](value: A): ScynamoDecoder[A] =
5254
instance(_ => Right(value))
5355

54-
// SAM syntax generates anonymous classes because of non-abstract methods like `defaultValue`.
55-
private[scynamo] def instance[A](f: AttributeValue => EitherNec[ScynamoDecodeError, A]): ScynamoDecoder[A] = f(_)
56+
/** SAM syntax generates anonymous classes on Scala 2 because of non-abstract methods like `defaultValue`. */
57+
def instance[A](decode: AttributeValue => EitherNec[ScynamoDecodeError, A]): ScynamoDecoder[A] = decode(_)
5658
}
5759

5860
trait DefaultScynamoDecoderInstances extends ScynamoDecoderFunctions with ScynamoIterableDecoder {
@@ -109,20 +111,10 @@ trait DefaultScynamoDecoderInstances extends ScynamoDecoderFunctions with Scynam
109111
ScynamoDecoder.instance(_.asEither(ScynamoType.Bool))
110112

111113
implicit val instantDecoder: ScynamoDecoder[Instant] =
112-
ScynamoDecoder.instance { attr =>
113-
for {
114-
number <- attr.asEither(ScynamoType.Number)
115-
result <- convert(number, "Long")(_.toLong)
116-
} yield Instant.ofEpochMilli(result)
117-
}
114+
longDecoder.map(Instant.ofEpochMilli)
118115

119116
implicit val instantTtlDecoder: ScynamoDecoder[Instant @@ TimeToLive] =
120-
ScynamoDecoder.instance { attr =>
121-
for {
122-
number <- attr.asEither(ScynamoType.Number)
123-
result <- convert(number, "Long")(_.toLong)
124-
} yield tag[TimeToLive][Instant](Instant.ofEpochSecond(result))
125-
}
117+
longDecoder.map(seconds => tag[TimeToLive](Instant.ofEpochSecond(seconds)))
126118

127119
implicit def seqDecoder[A: ScynamoDecoder]: ScynamoDecoder[Seq[A]] = iterableDecoder
128120
implicit def listDecoder[A: ScynamoDecoder]: ScynamoDecoder[List[A]] = iterableDecoder
@@ -140,13 +132,25 @@ trait DefaultScynamoDecoderInstances extends ScynamoDecoderFunctions with Scynam
140132
longDecoder.map(Duration.fromNanos)
141133

142134
implicit val durationDecoder: ScynamoDecoder[Duration] =
143-
longDecoder.map(Duration(_, TimeUnit.NANOSECONDS))
135+
finiteDurationDecoder.widen
136+
137+
implicit val javaDurationDecoder: ScynamoDecoder[java.time.Duration] =
138+
longDecoder.map(java.time.Duration.ofNanos)
144139

145140
implicit val yearMonthDecoder: ScynamoDecoder[YearMonth] =
146-
ScynamoDecoder.instance(_.asEither(ScynamoType.String).flatMap(convert(_, "YearMonth")(YearMonth.parse(_, yearMonthFormatter))))
141+
stringDecoder.emap(convert(_, "YearMonth")(YearMonth.parse(_, DateTimeFormatters.yearMonth)))
142+
143+
implicit val localDateDecoder: ScynamoDecoder[LocalDate] =
144+
stringDecoder.emap(convert(_, "LocalDate")(LocalDate.parse(_, DateTimeFormatters.localDate)))
145+
146+
implicit val localDateTimeDecoder: ScynamoDecoder[LocalDateTime] =
147+
stringDecoder.emap(convert(_, "LocalDateTime")(LocalDateTime.parse(_, DateTimeFormatters.localDateTime)))
148+
149+
implicit val zonedDateTimeDecoder: ScynamoDecoder[ZonedDateTime] =
150+
stringDecoder.emap(convert(_, "ZonedDateTime")(ZonedDateTime.parse(_, DateTimeFormatters.zonedDateTime)))
147151

148152
implicit val uuidDecoder: ScynamoDecoder[UUID] =
149-
ScynamoDecoder.instance(_.asEither(ScynamoType.String).flatMap(convert(_, "UUID")(UUID.fromString)))
153+
stringDecoder.emap(convert(_, "UUID")(UUID.fromString))
150154

151155
implicit def mapDecoder[A, B](implicit key: ScynamoKeyDecoder[A], value: ScynamoDecoder[B]): ScynamoDecoder[Map[A, B]] =
152156
ScynamoDecoder.instance(_.asEither(ScynamoType.Map).flatMap { attributes =>

src/main/scala/scynamo/ScynamoEncoder.scala

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,13 @@ import cats.syntax.all._
66
import scynamo.StackFrame.{Index, MapKey}
77
import scynamo.generic.auto.AutoDerivationUnlocked
88
import scynamo.generic.{GenericScynamoEncoder, SemiautoDerivationEncoder}
9-
import scynamo.wrapper.YearMonthFormatter.yearMonthFormatter
9+
import scynamo.wrapper.DateTimeFormatters
1010
import shapeless._
1111
import shapeless.labelled.FieldType
1212
import shapeless.tag.@@
1313
import software.amazon.awssdk.services.dynamodb.model.AttributeValue
1414

15-
import java.time.{Instant, YearMonth}
15+
import java.time._
1616
import java.util.UUID
1717
import scala.collection.compat._
1818
import scala.collection.immutable.Seq
@@ -27,8 +27,8 @@ trait ScynamoEncoder[A] { self =>
2727
object ScynamoEncoder extends DefaultScynamoEncoderInstances {
2828
def apply[A](implicit instance: ScynamoEncoder[A]): ScynamoEncoder[A] = instance
2929

30-
// SAM syntax generates anonymous classes because of non-abstract methods like `contramap`.
31-
private[scynamo] def instance[A](f: A => EitherNec[ScynamoEncodeError, AttributeValue]): ScynamoEncoder[A] = f(_)
30+
/** SAM syntax generates anonymous classes on Scala 2 because of non-abstract methods like `contramap`. */
31+
def instance[A](encode: A => EitherNec[ScynamoEncodeError, AttributeValue]): ScynamoEncoder[A] = encode(_)
3232
}
3333

3434
trait DefaultScynamoEncoderInstances extends ScynamoIterableEncoder {
@@ -104,14 +104,26 @@ trait DefaultScynamoEncoderInstances extends ScynamoIterableEncoder {
104104
implicit def someEncoder[A](implicit element: ScynamoEncoder[A]): ScynamoEncoder[Some[A]] =
105105
ScynamoEncoder.instance(some => element.encode(some.get))
106106

107-
implicit val finiteDurationEncoder: ScynamoEncoder[FiniteDuration] =
107+
implicit val durationEncoder: ScynamoEncoder[Duration] =
108108
numberStringEncoder.contramap(_.toNanos.toString)
109109

110-
implicit val durationEncoder: ScynamoEncoder[Duration] =
110+
implicit val finiteDurationEncoder: ScynamoEncoder[FiniteDuration] =
111+
durationEncoder.narrow
112+
113+
implicit val javaDurationEncoder: ScynamoEncoder[java.time.Duration] =
111114
numberStringEncoder.contramap(_.toNanos.toString)
112115

113116
implicit val yearMonthEncoder: ScynamoEncoder[YearMonth] =
114-
stringEncoder.contramap(_.format(yearMonthFormatter))
117+
stringEncoder.contramap(_.format(DateTimeFormatters.yearMonth))
118+
119+
implicit val localDateEncoder: ScynamoEncoder[LocalDate] =
120+
stringEncoder.contramap(_.format(DateTimeFormatters.localDate))
121+
122+
implicit val localDateTimeEncoder: ScynamoEncoder[LocalDateTime] =
123+
stringEncoder.contramap(_.format(DateTimeFormatters.localDateTime))
124+
125+
implicit val zonedDateTimeEncoder: ScynamoEncoder[ZonedDateTime] =
126+
stringEncoder.contramap(_.format(DateTimeFormatters.zonedDateTime))
115127

116128
implicit def mapEncoder[A, B](implicit key: ScynamoKeyEncoder[A], value: ScynamoEncoder[B]): ScynamoEncoder[Map[A, B]] =
117129
ScynamoEncoder.instance { kvs =>
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package scynamo.wrapper
2+
3+
import java.time.format.DateTimeFormatter
4+
5+
/** This collection of formatters ensures consistent encoding and decoding of Java time date types. */
6+
private[scynamo] object DateTimeFormatters {
7+
8+
/** This is a custom formatter for `YearMonth` because "Years outside the range 0000 to 9999 must be prefixed by the plus or minus
9+
* symbol." but the plus symbol is not added by the default `.toString`.
10+
*/
11+
final val yearMonth = DateTimeFormatter.ofPattern("uuuu-MM")
12+
final def localDate = DateTimeFormatter.ISO_LOCAL_DATE
13+
final def localDateTime = DateTimeFormatter.ISO_LOCAL_DATE_TIME
14+
final def zonedDateTime = DateTimeFormatter.ISO_ZONED_DATE_TIME
15+
}

src/main/scala/scynamo/wrapper/YearMonthFormatter.scala

Lines changed: 0 additions & 10 deletions
This file was deleted.

src/test/scala/scynamo/ScynamoCodecProps.scala

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@ import scynamo.generic.semiauto._
77
import scynamo.wrapper.{ScynamoNumberSet, ScynamoStringSet}
88
import shapeless.tag
99

10-
import java.time.{Instant, YearMonth}
1110
import java.time.temporal.ChronoUnit
11+
import java.time._
1212
import java.util.UUID
1313
import scala.concurrent.duration.Duration
1414

@@ -83,19 +83,24 @@ class ScynamoCodecProps extends Properties("ScynamoCodec") {
8383
propertyWithSeed("decode.encode === id (option)", propertySeed) = Prop.forAll { value: Option[Int] => decodeAfterEncodeIsIdentity(value) }
8484

8585
propertyWithSeed("decode.encode === id (finite duration)", propertySeed) =
86-
Prop.forAll(Gen.chooseNum[Long](-9223372036854775807L, 9223372036854775807L)) { value: Long =>
86+
Prop.forAll(Gen.chooseNum(-9223372036854775807L, 9223372036854775807L)) { value =>
8787
decodeAfterEncodeIsIdentity(Duration.fromNanos(value))
8888
}
8989

9090
propertyWithSeed("decode.encode === id (duration)", propertySeed) =
91-
Prop.forAll(Gen.chooseNum[Long](-9223372036854775807L, 9223372036854775807L)) { value: Long =>
91+
Prop.forAll(Gen.chooseNum(-9223372036854775807L, 9223372036854775807L)) { value =>
9292
decodeAfterEncodeIsIdentity(Duration.fromNanos(value): Duration)
9393
}
9494

95-
propertyWithSeed("decode.encode === id (year month)", propertySeed) = Prop.forAll { value: YearMonth =>
96-
decodeAfterEncodeIsIdentity(value)
95+
propertyWithSeed("decode.encode === id (java duration)", propertySeed) = Prop.forAll { value: Long =>
96+
decodeAfterEncodeIsIdentity(java.time.Duration.ofNanos(value))
9797
}
9898

99+
propertyWithSeed("decode.encode === id (year month)", propertySeed) = Prop.forAll(decodeAfterEncodeIsIdentity[YearMonth](_))
100+
propertyWithSeed("decode.encode === id (local date)", propertySeed) = Prop.forAll(decodeAfterEncodeIsIdentity[LocalDate](_))
101+
propertyWithSeed("decode.encode === id (local date time)", propertySeed) = Prop.forAll(decodeAfterEncodeIsIdentity[LocalDateTime](_))
102+
propertyWithSeed("decode.encode === id (zoned date time)", propertySeed) = Prop.forAll(decodeAfterEncodeIsIdentity[ZonedDateTime](_))
103+
99104
propertyWithSeed("decode.encode === id (case class)", propertySeed) = Prop.forAll { value: Int =>
100105
decodeAfterEncodeIsIdentity(ScynamoCodecProps.Foo(value))
101106
}

0 commit comments

Comments
 (0)