All Downloads are FREE. Search and download functionalities are using the official Maven repository.

morphling.SchemaF.scala Maven / Gradle / Ivy

The newest version!
package morphling

import cats.*
import cats.data.NonEmptyList
import cats.free.*
import glass.*

/**
 * The base trait for the schema GADT.
 *
 * @define PDefn
 *   The GADT type constructor for a sum type which defines the set of primitive types used in the schema.
 * @define IDefn
 *   The type of the Scala value to be produced (or consumed) by an interpreter of the schema. Also known as the "index"
 *   type of the schema.
 * @define FDefn
 *   The functor through which the structure of the schema will be interpreted. This will almost always be a fixpoint
 *   type such as [[morphling.HFix.HCofree]], which is used to introduce the ability to create recursive
 *   (tree-structured) schema.
 *
 * @tparam P
 *   $PDefn
 * @tparam F
 *   $FDefn
 * @tparam I
 *   $IDefn
 */
sealed trait SchemaF[P[_], F[_], I] {

  /**
   * HFunctor operation which allows transformation of the functor through which the structure of the schema will be
   * interpreted.
   *
   * Defining this operation directly on the SchemaF type rather than in [[morphling.SchemaF.schemaFHFunctor]]
   * simplifies type inference.
   */
  def hfmap[G[_]](nt: F ~> G): SchemaF[P, G, I]

  /**
   * HFunctor operation which allows transformation of the primitive algebra of the schema.
   *
   * Defining this operation directly on the SchemaF type rather than in [[morphling.SchemaF.schemaFHFunctor]]
   * simplifies type inference.
   */
  def pmap[Q[_]](nt: P ~> Q): SchemaF[Q, F, I]
}

object SchemaF {
  given [P[_]]: HFunctor[[Y[_], Z] =>> SchemaF[P, Y, Z]] = new HFunctor[[Y[_], Z] =>> SchemaF[P, Y, Z]] {
    override def hlift[M[_], N[_]](nt: M ~> N): SchemaF[P, M, _] ~> SchemaF[P, N, _] =
      new (SchemaF[P, M, _] ~> SchemaF[P, N, _]) {
        def apply[I](fa: SchemaF[P, M, I]): SchemaF[P, N, I] = fa.hfmap(nt)
      }
  }
}

/**
 * Schema constructor that wraps a value of an underlying GADT of allowed primitive types.
 *
 * The underlying GADT defines a set of types via GADT constructors; see [[morphling.protocol.SType]] for an example.
 * This set of types defines what types may be treated as primitive (and have parsing/ serialization/etc deferred to an
 * external handler) when interpreting a schema value. For example, one might want to construct a GADT for for the Scala
 * primitive types as such:
 *
 * {{{
 *  sealed trait SType[I]
 *
 *  case object SNullT   extends SType[Unit]
 *  case object SBoolT   extends SType[Boolean]
 *
 *  case object SByteT   extends SType[Byte]
 *  case object SShortT  extends SType[Short]
 *  case object SIntT    extends SType[Int]
 *  case object SLongT   extends SType[Long]
 *
 *  case object SFloatT  extends SType[Float]
 *  case object SDoubleT extends SType[Double]
 *
 *  case object SCharT   extends SType[Char]
 *  case object SStrT    extends SType[String]
 * }}}
 *
 * This example treats String values as primitive as well, even though strictly speaking they're reference types, just
 * because virtually any interpreter for a schema algebra will not want to represent strings in terms of sum or product
 * types. The same might hold true for, for example, [[scala.Array]] but for the purposes of this example issues related
 * to `ClassManifest` instances would introduce excessive complexity.
 *
 * @tparam P
 *   $PDefn
 * @tparam F
 *   $FDefn
 * @tparam I
 *   $IDefn
 * @param prim
 *   value identifying a primitive type.
 */
final case class PrimSchema[P[_], F[_], I](prim: P[I]) extends SchemaF[P, F, I] {
  def hfmap[G[_]](nt: F ~> G): PrimSchema[P, G, I] = PrimSchema[P, G, I](prim)
  def pmap[Q[_]](nt: P ~> Q): PrimSchema[Q, F, I]  = PrimSchema[Q, F, I](nt(prim))
}

/**
 * Constructor that enables creation of schema for sum types.
 *
 * Each constructor of the sum type `I` is represented as a member of the list of alternatives. Each alternative defines
 * a prism between a single constructor of the sum type, and an underlying type describing the arguments demanded by
 * that constructor.
 *
 * Consider the following sum type. The first constructor takes no arguments; the second takes two.
 *
 * {{{
 *  sealed trait Role
 *
 *  case object User extends Role
 *  case class Administrator(department: String, subordinateCount: Int) extends Role
 * }}}
 *
 * A schema value for this type looks like:
 *
 * {{{
 *  val roleSchema = oneOf(
 *    alt[Unit, Prim, Role, Unit](
 *      "user",
 *      Schema.empty,
 *      (_: Unit) => User,
 *      {
 *        case User => Some(Unit)
 *        case _ => None
 *      }
 *    ) ::
 *    alt[Unit, Prim, Role, Administrator](
 *      "administrator",
 *      rec[Prim, Administrator](
 *        (
 *          required("department", Prim.str, (_: Administrator).department),
 *          required("subordinateCount", Prim.int, (_: Administrator).subordinateCount)
 *        ).mapN(Administrator.apply)
 *      ),
 *      identity,
 *      {
 *        case a @ Administrator(_, _) => Some(a)
 *        case _ => None
 *      }
 *    ) :: Nil
 *  )
 * }}}
 *
 * @tparam P
 *   $PDefn
 * @tparam F
 *   $FDefn
 * @tparam I
 *   $IDefn
 */
final case class OneOfSchema[P[_], F[_], I](alts: NonEmptyList[Alt[F, I, ?]], discriminator: Option[String] = None)
    extends SchemaF[P, F, I] {
  def hfmap[G[_]](nt: F ~> G): OneOfSchema[P, G, I] = OneOfSchema[P, G, I](alts.map(_.hfmap(nt)), discriminator)
  def pmap[Q[_]](nt: P ~> Q): OneOfSchema[Q, F, I]  = OneOfSchema[Q, F, I](alts, discriminator)
}

/**
 * A prism between a base type containing the arguments required by a single constructor of a sum type, and that sum
 * type, along with the schema for the base type is used to describe those constructor arguments. The identifier is used
 * to distinguish which constructor is being represented in the serialized form.
 *
 * @define IDefn
 *   The type of the Scala value to be produced (or consumed) by an interpreter of the schema. Also known as the "index"
 *   type of the schema.
 *
 * @define FDefn
 *   The functor through which the structure of the schema will be interpreted. This will almost always be a fixpoint
 *   type such as [[morphling.HFix.HCofree]], which is used to introduce the ability to create recursive
 *   (tree-structured) schema.
 *
 * @tparam F
 *   $FDefn
 * @tparam I
 *   $IDefn
 * @tparam I0
 *   The base type which corresponds to the arguments to the selected constructor.
 * @param id
 *   The unique identifier of the constructor
 * @param base
 *   The schema for the `I0` type
 * @param subset
 *   Subset between the sum type and the selected constructor.
 */
final case class Alt[F[_], I, I0](id: String, base: F[I0], subset: Subset[I, I0]) {
  def hfmap[G[_]](nt: F ~> G): Alt[G, I, I0] = Alt(id, nt(base), subset)
}

/**
 * Wrapper for the free applicative structure which is used to construct and disassemble values of product types.
 *
 * @tparam P
 *   $PDefn
 * @tparam F
 *   $FDefn
 * @tparam I
 *   $IDefn
 * @param props
 *   the free applicative value composed of zero or more PropSchema instances
 */
final case class RecordSchema[P[_], F[_], I](props: FreeApplicative[PropSchema[I, F, *], I]) extends SchemaF[P, F, I] {
  def hfmap[G[_]](nt: F ~> G): RecordSchema[P, G, I] = RecordSchema[P, G, I](
    props.compile[PropSchema[I, G, *]](HFunctor[[Y[_], Z] =>> PropSchema[I, Y, Z]].hlift[F, G](nt))
  )
  def pmap[Q[_]](nt: P ~> Q): RecordSchema[Q, F, I] = RecordSchema[Q, F, I](props)
}

/**
 * Base trait for values which describe record properties.
 *
 * @define FDefn
 *   The functor through which the structure of the schema will be interpreted. This will almost always be a fixpoint
 *   type such as [[morphling.HFix.HCofree]], which is used to introduce the ability to create recursive
 *   (tree-structured) schema.
 *
 * @tparam O
 *   The record type.
 * @tparam F
 *   $FDefn
 * @tparam I
 *   The type of the property value.
 */
sealed trait PropSchema[O, F[_], I] {
  def fieldName: String
  def extract: Extract[O, I]

  def hfmap[G[_]](nt: F ~> G): PropSchema[O, G, I]
}

/**
 * Class describing a required property of a record.
 *
 * @param fieldName
 *   The name of the property.
 * @param base
 *   Schema for the property's value type.
 * @param extract
 *   Extract lens from the record type to the property.
 * @param default
 *   Optional default value, for use in the case that a serialized form is missing the property.
 */
final case class Required[O, F[_], I](
    fieldName: String,
    base: F[I],
    extract: Extract[O, I],
    default: Option[I]
) extends PropSchema[O, F, I] {
  def hfmap[G[_]](nt: F ~> G): PropSchema[O, G, I] =
    Required(fieldName, nt(base), extract, default)
}

/**
 * Class describing an optional property of a record. Since in many serialized forms optional properties may be omitted
 * entirely from the serialized form, a distinct type is needed in order to be able to correctly interpret the absence
 * of a field.
 *
 * @param fieldName
 *   The name of the property.
 * @param base
 *   Schema for the property's value type.
 * @param extract
 *   Extract lens from the record type to the property.
 */
final case class Optional[O, F[_], I](
    fieldName: String,
    base: F[I],
    extract: Extract[O, Option[I]]
) extends PropSchema[O, F, Option[I]] {
  def hfmap[G[_]](nt: F ~> G): PropSchema[O, G, Option[I]] =
    Optional(fieldName, nt(base), extract)
}

/**
 * Class describing an optional property of a record that is always absent.
 *
 * @param fieldName
 *   The name of the property.
 * @param extract
 *   Extract lens from the record type to the property.
 */
final case class Absent[O, F[_], I](
    fieldName: String,
    extract: Extract[O, Option[I]]
) extends PropSchema[O, F, Option[I]] {
  def hfmap[G[_]](nt: F ~> G): PropSchema[O, G, Option[I]] =
    Absent(fieldName, extract)
}

/**
 * Class describing a constant (non-serializable) property of a record.
 * @param fieldName
 *   The name of the property.
 * @param value
 *   The value of the property.
 * @param extract
 *   Extract lens from the record type to the property.
 */
final case class Constant[O, F[_], I](
    fieldName: String,
    value: I,
    extract: Extract[O, I]
) extends PropSchema[O, F, I] {
  override def hfmap[G[_]](nt: F ~> G): PropSchema[O, G, I] =
    this.asInstanceOf[PropSchema[O, G, I]]
}

object PropSchema {
  given [O]: HFunctor[[Y[_], Z] =>> PropSchema[O, Y, Z]] =
    new HFunctor[[Y[_], Z] =>> PropSchema[O, Y, Z]] {
      def hlift[M[_], N[_]](nt: M ~> N): PropSchema[O, M, _] ~> PropSchema[O, N, _] =
        new (PropSchema[O, M, _] ~> PropSchema[O, N, _]) {
          def apply[I](ps: PropSchema[O, M, I]): PropSchema[O, N, I] = ps.hfmap(nt)
        }
    }

  private def extract[A, B](f: A => B): Extract[A, B] = (s: A) => f(s)

  def contraNT[O, N, F[_]](f: N => O): PropSchema[O, F, _] ~> PropSchema[N, F, _] =
    new (PropSchema[O, F, _] ~> PropSchema[N, F, _]) {
      def apply[I](pso: PropSchema[O, F, I]): PropSchema[N, F, I] =
        pso match {
          case Required(n, s, g, d)              => Required(n, s, extract(f) >> g, d)
          case opt: Optional[O, F, i] @unchecked => Optional(opt.fieldName, opt.base, extract(f) >> opt.extract)
          case Constant(fn, v, g)                => Constant(fn, v, extract(f) >> g)
          case abs: Absent[O, F, i] @unchecked   => Absent(abs.fieldName, extract(f) >> abs.extract)
        }
    }
}

case class IsoSchema[P[_], F[_], I, J](base: F[I], eqv: Equivalent[I, J]) extends SchemaF[P, F, J] {
  def hfmap[G[_]](nt: F ~> G): IsoSchema[P, G, I, J] = IsoSchema(nt(base), eqv)
  def pmap[Q[_]](nt: P ~> Q): IsoSchema[Q, F, I, J]  = IsoSchema(base, eqv)
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy