CLEλR-C0DE

How to create a MongoDb Codec for a val type

import org.bson.codecs._
import org.bson.{BsonReader, BsonType, BsonWriter}
import shapeless._
import shapeless.ops.hlist.IsHCons

import java.util.Date
import scala.reflect.ClassTag

trait ValueWrapperMongoCodecCompanion[A] extends DefaultMongoCodecs {
  implicit def mongoCodec[Repr <: HList, Head](
      implicit ct: ClassTag[A],
      gen:         Generic.Aux[A, Repr],
      isHCons:     IsHCons.Aux[Repr, Head, HNil],
      codec:       Codec[Head]
  ): Codec[A] =
    ValueWrapperMongoCodec.valueWrapperMongoCodec
}

object ValueWrapperMongoCodec {
  implicit def valueWrapperMongoCodec[A, Repr <: HList, Head](
      implicit ct: ClassTag[A],
      gen:         Generic.Aux[A, Repr],
      isHCons:     IsHCons.Aux[Repr, Head, HNil],
      codec:       Codec[Head]
  ): Codec[A] =
    new Codec[A] {
      override def decode(reader: BsonReader, decoderContext: DecoderContext): A =
        gen.from(isHCons.cons(codec.decode(reader, decoderContext), HNil))

      override def encode(writer: BsonWriter, value: A, encoderContext: EncoderContext): Unit =
        codec.encode(writer, gen.to(value).head, encoderContext)

      override def getEncoderClass: Class[A] =
        ct.runtimeClass.asInstanceOf[Class[A]]
    }
}

trait DefaultMongoCodecs {
  implicit val booleanMongoCodec: Codec[Boolean] = imap[java.lang.Boolean, Boolean](new BooleanCodec(), Boolean.unbox, Boolean.box)
  implicit val intMongoCodec:     Codec[Int]     = imap[Integer, Int](new IntegerCodec(), Int.unbox, Int.box)
  implicit val longMongoCodec:    Codec[Long]    = imap[java.lang.Long, Long](new LongCodec(), Long.unbox, Long.box)
  implicit val floatMongoCodec:   Codec[Float]   = imap[java.lang.Float, Float](new FloatCodec(), Float.unbox, Float.box)
  implicit val doubleMongoCodec:  Codec[Double]  = imap[java.lang.Double, Double](new DoubleCodec(), Double.unbox, Double.box)
  implicit val stringCodec:       Codec[String]  = new StringCodec()
  implicit val dateMongoCodec:    Codec[Date]    = new DateCodec()

  def imap[A, B](codec: Codec[A], f: A => B, g: B => A)(implicit ct: ClassTag[B]): Codec[B] =
    new Codec[B] {
      override def getEncoderClass: Class[B] =
        ct.runtimeClass.asInstanceOf[Class[B]]

      override def decode(reader: BsonReader, decoderContext: DecoderContext): B =
        f(codec.decode(reader, decoderContext))

      override def encode(writer: BsonWriter, value: B, encoderContext: EncoderContext): Unit =
        codec.encode(writer, g(value), encoderContext)
    }
}

object DefaultMongoCodecs extends DefaultMongoCodecs

Example

given:

case class Email(email: String) extends AnyVal

we can declare

object Email extends ValueWrapperMongoCodecCompanion[Email] {
  implicit val codec: Codec[Email] = mongoCodec
}

or

object Email {
  import DefaultMongoCodecs._
  implicit val codec: Codec[Email] = ValueWrapperMongoCodec.valueWrapperMongoCodec
}