morphling ![Sonatype Releases Release Artifacts](https://camo.githubusercontent.com/a72c96d9086792671609d4c67b9695fe1417e987156938fddb33b6a759104386/68747470733a2f2f696d672e736869656c64732e696f2f6e657875732f722f68747470732f6f73732e736f6e61747970652e6f72672f636f6d2e6769746875622e64616e736c61706d616e2f6d6f7270686c696e675f322e31332e737667)
![Gitpod ready-to-code](https://camo.githubusercontent.com/daadb4894128d1e19b72d80236f5959f1f2b47f9fe081373f3246131f0189f6c/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f476974706f642d72656164792d2d746f2d2d636f64652d626c75653f6c6f676f3d676974706f64)
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 += "com.github.danslapman" %% "morphling" % "2.7.0"
Version compatibility table
morphling | cats | circe | reactivemongo | typed-schema | scalacheck | tofu |
---|---|---|---|---|---|---|
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 Int
s, BigInt
s, Instant
s, 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 tofu.optics.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