danslapman / morphling   4.0.0

Do What The F*ck You Want To Public License GitHub

Cats-based Scala library for free applicative schemas (cats-based port of Xenomorph)

Scala versions: 3.x 2.13 2.12

morphling Release Artifacts

Cats-based Scala library for free applicative schemas. Core module of morphling initially was a cats-based port of the excellent Kris Nuttycombe's xenomorph

Getting started

All You need is love:

    libraryDependencies ++= Seq(
      "com.github.danslapman" %% "morphling"               % "4.0.0", //core module
      "com.github.danslapman" %% "morphling-circe"         % "4.0.0",
      "com.github.danslapman" %% "morphling-reactivemongo" % "4.0.0",
      "com.github.danslapman" %% "morphling-typed-schema"  % "4.0.0",
      "com.github.danslapman" %% "morphling-scalacheck"    % "4.0.0",
      "com.github.danslapman" %% "morphling-tapir"         % "4.0.0"
    )

Version compatibility table

morphling cats circe reactivemongo typed-schema scalacheck tofu glass tapir
4.0 2.8 0.14.2 1.0.3 / 1.1.0-RC6 0.14.3 1.15.3 - 0.3 1.0.0
3.1 2.8 0.14.2 1.0.3 / 1.1.0-RC6 0.14.3 1.15.3 - 0.1 1.0.0
3.0 2.8 0.14.2 1.0.3 / 1.1.0-RC6 0.14.3 1.15.3 - 0.1 -
2.7-glass 2.8 0.13.0 1.0.3 0.14.3 1.15.3 - 0.1 -
2.7 2.4.2 0.13.0 1.0.3 0.14.3 1.15.3 0.10.0 - -
2.6 2.4.2 0.12.3 0.19.3 0.12.5.1 1.14.3 0.7.9 - -
2.4 2.0.0 0.12.3 0.19.3 0.12.5.1 1.14.3 0.7.9 - -
2.1 2.0.0 0.12.3 0.19.3 0.12.4 1.14.3 0.7.4 - -
2.0 2.0.0 0.12.3 0.19.3 0.11.1 1.14.3 0.6.1 - -
1.5.1 2.0.0 0.12.3 0.19.3 0.11.1 1.14.0 - - -
1.5 2.0.0 0.12.3 0.19.0 0.11.0 1.14.0 - - -
1.1 2.0.0 0.11.1 0.17.0 0.11.0-beta6 1.14.0 - - -
1.0 1.6.1 0.11.1 0.16.4 0.11.0-beta6 1.14.0 - - -

Setting up protocol

First of all, You need to define a set of "scalar" types You like to support. They can be Ints, BigInts, Instants, any type You mean to treat as scalar, actually. You can find an example protocol in tests of core module:

import morphling.HMutu
import morphling.Schema._

sealed trait SType[F[_], I]

case class SNullT[F[_]]()   extends SType[F, Unit]
case class SBoolT[F[_]]()   extends SType[F, Boolean]

case class SIntT[F[_]]()    extends SType[F, Int]
case class SLongT[F[_]]()   extends SType[F, Long]

case class SFloatT[F[_]]()  extends SType[F, Float]
case class SDoubleT[F[_]]() extends SType[F, Double]

case class SCharT[F[_]]()   extends SType[F, Char]
case class SStrT[F[_]]()    extends SType[F, String]

case class SArrayT[F[_], I](elem: F[I]) extends SType[F, Vector[I]]

Also it will be convenient to define some helper methods (will see their purpose later):

object SType {
  type SSchema[I] = HMutu[SType, Schema, I]

  val sNull = prim(HMutu[SType, Schema, Unit](SNullT()))
  val sBool = prim(HMutu[SType, Schema, Boolean](SBoolT()))
  val sInt = prim(HMutu[SType, Schema, Int](SIntT()))
  val sLong = prim(HMutu[SType, Schema, Long](SLongT()))
  val sFloat = prim(HMutu[SType, Schema, Float](SFloatT()))
  val sDouble = prim(HMutu[SType, Schema, Double](SDoubleT()))
  val sChar = prim(HMutu[SType, Schema, Char](SCharT()))
  val sStr = prim(HMutu[SType, Schema, String](SStrT()))

  def sArray[I](elem: Schema[SSchema, I]) = prim(HMutu[SType, Schema, Vector[I]](SArrayT(elem)))
}

Creating a Schema

Now we can define a schema for an arbitrary type using our protocol:

import cats.syntax.apply._
import morphling.Schema
import morphling.Schema._
import glass.macros._
import SType._ //Defined above

case @Optics class Server(host: String, port: Int)
object Server {
  val serverSchema: Schema[SSchema, Server] = rec(
    (
      required("host", sStr, Server.host),
      required("port", sInt, Server.port)
    ).mapN(Server.apply)
  )
}

That's it.

Generating instances

morphling provides a set of modules which enables generation of typeclasses from schema instances. To use them You need to define an "implementation" of protocol You previously defined. Let's do it for circe Encoding:

import cats._
import io.circe.{Decoder, Encoder, Json}
import SType.SSchema
import morphling.Schema.Schema

def sTypeEncoder[F[_]: ToJson]: (SType[F, *] ~> Encoder) =
    new (SType[F, *] ~> Encoder) {
      import ToJson._
    
      override def apply[A](st: SType[F, A]): Encoder[A] = st match {
        case SNullT() => Encoder.encodeUnit
        case SBoolT() => Encoder.encodeBoolean
        case SIntT() => Encoder.encodeInt
        case SLongT() => Encoder.encodeLong
        case SFloatT() => Encoder.encodeFloat
        case SDoubleT() => Encoder.encodeDouble
        case SCharT() => Encoder.encodeChar
        case SStrT() => Encoder.encodeString
        case SArrayT(elem) => Encoder.encodeVector(elem.encoder)
      }
    }

implicit val primFromJson: FromJson[SSchema] = new FromJson[SSchema] {
    val decoder = new (SSchema ~> Decoder) {
      def apply[I](s: SSchema[I]): Decoder[I] = sTypeDecoder[SSchema[I]#Inner].apply(s.unmutu)
    }

With such a transformation defined we can derive an Encoder for Server:

val encoder = Server.schema.encoder