Skip to content

Commit b21609f

Browse files
authored
Merge pull request #403 from AVSystem/mongo-typed-map
Reworked TypedMap and added support for it in typed Mongo API
2 parents ec881e9 + 405560d commit b21609f

File tree

13 files changed

+188
-48
lines changed

13 files changed

+188
-48
lines changed

commons-core/src/main/scala/com/avsystem/commons/misc/SealedUtils.scala

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,13 @@ trait AutoNamedEnum extends NamedEnum with Product {
9494
def name: String = productPrefix
9595
}
9696

97+
/**
98+
* Like [[AutoNamedEnum]] but derived names are uncapitalized (first letter lowercased).
99+
*/
100+
trait LowerCaseAutoNamedEnum extends AutoNamedEnum {
101+
override def name: String = super.name.uncapitalize
102+
}
103+
97104
/**
98105
* Base trait for companion objects of sealed traits that serve as named enums. `NamedEnumCompanion` is an
99106
* extension of [[SealedEnumCompanion]] which additionally requires that every enum value has distinct string
Lines changed: 85 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,74 @@
1-
package com.avsystem.commons
2-
package misc
1+
package com.avsystem.commons.misc
32

3+
import com.avsystem.commons.SharedExtensions._
44
import com.avsystem.commons.misc.TypedMap.GenCodecMapping
5-
import com.avsystem.commons.serialization.{GenCodec, GenKeyCodec, ObjectInput, ObjectOutput}
5+
import com.avsystem.commons.serialization._
66

7+
/**
8+
* A map whose keys are parameterized with value type.
9+
* This makes it possible to associate different value type with each key, in a type-safe way.
10+
*
11+
* [[TypedMap[K]]] has a [[GenCodec]] instance as long as there is a `GenCodec[K[_]]` instance for the key
12+
* type and a [[GenCodecMapping[K]]] instance that determines the codec for the value type associated with given
13+
* key.
14+
*
15+
* Example:
16+
* {{{
17+
* sealed abstract class AttributeKey[T](implicit val valueCodec: GenCodec[T])
18+
* extends TypedKey[T] with AutoNamedEnum
19+
*
20+
* object AttributeKey extends NamedEnumCompanion[AttributeKey[_]] {
21+
* object StringKey extends AttributeKey[String]
22+
* object IntKey extends AttributeKey[Int]
23+
*
24+
* val values: List[AttributeKey[_]] = caseObjects
25+
* }
26+
*
27+
* val attributes = TypedMap[AttributeKey](
28+
* AttributeKey.StringKey -> "foo",
29+
* AttributeKey.IntKey -> 42,
30+
* )
31+
* }}}
32+
*
33+
* Note that since all keys and value types are known statically,
34+
* the map above is somewhat equivalent to a case class:
35+
*
36+
* {{{
37+
* case class Attributes(
38+
* string: Opt[String],
39+
* int: Opt[Int]
40+
* )
41+
* }}}
42+
*
43+
* [[TypedMap]] might be a good choice if there is a lot of attribute keys, they aren't statically known or some
44+
* collection-like behaviour is necessary (e.g. computing the size, iterating over all elements). A [[TypedMap]]
45+
* is also easier to evolve than a case class (e.g. because of binary compatibility issues).
46+
*/
747
class TypedMap[K[_]](val raw: Map[K[_], Any]) extends AnyVal {
8-
def apply[T](key: K[T]): Option[T] =
9-
raw.get(key).map(_.asInstanceOf[T])
48+
def apply[T](key: K[T]): T =
49+
raw(key).asInstanceOf[T]
50+
51+
def get[T](key: K[T]): Option[T] =
52+
raw.get(key).asInstanceOf[Option[T]]
53+
54+
def getOpt[T](key: K[T]): Opt[T] =
55+
raw.getOpt(key).asInstanceOf[Opt[T]]
56+
57+
def getOrElse[T](key: K[T], defaultValue: => T): T =
58+
get(key).getOrElse(defaultValue)
1059

1160
def updated[T](key: K[T], value: T): TypedMap[K] =
1261
new TypedMap[K](raw.updated(key, value))
1362

63+
def ++(other: TypedMap[K]): TypedMap[K] =
64+
new TypedMap[K](raw ++ other.raw)
65+
66+
def keys: Iterable[K[_]] = raw.keys
67+
def keysIterator: Iterator[K[_]] = raw.keysIterator
68+
def keySet: Set[K[_]] = raw.keySet
69+
70+
def size: Int = raw.size
71+
1472
override def toString = s"TypedMap($raw)"
1573
}
1674

@@ -33,39 +91,46 @@ object TypedMap {
3391
def valueCodec[T](key: K[T]): GenCodec[T]
3492
}
3593

36-
implicit def typedMapCodec[K[_]](implicit keyCodec: GenKeyCodec[K[_]], codecMapping: GenCodecMapping[K]): GenCodec[TypedMap[K]] =
94+
implicit def typedMapCodec[K[_]](implicit
95+
keyCodec: GenKeyCodec[K[_]],
96+
codecMapping: GenCodecMapping[K]
97+
): GenObjectCodec[TypedMap[K]] =
3798
new GenCodec.ObjectCodec[TypedMap[K]] {
3899
def nullable = false
39100
def readObject(input: ObjectInput): TypedMap[K] = {
40101
val rawBuilder = Map.newBuilder[K[_], Any]
102+
input.knownSize match {
103+
case -1 =>
104+
case size => rawBuilder.sizeHint(size)
105+
}
41106
while (input.hasNext) {
42107
val fieldInput = input.nextField()
43108
val key = keyCodec.read(fieldInput.fieldName)
44109
rawBuilder += ((key, codecMapping.valueCodec(key).read(fieldInput)))
45110
}
46111
new TypedMap[K](rawBuilder.result())
47112
}
48-
def writeObject(output: ObjectOutput, typedMap: TypedMap[K]): Unit =
113+
def writeObject(output: ObjectOutput, typedMap: TypedMap[K]): Unit = {
114+
output.declareSizeOf(typedMap.raw)
49115
typedMap.raw.foreach { case (key, value) =>
50-
codecMapping.valueCodec(key.asInstanceOf[K[Any]]).write(output.writeField(keyCodec.write(key)), value)
116+
val valueCodec = codecMapping.valueCodec(key.asInstanceOf[K[Any]])
117+
valueCodec.write(output.writeField(keyCodec.write(key)), value)
51118
}
119+
}
52120
}
53121
}
54122

55123
/**
56-
* Base class for sealed enums which can be used as key type for a [[TypedMap]]. It also ensures that
57-
* the [[TypedMap]] using [[TypedKey]] as a key type will always have a `GenCodec`.
124+
* Base class for key types of [[TypedMap]] (typically enums parameterized by value type).
125+
* Provides an instance of [[GenCodecMapping]] which is necessary for [[GenCodec]] instance for [[TypedMap]] that
126+
* uses this key type.
58127
*/
59-
abstract class TypedKey[T](implicit val valueCodec: GenCodec[T])
60-
trait TypedKeyCompanion[K[X] <: TypedKey[X]] extends SealedEnumCompanion[K[_]] {
61-
/**
62-
* `GenKeyCodec` for typed key `K`.
63-
* You can implement this with `GenKeyCodec.forSealedEnum` macro
64-
*/
65-
implicit def keyCodec: GenKeyCodec[K[_]]
66-
67-
implicit val codecMapping: GenCodecMapping[K] =
128+
trait TypedKey[T] {
129+
def valueCodec: GenCodec[T]
130+
}
131+
object TypedKey {
132+
implicit def codecMapping[K[X] <: TypedKey[X]]: GenCodecMapping[K] =
68133
new GenCodecMapping[K] {
69-
def valueCodec[T](key: K[T]) = key.valueCodec
134+
def valueCodec[T](key: K[T]): GenCodec[T] = key.valueCodec
70135
}
71136
}

commons-core/src/main/scala/com/avsystem/commons/serialization/GenCodec.scala

Lines changed: 2 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -434,21 +434,9 @@ object GenCodec extends RecursiveAutoCodecs with TupleGenCodecs {
434434
implicit lazy val BytesCodec: GenCodec[Bytes] =
435435
GenCodec.nullableSimple(i => Bytes(i.readBinary()), (o, b) => o.writeBinary(b.bytes))
436436

437-
private def declareSize(o: SequentialOutput, coll: BIterable[_]): Unit =
438-
o.sizePolicy match {
439-
case SizePolicy.Ignored =>
440-
case SizePolicy.Optional =>
441-
coll.knownSize match {
442-
case -1 =>
443-
case size => o.declareSize(size)
444-
}
445-
case SizePolicy.Required =>
446-
o.declareSize(coll.size)
447-
}
448-
449437
private implicit class IterableOps[A](private val coll: BIterable[A]) extends AnyVal {
450438
def writeToList(lo: ListOutput)(implicit writer: GenCodec[A]): Unit = {
451-
declareSize(lo, coll)
439+
lo.declareSizeOf(coll)
452440
coll.foreach(new (A => Unit) {
453441
private var idx = 0
454442
def apply(a: A): Unit = {
@@ -463,7 +451,7 @@ object GenCodec extends RecursiveAutoCodecs with TupleGenCodecs {
463451

464452
private implicit class PairIterableOps[A, B](private val coll: BIterable[(A, B)]) extends AnyVal {
465453
def writeToObject(oo: ObjectOutput)(implicit keyWriter: GenKeyCodec[A], writer: GenCodec[B]): Unit = {
466-
declareSize(oo, coll)
454+
oo.declareSizeOf(coll)
467455
coll.foreach { case (key, value) =>
468456
val fieldName = keyWriter.write(key)
469457
try writer.write(oo.writeField(fieldName), value) catch {

commons-core/src/main/scala/com/avsystem/commons/serialization/InputOutput.scala

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,21 @@ trait SequentialOutput extends Any with AcceptsCustomEvents {
176176
* have been written. This method MUST always be called after list/object writing has been finished.
177177
*/
178178
def finish(): Unit
179+
180+
/**
181+
* Based on given collection's `knownSize` and [[sizePolicy]], declares the size
182+
* of this output as size of this collection if it is either cheap or required to do so.
183+
*/
184+
final def declareSizeOf(coll: BIterable[_]): Unit = sizePolicy match {
185+
case SizePolicy.Ignored =>
186+
case SizePolicy.Optional =>
187+
coll.knownSize match {
188+
case -1 =>
189+
case size => declareSize(size)
190+
}
191+
case SizePolicy.Required =>
192+
declareSize(coll.size)
193+
}
179194
}
180195
/**
181196
* Represents an abstract sink for serialization of sequences of values. Any [[ListOutput]] instance

commons-core/src/test/scala/com/avsystem/commons/serialization/CodecTestData.scala

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,8 @@ package com.avsystem.commons
22
package serialization
33

44
import com.avsystem.commons.annotation.AnnotationAggregate
5-
import com.avsystem.commons.meta.MacroInstances
6-
import com.avsystem.commons.meta.AutoOptionalParams
7-
import com.avsystem.commons.misc.{TypedKey, TypedKeyCompanion}
5+
import com.avsystem.commons.meta.{AutoOptionalParams, MacroInstances}
6+
import com.avsystem.commons.misc.{AutoNamedEnum, NamedEnumCompanion, TypedKey}
87

98
object CodecTestData {
109
def col[T <: JCollection[Int]](col: T): T = {
@@ -300,15 +299,13 @@ object CodecTestData {
300299
implicit val codec: GenCodec[KeyEnumz] = GenCodec.forSealedEnum[KeyEnumz]
301300
}
302301

303-
sealed abstract class SealedKey[T: GenCodec] extends TypedKey[T]
304-
object SealedKey extends TypedKeyCompanion[SealedKey] {
305-
@name("StrKey")
302+
sealed abstract class SealedKey[T](implicit val valueCodec: GenCodec[T]) extends TypedKey[T] with AutoNamedEnum
303+
object SealedKey extends NamedEnumCompanion[SealedKey[_]] {
306304
case object StringKey extends SealedKey[String]
307305
case object IntKey extends SealedKey[Int]
308306
case object BooleanKey extends SealedKey[Boolean]
309307

310308
val values: List[SealedKey[_]] = caseObjects
311-
implicit def keyCodec: GenKeyCodec[SealedKey[_]] = GenKeyCodec.forSealedEnum[SealedKey[_]]
312309
}
313310

314311
@flatten("kejs") sealed trait CustomizedSeal

commons-core/src/test/scala/com/avsystem/commons/serialization/SimpleGenCodecTest.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -322,7 +322,7 @@ class SimpleGenCodecTest extends SimpleIOCodecTest {
322322

323323
testWrite(
324324
TypedMap(StringKey -> "lol", IntKey -> 42, BooleanKey -> true),
325-
Map[String, Any]("StrKey" -> "lol", "IntKey" -> 42, "BooleanKey" -> true)
325+
Map[String, Any]("StringKey" -> "lol", "IntKey" -> 42, "BooleanKey" -> true)
326326
)
327327
}
328328

commons-macros/src/main/scala/com/avsystem/commons/macros/serialization/MongoMacros.scala

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ class MongoMacros(ctx: blackbox.Context) extends CodecMacroCommons(ctx) {
1212
lazy val SeqApplySym: Symbol = typeOf[scala.collection.Seq[Any]].member(TermName("apply"))
1313
lazy val SeqHeadRef: Symbol = typeOf[scala.collection.Seq[Any]].member(TermName("head"))
1414
lazy val MapApplySym: Symbol = typeOf[scala.collection.Map[Any, Any]].member(TermName("apply"))
15+
lazy val TypedMapApplySym: Symbol = getType(tq"$MiscPkg.TypedMap[$ScalaPkg.Any]").member(TermName("apply"))
1516
lazy val AdtAsSym: Symbol = getType(tq"$MongoTypedPkg.AbstractMongoDataCompanion[Any, Any]#macroDslExtensions").member(TermName("as"))
1617

1718
// check if some symbol is an abstract method of a sealed trait/class implemented in every case class
@@ -74,6 +75,11 @@ class MongoMacros(ctx: blackbox.Context) extends CodecMacroCommons(ctx) {
7475

7576
q"${extractRefStep(prefix)}.apply($argument)"
7677

78+
case Apply(TypeApply(Select(prefix, TermName("apply")), List(valueTpeTree)), List(argument))
79+
if overridesAnyOf(body.symbol, TypedMapApplySym) =>
80+
81+
q"${extractRefStep(prefix)}.apply[$valueTpeTree]($argument)"
82+
7783
case _ =>
7884
invalid(body)
7985
}

commons-mongo/jvm/src/main/scala/com/avsystem/commons/mongo/typed/MongoFormat.scala

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ package mongo.typed
33

44
import com.avsystem.commons.annotation.positioned
55
import com.avsystem.commons.meta._
6-
import com.avsystem.commons.misc.ValueOf
6+
import com.avsystem.commons.misc.{TypedMap, ValueOf}
77
import com.avsystem.commons.mongo.{BsonValueInput, BsonValueOutput}
88
import com.avsystem.commons.serialization._
99
import org.bson.{BsonNull, BsonValue}
@@ -61,6 +61,12 @@ object MongoFormat extends MetadataCompanion[MongoFormat] with MongoFormatLowPri
6161
valueFormat: MongoFormat[V]
6262
) extends MongoFormat[M[K, V]]
6363

64+
final case class TypedMapFormat[K[_]](
65+
codec: GenCodec[TypedMap[K]],
66+
keyCodec: GenKeyCodec[K[_]],
67+
valueFormats: MongoFormatMapping[K]
68+
) extends MongoFormat[TypedMap[K]]
69+
6470
final case class OptionalFormat[O, T](
6571
codec: GenCodec[O],
6672
optionLike: OptionLike.Aux[O, T],
@@ -75,6 +81,10 @@ object MongoFormat extends MetadataCompanion[MongoFormat] with MongoFormatLowPri
7581
implicit mapCodec: GenCodec[M[K, V]], keyCodec: GenKeyCodec[K], valueFormat: MongoFormat[V]
7682
): MongoFormat[M[K, V]] = DictionaryFormat(mapCodec, keyCodec, valueFormat)
7783

84+
implicit def typedMapFormat[K[_]](
85+
implicit keyCodec: GenKeyCodec[K[_]], valueFormats: MongoFormatMapping[K]
86+
): MongoFormat[TypedMap[K]] = TypedMapFormat[K](TypedMap.typedMapCodec, keyCodec, valueFormats)
87+
7888
implicit def optionalFormat[O, T](
7989
implicit optionLike: OptionLike.Aux[O, T], optionCodec: GenCodec[O], wrappedFormat: MongoFormat[T]
8090
): MongoFormat[O] = OptionalFormat(optionCodec, optionLike, wrappedFormat)
@@ -96,6 +106,15 @@ object MongoFormat extends MetadataCompanion[MongoFormat] with MongoFormatLowPri
96106
"do you have a custom implicit MongoFormat for that type?")
97107
}
98108
}
109+
110+
implicit class typedMapFormatOps[K[_]](private val format: MongoFormat[TypedMap[K]]) extends AnyVal {
111+
def assumeTypedMap: TypedMapFormat[K] = format match {
112+
case typedMap: TypedMapFormat[K] => typedMap
113+
case _ => throw new IllegalArgumentException(
114+
"Encountered a non-typed-map MongoFormat for a TypedMap type - " +
115+
"do you have a custom implicit MongoFormat for that type?")
116+
}
117+
}
99118
}
100119
trait MongoFormatLowPriority { this: MongoFormat.type =>
101120
implicit def leafFormat[T: GenCodec]: MongoFormat[T] = Opaque(GenCodec[T])

commons-mongo/jvm/src/main/scala/com/avsystem/commons/mongo/typed/MongoRef.scala

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package mongo.typed
33

44
import com.avsystem.commons.annotation.macroPrivate
55
import com.avsystem.commons.meta.OptionLike
6+
import com.avsystem.commons.misc.TypedMap
67
import com.avsystem.commons.mongo.typed.MongoPropertyRef.Separator
78
import com.avsystem.commons.mongo.{BsonValueInput, KeyEscaper}
89
import com.avsystem.commons.serialization.GenCodec.ReadFailure
@@ -257,6 +258,13 @@ object MongoPropertyRef {
257258
}
258259
}
259260

261+
implicit class TypedMapRefOps[E, K[_]](private val ref: MongoPropertyRef[E, TypedMap[K]]) extends AnyVal {
262+
def apply[T](key: K[T]): MongoPropertyRef[E, T] = {
263+
val tmFormat = ref.format.assumeTypedMap
264+
MongoRef.FieldRef(ref, tmFormat.keyCodec.write(key), tmFormat.valueFormats.valueFormat(key), Opt.Empty)
265+
}
266+
}
267+
260268
implicit def optionalRefOps[E, O, T](ref: MongoPropertyRef[E, O])(implicit optionLike: OptionLike.Aux[O, T]): OptionalRefOps[E, O, T] =
261269
new OptionalRefOps[E, O, T](ref)
262270

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package com.avsystem.commons
2+
package mongo.typed
3+
4+
import com.avsystem.commons.misc.TypedKey
5+
import com.avsystem.commons.misc.TypedMap.GenCodecMapping
6+
import com.avsystem.commons.serialization.GenCodec
7+
8+
trait MongoFormatMapping[K[_]] extends GenCodecMapping[K] {
9+
def valueFormat[T](key: K[T]): MongoFormat[T]
10+
override def valueCodec[T](key: K[T]): GenCodec[T] = valueFormat(key).codec
11+
}
12+
13+
trait MongoTypedKey[T] extends TypedKey[T] {
14+
def valueFormat: MongoFormat[T]
15+
override def valueCodec: GenCodec[T] = valueFormat.codec
16+
}
17+
object MongoTypedKey {
18+
implicit def mongoFormatMapping[K[X] <: MongoTypedKey[X]]: MongoFormatMapping[K] =
19+
new MongoFormatMapping[K] {
20+
override def valueFormat[T](key: K[T]): MongoFormat[T] = key.valueFormat
21+
}
22+
}

0 commit comments

Comments
 (0)