11package scynamo
22
3- import cats .data . EitherNec
4- import cats .syntax . either . _
5- import cats .syntax .parallel ._
3+ import cats .Contravariant
4+ import cats .data .{ Chain , EitherNec , NonEmptyChain }
5+ import cats .syntax .all ._
66import scynamo .StackFrame .{Index , MapKey }
77import scynamo .generic .auto .AutoDerivationUnlocked
88import scynamo .generic .{GenericScynamoEncoder , SemiautoDerivationEncoder }
@@ -12,119 +12,151 @@ import shapeless.tag.@@
1212import software .amazon .awssdk .services .dynamodb .model .AttributeValue
1313
1414import java .time .Instant
15- import java .util .UUID
15+ import java .util .{Collections , UUID }
16+ import scala .collection .compat ._
17+ import scala .collection .immutable .Seq
1618import scala .concurrent .duration .{Duration , FiniteDuration }
17- import scala .jdk .CollectionConverters ._
1819
1920trait ScynamoEncoder [A ] { self =>
2021 def encode (value : A ): EitherNec [ScynamoEncodeError , AttributeValue ]
21-
22- def contramap [ B ]( f : B => A ) : ScynamoEncoder [ B ] = value => self.encode(f(value))
22+ def contramap [ B ]( f : B => A ) : ScynamoEncoder [ B ] =
23+ ScynamoEncoder .instance( value => self.encode(f(value) ))
2324}
2425
2526object ScynamoEncoder extends DefaultScynamoEncoderInstances {
2627 def apply [A ](implicit instance : ScynamoEncoder [A ]): ScynamoEncoder [A ] = instance
28+
29+ // SAM syntax generates anonymous classes because of non-abstract methods like `contramap`.
30+ private [scynamo] def instance [A ](f : A => EitherNec [ScynamoEncodeError , AttributeValue ]): ScynamoEncoder [A ] = f(_)
2731}
2832
2933trait DefaultScynamoEncoderInstances extends ScynamoIterableEncoder {
30- implicit val stringEncoder : ScynamoEncoder [String ] = value => Right (AttributeValue .builder().s(value).build())
34+ private val rightNul = Right (AttributeValue .builder.nul(true ).build())
35+
36+ implicit val catsInstances : Contravariant [ScynamoEncoder ] = new Contravariant [ScynamoEncoder ] {
37+ override def contramap [A , B ](fa : ScynamoEncoder [A ])(f : B => A ) = fa.contramap(f)
38+ }
3139
32- private [this ] val numberStringEncoder : ScynamoEncoder [String ] = value => Right (AttributeValue .builder().n(value).build())
40+ implicit val stringEncoder : ScynamoEncoder [String ] =
41+ ScynamoEncoder .instance(value => Right (AttributeValue .builder.s(value).build()))
3342
34- implicit val intEncoder : ScynamoEncoder [Int ] = numberStringEncoder.contramap[Int ](_.toString)
43+ private [this ] val numberStringEncoder : ScynamoEncoder [String ] =
44+ ScynamoEncoder .instance(value => Right (AttributeValue .builder.n(value).build()))
3545
36- implicit val longEncoder : ScynamoEncoder [Long ] = numberStringEncoder.contramap[Long ](_.toString)
46+ implicit val intEncoder : ScynamoEncoder [Int ] =
47+ numberStringEncoder.contramap(_.toString)
3748
38- implicit val bigIntEncoder : ScynamoEncoder [BigInt ] = numberStringEncoder.contramap[BigInt ](_.toString)
49+ implicit val longEncoder : ScynamoEncoder [Long ] =
50+ numberStringEncoder.contramap(_.toString)
3951
40- implicit val floatEncoder : ScynamoEncoder [Float ] = numberStringEncoder.contramap[Float ](_.toString)
52+ implicit val bigIntEncoder : ScynamoEncoder [BigInt ] =
53+ numberStringEncoder.contramap(_.toString)
4154
42- implicit val doubleEncoder : ScynamoEncoder [Double ] = numberStringEncoder.contramap[Double ](_.toString)
55+ implicit val floatEncoder : ScynamoEncoder [Float ] =
56+ numberStringEncoder.contramap(_.toString)
4357
44- implicit val bigDecimalEncoder : ScynamoEncoder [BigDecimal ] = numberStringEncoder.contramap[BigDecimal ](_.toString)
58+ implicit val doubleEncoder : ScynamoEncoder [Double ] =
59+ numberStringEncoder.contramap(_.toString)
4560
46- implicit val booleanEncoder : ScynamoEncoder [Boolean ] = value => Right (AttributeValue .builder().bool(value).build())
61+ implicit val bigDecimalEncoder : ScynamoEncoder [BigDecimal ] =
62+ numberStringEncoder.contramap(_.toString)
4763
48- implicit val instantEncoder : ScynamoEncoder [Instant ] = numberStringEncoder.contramap[Instant ](_.toEpochMilli.toString)
64+ implicit val booleanEncoder : ScynamoEncoder [Boolean ] =
65+ ScynamoEncoder .instance(value => Right (AttributeValue .builder.bool(value).build()))
66+
67+ implicit val instantEncoder : ScynamoEncoder [Instant ] =
68+ numberStringEncoder.contramap(_.toEpochMilli.toString)
4969
5070 implicit val instantTtlEncoder : ScynamoEncoder [Instant @@ TimeToLive ] =
5171 numberStringEncoder.contramap[Instant @@ TimeToLive ](_.getEpochSecond.toString)
5272
53- implicit val uuidEncoder : ScynamoEncoder [UUID ] = stringEncoder.contramap[UUID ](_.toString)
73+ implicit val uuidEncoder : ScynamoEncoder [UUID ] =
74+ stringEncoder.contramap(_.toString)
75+
76+ implicit def seqEncoder [A ](implicit element : ScynamoEncoder [A ]): ScynamoEncoder [Seq [A ]] =
77+ ScynamoEncoder .instance { xs =>
78+ var allErrors = Chain .empty[ScynamoEncodeError ]
79+ val attrValues = List .newBuilder[AttributeValue ]
80+ for ((x, i) <- xs.iterator.zipWithIndex) element.encode(x) match {
81+ case Right (attr) => attrValues += attr
82+ case Left (errors) => allErrors ++= StackFrame .encoding(errors, Index (i)).toChain
83+ }
5484
55- implicit def seqEncoder [ A : ScynamoEncoder ] : ScynamoEncoder [scala.collection.immutable. Seq [ A ]] =
56- value => value.toVector.parTraverse( ScynamoEncoder [ A ].encode).map(xs => AttributeValue .builder().l( xs : _* ).build())
85+ NonEmptyChain .fromChain(allErrors).toLeft( AttributeValue .builder.l(attrValues.result() : _* ).build())
86+ }
5787
5888 implicit def listEncoder [A : ScynamoEncoder ]: ScynamoEncoder [List [A ]] =
59- value =>
60- value.zipWithIndex
61- .parTraverse { case (x, i) =>
62- ScynamoEncoder [A ].encode(x).leftMap(_.map(_.push(Index (i))))
63- }
64- .map(xs => AttributeValue .builder().l(xs : _* ).build())
89+ seqEncoder[A ].narrow
6590
6691 implicit def vectorEncoder [A : ScynamoEncoder ]: ScynamoEncoder [Vector [A ]] =
67- value =>
68- value.zipWithIndex
69- .parTraverse { case (x, i) =>
70- ScynamoEncoder [A ].encode(x).leftMap(_.map(_.push(Index (i))))
71- }
72- .map(xs => AttributeValue .builder().l(xs : _* ).build())
73-
74- implicit def setEncoder [A : ScynamoEncoder ]: ScynamoEncoder [Set [A ]] = listEncoder[A ].contramap[Set [A ]](x => x.toList)
75-
76- implicit def optionEncoder [A : ScynamoEncoder ]: ScynamoEncoder [Option [A ]] = {
77- case Some (value) => ScynamoEncoder [A ].encode(value)
78- case None => Right (AttributeValue .builder().nul(true ).build())
79- }
92+ seqEncoder[A ].narrow
8093
81- implicit def someEncoder [A : ScynamoEncoder ]: ScynamoEncoder [Some [A ]] = x => ScynamoEncoder [A ].encode(x.get)
94+ implicit def setEncoder [A : ScynamoEncoder ]: ScynamoEncoder [Set [A ]] =
95+ listEncoder[A ].contramap(_.toList)
8296
83- implicit val finiteDurationEncoder : ScynamoEncoder [FiniteDuration ] = longEncoder.contramap(_.toNanos)
84-
85- implicit val durationEncoder : ScynamoEncoder [Duration ] = longEncoder.contramap(_.toNanos)
97+ implicit def optionEncoder [A ](implicit element : ScynamoEncoder [A ]): ScynamoEncoder [Option [A ]] =
98+ ScynamoEncoder .instance {
99+ case Some (value) => element.encode(value)
100+ case None => rightNul
101+ }
86102
87- implicit def mapEncoder [A , B ](implicit keyEncoder : ScynamoKeyEncoder [A ], valueEncoder : ScynamoEncoder [B ]): ScynamoEncoder [Map [A , B ]] =
88- value => {
89- value.toVector
90- .parTraverse { case (k, v) =>
91- (keyEncoder.encode(k), valueEncoder.encode(v)).parMapN(_ -> _).leftMap(_.map(_.push(MapKey (k))))
92- }
93- .map {
94- _.foldLeft(new java.util.HashMap [String , AttributeValue ]()) { case (acc, (k, v)) =>
95- acc.put(k, v)
96- acc
97- }
103+ implicit def someEncoder [A ](implicit element : ScynamoEncoder [A ]): ScynamoEncoder [Some [A ]] =
104+ ScynamoEncoder .instance(some => element.encode(some.get))
105+
106+ implicit val finiteDurationEncoder : ScynamoEncoder [FiniteDuration ] =
107+ numberStringEncoder.contramap(_.toNanos.toString)
108+
109+ implicit val durationEncoder : ScynamoEncoder [Duration ] =
110+ numberStringEncoder.contramap(_.toNanos.toString)
111+
112+ implicit def mapEncoder [A , B ](implicit key : ScynamoKeyEncoder [A ], value : ScynamoEncoder [B ]): ScynamoEncoder [Map [A , B ]] =
113+ ScynamoEncoder .instance { kvs =>
114+ var allErrors = Chain .empty[ScynamoEncodeError ]
115+ val attrValues = new java.util.HashMap [String , AttributeValue ](kvs.size)
116+ kvs.foreachEntry { (k, v) =>
117+ (key.encode(k), value.encode(v)) match {
118+ case (Right (k), Right (attr)) =>
119+ // Omit `nul` for efficiency and GSI support (see https://github.com/aws/aws-sdk-go/issues/1803)
120+ if (! attr.nul) attrValues.put(k, attr)
121+ case (Left (errors), Right (_)) =>
122+ allErrors ++= StackFrame .encoding(errors, MapKey (k)).toChain
123+ case (Right (_), Left (errors)) =>
124+ allErrors ++= StackFrame .encoding(errors, MapKey (k)).toChain
125+ case (Left (kErrors), Left (vErrors)) =>
126+ allErrors ++= StackFrame .encoding(kErrors ++ vErrors, MapKey (k)).toChain
98127 }
99- .map(hm => AttributeValue .builder().m(hm).build())
128+ }
129+
130+ NonEmptyChain .fromChain(allErrors).toLeft(AttributeValue .builder.m(attrValues).build())
100131 }
101132
102- implicit val attributeValueEncoder : ScynamoEncoder [AttributeValue ] = { value =>
103- import scynamo .syntax .attributevalue ._
133+ implicit val attributeValueEncoder : ScynamoEncoder [AttributeValue ] =
134+ ScynamoEncoder .instance { value =>
135+ import scynamo .syntax .attributevalue ._
104136
105- val nonEmptyStringSet = value.asOption( ScynamoType .StringSet ).map(x => ScynamoType .StringSet -> (x.size() > 0 ))
106- val nonEmptyNumberSet = value.asOption(ScynamoType . NumberSet ).map(x => ScynamoType . NumberSet -> (x.size() > 0 ) )
107- val nonEmptyBinarySet = value.asOption( ScynamoType . BinarySet ).map(x => ScynamoType . BinarySet -> (x.size() > 0 ))
137+ def nonEmpty [ A ]( typ : ScynamoType .Aux [java.util. List [ A ]] with ScynamoType .TypeInvalidIfEmpty ) =
138+ if ( value.asOption(typ).exists( ! _.isEmpty)) Right (value )
139+ else Either .leftNec( ScynamoEncodeError .invalidEmptyValue(typ ))
108140
109- nonEmptyStringSet.orElse(nonEmptyNumberSet).orElse(nonEmptyBinarySet) match {
110- case Some ((typ, false )) => Either .leftNec(ScynamoEncodeError .invalidEmptyValue(typ))
111- case Some ((_, true )) | None => Right (value)
141+ if (value.hasSs) nonEmpty(ScynamoType .StringSet )
142+ else if (value.hasNs) nonEmpty(ScynamoType .NumberSet )
143+ else if (value.hasBs) nonEmpty(ScynamoType .BinarySet )
144+ else Right (value)
112145 }
113- }
114146
115- implicit def eitherScynamoErrorEncoder [A : ScynamoEncoder ]: ScynamoEncoder [EitherNec [ScynamoEncodeError , A ]] = {
116- case Left (value) => Left (value)
117- case Right (value) => ScynamoEncoder [A ].encode(value)
118- }
147+ implicit def eitherScynamoErrorEncoder [A ](implicit right : ScynamoEncoder [A ]): ScynamoEncoder [EitherNec [ScynamoEncodeError , A ]] =
148+ ScynamoEncoder .instance {
149+ case Left (errors) => Left (errors)
150+ case Right (value) => right.encode(value)
151+ }
119152
120153 implicit def fieldEncoder [K , V ](implicit V : Lazy [ScynamoEncoder [V ]]): ScynamoEncoder [FieldType [K , V ]] =
121- field => V .value.encode(field )
154+ ScynamoEncoder .instance( V .value.encode)
122155}
123156
124157trait ScynamoIterableEncoder extends LowestPrioAutoEncoder {
125158 def iterableEncoder [A : ScynamoEncoder ]: ScynamoEncoder [Iterable [A ]] =
126- value =>
127- value.toList.parTraverse(ScynamoEncoder [A ].encode).map(encodedValues => AttributeValue .builder().l(encodedValues.asJava).build())
159+ ScynamoEncoder .listEncoder[A ].contramap(_.toList)
128160}
129161
130162trait LowestPrioAutoEncoder {
@@ -136,41 +168,60 @@ trait LowestPrioAutoEncoder {
136168
137169trait ObjectScynamoEncoder [A ] extends ScynamoEncoder [A ] {
138170 def encodeMap (value : A ): EitherNec [ScynamoEncodeError , java.util.Map [String , AttributeValue ]]
139-
140171 override def encode (value : A ): EitherNec [ScynamoEncodeError , AttributeValue ] =
141- encodeMap(value).map(AttributeValue .builder() .m(_).build())
172+ encodeMap(value).map(AttributeValue .builder.m(_).build())
142173}
143174
144175object ObjectScynamoEncoder extends SemiautoDerivationEncoder {
145176 def apply [A ](implicit instance : ObjectScynamoEncoder [A ]): ObjectScynamoEncoder [A ] = instance
146177
147- implicit def mapEncoder [A ](implicit valueEncoder : ScynamoEncoder [A ]): ObjectScynamoEncoder [Map [String , A ]] =
148- value => {
149- value.toList
150- .parTraverse { case (k, v) => valueEncoder.encode(v).map(k -> _) }
151- .map {
152- _.foldLeft(new java.util.HashMap [String , AttributeValue ]()) { case (acc, (k, v)) =>
153- acc.put(k, v)
154- acc
155- }
178+ // SAM syntax generates anonymous classes because of non-abstract methods like `encode`.
179+ private [scynamo] def instance [A ](
180+ f : A => EitherNec [ScynamoEncodeError , java.util.Map [String , AttributeValue ]]
181+ ): ObjectScynamoEncoder [A ] = f(_)
182+
183+ implicit val catsInstances : Contravariant [ObjectScynamoEncoder ] = new Contravariant [ObjectScynamoEncoder ] {
184+ override def contramap [A , B ](fa : ObjectScynamoEncoder [A ])(f : B => A ) =
185+ instance(value => fa.encodeMap(f(value)))
186+ }
187+
188+ implicit def mapEncoder [A ](implicit value : ScynamoEncoder [A ]): ObjectScynamoEncoder [Map [String , A ]] =
189+ instance { kvs =>
190+ var allErrors = Chain .empty[ScynamoEncodeError ]
191+ val attrValues = new java.util.HashMap [String , AttributeValue ](kvs.size)
192+ kvs.foreachEntry { (k, v) =>
193+ value.encode(v) match {
194+ // Omit `nul` for efficiency and GSI support (see https://github.com/aws/aws-sdk-go/issues/1803)
195+ case Right (attr) => if (! attr.nul) attrValues.put(k, attr)
196+ case Left (errors) => allErrors ++= StackFrame .encoding(errors, MapKey (k)).toChain
156197 }
198+ }
199+
200+ NonEmptyChain .fromChain(allErrors).toLeft(Collections .unmodifiableMap(attrValues))
157201 }
158202}
159203
160204trait ScynamoKeyEncoder [A ] { self =>
161205 def encode (value : A ): EitherNec [ScynamoEncodeError , String ]
162-
163- def contramap [ B ]( f : B => A ) : ScynamoKeyEncoder [ B ] = value => self.encode(f(value))
206+ def contramap [ B ]( f : B => A ) : ScynamoKeyEncoder [ B ] =
207+ ScynamoKeyEncoder .instance( value => self.encode(f(value) ))
164208}
165209
166210object ScynamoKeyEncoder {
167211 def apply [A ](implicit encoder : ScynamoKeyEncoder [A ]): ScynamoKeyEncoder [A ] = encoder
168212
169- implicit val stringKeyEncoder : ScynamoKeyEncoder [String ] = value =>
170- if (value.nonEmpty)
171- Right (value)
172- else
173- Either .leftNec(ScynamoEncodeError .invalidEmptyValue(ScynamoType .String ))
213+ // SAM syntax generates anonymous classes because of non-abstract methods like `contramap`.
214+ private [scynamo] def instance [A ](f : A => EitherNec [ScynamoEncodeError , String ]): ScynamoKeyEncoder [A ] = f(_)
215+
216+ implicit val catsInstances : Contravariant [ScynamoKeyEncoder ] = new Contravariant [ScynamoKeyEncoder ] {
217+ override def contramap [A , B ](fa : ScynamoKeyEncoder [A ])(f : B => A ) = fa.contramap(f)
218+ }
219+
220+ implicit val stringKeyEncoder : ScynamoKeyEncoder [String ] = instance { value =>
221+ if (value.nonEmpty) Right (value)
222+ else Either .leftNec(ScynamoEncodeError .invalidEmptyValue(ScynamoType .String ))
223+ }
174224
175- implicit val uuidKeyEncoder : ScynamoKeyEncoder [UUID ] = ScynamoKeyEncoder [String ].contramap[UUID ](_.toString)
225+ implicit val uuidKeyEncoder : ScynamoKeyEncoder [UUID ] =
226+ ScynamoKeyEncoder [String ].contramap(_.toString)
176227}
0 commit comments