A BSON library based on org.bson
. Encoder and Decoder type classes with instances for common types.
To use calypso in an existing sbt project, add the following dependencies to your build.sbt:
libraryDependencies += "ru.m2" %% "calypso-core" % "<version>"
The most current release can be found in the maven badge at the top of this readme.
Unit Boolean Int Long Double String Array[Byte] (A, B) List[A] Set[A] SortedSet[A] Map[K, V] SortedMap[K, V] Option[A] Either[A, B]
java.time.Instant java.util.UUIDProduct type (case class)
It is possible to construct codecs for product types (case classes) using forProductN
constructors if you have codecs for each of its elements.
import org.bson.BsonValue import ru.m2.calypso.syntax.* import ru.m2.calypso.{Decoder, Encoder} case class Record(id: Int, name: String) object Record: given Encoder[Record] = Encoder.forProduct2("id", "name")(r => (r.id, r.name)) given 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
constructors.
import org.bson.BsonValue import ru.m2.calypso.syntax.* import ru.m2.calypso.{Decoder, Encoder} enum AorB: case A(i: Int) case B(s: String) object AorB: import AorB.* given Encoder[A] = Encoder.forProduct1("i")(_.i) given Encoder[B] = Encoder.forProduct1("s")(_.s) given Encoder[AorB] = Encoder.forCoproduct { case a: A => "A" -> a.asBson case b: B => "B" -> b.asBson } given Decoder[A] = Decoder.forProduct1("i")(A.apply) given Decoder[B] = Decoder.forProduct1("s")(B.apply) given Decoder[AorB] = Decoder.forCoproduct2[AorB, A, B]("A", "B") object Example: import AorB.* 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))
Use existing codecs to derive new ones.
import org.bson.BsonValue import ru.m2.calypso.syntax.* import ru.m2.calypso.{Decoder, Encoder} import java.time.{Instant, LocalDateTime, ZoneOffset} given Encoder[LocalDateTime] = Encoder[Instant].contramap(_.toInstant(ZoneOffset.UTC)) given Decoder[LocalDateTime] = Decoder[Instant].map(LocalDateTime.ofInstant(_, ZoneOffset.UTC)) val bson: BsonValue = LocalDateTime.of(1970, 1, 1, 0, 0, 0).asBson // BsonDateTime{value=0} val time: Either[String, LocalDateTime] = bson.as[LocalDateTime] // Right(1970-01-01T00:00)
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]
.
To use calypso-refined in an existing sbt project, add the following dependencies to your build.sbt:
libraryDependencies += "ru.m2" %% "calypso-refined" % "<version>"
import eu.timepit.refined.types.string.NonEmptyString import ru.m2.calypso.refined.* import ru.m2.calypso.syntax.* NonEmptyString("Text").asBson // BsonString{value='Text'}
Passion for going with MongoDB Java Reactive Streams in a type-safe manner.
MongoDB Scala Driver
are wrappers around org.bson
without advantages.ReactiveMongo
is not compatible with MongoDB Java Reactive Streams, as well as it does not offer reasonable API to encode/decode case classes.MongoLess
, shapeless-reactivemongo
, Pure BSON
and medeia
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.opaque type Encoder[A] = A => org.bson.BsonValue opaque type Decoder[A] = org.bson.BsonValue => Either[String, A] opaque type KeyEncoder[A] = A => String opaque type 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.
On optional values: object keys with null values and non-existing object keys are semantically equal.
Calypso type classes come with laws. Encoder and Decoder instances should hold CodecLaws. Calypso uses discipline to define type class laws and the ScalaCheck tests based on them.
First, you will need to specify dependencies on calypso-testing
in your build.sbt
file.
libraryDependencies ++= List( "ru.m2" %% "calypso-testing" % "<version>" % Test, "org.typelevel" %% "discipline-munit" % "1.0.9" % Test )
We’ll begin by creating an Eq
instance for UserId data type, as laws will need to compare values.
import cats.Eq import ru.m2.calypso.{Decoder, Encoder} opaque type UserId = Long object UserId: def apply(value: Long): UserId = value given Encoder[UserId] = Encoder.given_Encoder_Long given Decoder[UserId] = Decoder.given_Decoder_Long given Eq[UserId] = Eq.fromUniversalEquals
ScalaCheck requires Arbitrary instances for data types being tested. The following example is for MUnit.
import munit.DisciplineSuite import org.scalacheck.{Arbitrary, Gen} import ru.m2.calypso.testing.CodecTests class CodecSuite extends DisciplineSuite: checkAll("Codec[UserId]", CodecTests[UserId].codec) given Arbitrary[UserId] = Arbitrary(Gen.long.map(UserId.apply))
Now when we run test in sbt console, ScalaCheck will test if the Codec laws hold for our UserId type. You should see something like this:
ru.m2.example.CodecSuite:
+ Codec[UserId]: codec.roundTrip 0.04s
[info] Passed: Total 1, Failed 0, Errors 0, Passed 1
You are gorgeous — the data type upholds the Codec laws!
RetroSearch is an open source project built by @garambo | Open a GitHub Issue
Search and Browse the WWW like it's 1997 | Search results from DuckDuckGo
HTML:
3.2
| Encoding:
UTF-8
| Version:
0.7.4