/calypso

BSON library for Scala

Primary LanguageScalaMIT LicenseMIT

calypso

build Maven Central

A BSON library based on org.bson. Encoder and Decoder type classes with instances for common types.

Supported types

Scala

Boolean
Int
Long
Double
String
Array[Byte]
(A, B)
List[A]
Set[A]
Map[K, V]
Option[A]
Either[A, B]
Unit

java

java.time.Instant
java.util.UUID

cats

cats.data.NonEmptyList

Refined

Codecs for refined types are derived, so if you have Encoder[A], then you have Encoder[A Refined P] (where P is a predicate) for free. The same for decoders, so having Decoder[A] in implicit scope automatically gives you Decoder[A Refined P].

Product type (case class)

It is possible to construct codecs for product types (case classes) using forProductN helper methods if you have codecs for each of its elements.

import ru.m2.calypso.syntax._

final case class Record(id: Int, name: String)
object Record {
  implicit val encodeRecord: Encoder[Record] =
    Encoder.forProduct2("id", "name") { r =>
      (r.id, r.name)
    }
  implicit val decodeRecord: Decoder[Record] =
    Decoder.forProduct2("id", "name")(Record.apply)
}

val bson: BsonValue = Record(1, "John").asBson // {"id": 1, "name": "John"}

val record: Either[String, Record] = bson.as[Record] // Right(Record(1,John))

Coproduct type (sealed trait hierarchy)

Coproduct is also known as ADT, sum, or tagged union. Not as ergonomic as product type, but it is possible to create codecs for coproduct types using forCoproductN helper methods.

sealed trait AorB
object AorB {
  final case class A(i: Int)    extends AorB
  final case class B(s: String) extends AorB
}

implicit val eqAorB: Eq[AorB] = Eq.fromUniversalEquals

implicit val encodeA: Encoder[A] = Encoder.forProduct1("i")(_.i)
implicit val encodeB: Encoder[B] = Encoder.forProduct1("s")(_.s)
implicit val encodeAorB: Encoder[AorB] = Encoder.forCoproduct {
  case a @ A(_) => "a" -> a.asBson
  case b @ B(_) => "b" -> b.asBson
}

implicit val decodeA: Decoder[A]       = Decoder.forProduct1("i")(A.apply)
implicit val decodeB: Decoder[B]       = Decoder.forProduct1("s")(B.apply)
implicit val decodeAorB: Decoder[AorB] = Decoder.forCoproduct2[AorB, A, B]("a", "b")

val aBson: BsonValue = (A(42): AorB).asBson      // {"tag": "a", "value": {"i": 42}}
val bBson: BsonValue = (B("hello"): AorB).asBson // {"tag": "b", "value": {"s": "hello"}}

val a: Either[String, AorB] = aBson.as[AorB] // Right(A(42))
val b: Either[String, AorB] = bBson.as[AorB] // Right(B(hello))

Derive codecs

Use existing codecs to derive complex ones.

final case class UserId(value: Long) extends AnyVal
object UserId {
  implicit val encodeUserId: Encoder[UserId] =
    Encoder.encodeLong.contramap(_.value)

  implicit val decodeUserId: Decoder[UserId] =
    Decoder.decodeLong.map(UserId.apply)
}

val bson: BsonValue = UserId(42).asBson // BsonInt64{value=42}

val userId: Either[String, UserId] = bson.as[UserId] // Right(UserId(42))

Why?

Passion for going with Java MongoDB driver in a type-safe manner.

  • MongoDB Scala Driver are just wrappers over org.bson without advantages.
  • Reactive Scala Driver for MongoDB can not be used without shenanigans with Java MongoDB driver, as well as it does not offer reasonable API to encode/decode case classes.
  • MongoLess, shapeless-reactivemongo, and Pure BSON are based on shapeless, so they are refactoring blind and not a safe way to express persistence schema.
  • circe-bson use JSON subset of BSON which is a no go for binary data

Design

Encoder[A]: A => org.bson.BsonValue
Decoder[A]: org.bson.BsonValue => Either[String, A]

KeyEncoder[A]: A => String
KeyDecoder[A]: String => Either[String, A]

This type classes allows to map Scala types to BSON and back. Key codecs are essential to preserving Map keys. Library is heavily inspired by circe and argonaut.

  • Map keys are encoded as strings
  • Tuple (A, B) is encoded as object {"_1": A, "_2": B}
  • String Refined Uuid is encoded as native binary BsonUuid
  • Instant is encoded as DateTime (epoch millis)

On optional values: object keys with null values and non-existing object keys are semantically equal.

Test

Calypso type classes come with laws. Encoder and Decoder instances should hold CodecLaws.

sbt test