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

io.hireproof.structure.dsl.scala Maven / Gradle / Ivy

The newest version!
package io.hireproof.structure

import cats.Order
import cats.data.{Chain, NonEmptyChain, NonEmptyList, NonEmptyMap}
import cats.syntax.all._
import io.circe.Json
import io.circe.syntax._
import io.hireproof.screening.validations._
import io.hireproof.screening.{Constraint, Selection, Validation, Violation, Violations}
import org.typelevel.ci.CIString

import java.util.UUID
import scala.collection.immutable.SortedMap

object dsl {
  val bigInt: Schema.Primitive[BigInt] = Schema.Primitive.bigInt
  val bigDecimal: Schema.Primitive[BigDecimal] = Schema.Primitive.bigDecimal
  val boolean: Schema.Primitive[Boolean] = Schema.Primitive.boolean
  val double: Schema.Primitive[Double] = Schema.Primitive.double
  val int: Schema.Primitive[Int] = Schema.Primitive.int
  val float: Schema.Primitive[Float] = Schema.Primitive.float
  val long: Schema.Primitive[Long] = Schema.Primitive.long
  val string: Schema.Primitive[String] = Schema.Primitive.string
  val uuid: Schema.Primitive[UUID] = string.ivalidate(parsing.uuid)(_.toString).withFormat("uuid")

  def optional[A](schema: => Schema[A]): Schema.Optional[Option[A]] = Schema.Optional(schema)
  def optional[A](header: Header[A]): Header[Option[A]] = header.mapSchema(optional[A](_))
  def optional[A](query: Query[A]): Query[Option[A]] = query.mapSchema(optional[A](_))

  object collection {
    def seq[A](schema: => Schema[A]): Schema.Collection[Seq[A]] = Schema.Collection.seq(schema)
    def list[A](schema: => Schema[A]): Schema.Collection[List[A]] = seq(schema).imap(_.toList)(_.toSeq)
    def chain[A](schema: => Schema[A]): Schema.Collection[Chain[A]] = seq(schema).imap(Chain.fromSeq)(_.toList)
    def vector[A](schema: => Schema[A]): Schema.Collection[Vector[A]] = seq(schema).imap(_.toVector)(_.toSeq)
    def set[A](schema: => Schema[A]): Schema.Collection[Set[A]] = seq(schema).imap(_.toSet)(_.toSeq)
    def map[A, B](schema: => Schema[(A, B)]): Schema.Collection[Map[A, B]] = seq(schema).imap(_.toMap)(_.toSeq)
    final def sortedMap[A, B](schema: => Schema[(A, B)])(implicit order: Order[A]): Schema.Collection[SortedMap[A, B]] =
      map(schema).imap(SortedMap.from(_)(order.toOrdering))(_.toMap)
    final def nonEmptyMap[A, B](
        schema: => Schema[(A, B)]
    )(implicit order: Order[A]): Schema.Collection[NonEmptyMap[A, B]] =
      sortedMap(schema).ivalidate {
        Validation.fromOptionNel[SortedMap[A, B], NonEmptyMap[A, B]](
          Constraint.number.greaterThan(reference = 1)
        )(NonEmptyMap.fromMap(_))(_.toList.mapFilter(schema.toJson).asJson)
      }(_.toSortedMap)
    def seq[A](header: Header[A]): Header[Seq[A]] = header.mapSchema(seq[A](_))
    def list[A](header: Header[A]): Header[List[A]] = header.mapSchema(list[A](_))
    def chain[A](header: Header[A]): Header[Chain[A]] = header.mapSchema(chain[A](_))
    def vector[A](header: Header[A]): Header[Vector[A]] = header.mapSchema(vector[A](_))
    def set[A](header: Header[A]): Header[Set[A]] = header.mapSchema(set[A](_))
    def seq[A](query: Query[A]): Query[Seq[A]] = query.mapSchema(seq[A](_))
    def list[A](query: Query[A]): Query[List[A]] = query.mapSchema(list[A](_))
    def chain[A](query: Query[A]): Query[Chain[A]] = query.mapSchema(chain[A](_))
    def vector[A](query: Query[A]): Query[Vector[A]] = query.mapSchema(vector[A](_))
    def set[A](query: Query[A]): Query[Set[A]] = query.mapSchema(set[A](_))

    final def nonEmptyChain[A](schema: => Schema[A]): Schema.Collection[NonEmptyChain[A]] =
      chain(schema).ivalidate {
        Validation
          .fromOptionNel[Chain[A], NonEmptyChain[A]](Constraint.number.greaterThan(reference = 1))(
            NonEmptyChain.fromChain
          )(_.mapFilter(schema.toJson).asJson)
      }(_.toChain)
    final def nonEmptyList[A](schema: => Schema[A]): Schema.Collection[NonEmptyList[A]] =
      list(schema).ivalidate {
        Validation
          .fromOptionNel[List[A], NonEmptyList[A]](Constraint.number.greaterThan(reference = 1))(
            NonEmptyList.fromList
          )(_.mapFilter(schema.toJson).asJson)
      }(_.toList)
  }

  object dictionary {
    final def map[A](schema: => Schema[A]): Schema.Dictionary[Map[String, A]] = Schema.Dictionary.map(schema)
    final def sortedMap[A](schema: => Schema[A]): Schema.Dictionary[SortedMap[String, A]] =
      map(schema).imap(SortedMap.from(_))(_.toMap)
    final def nonEmptyMap[A](schema: => Schema[A]): Schema.Dictionary[NonEmptyMap[String, A]] =
      sortedMap(schema).ivalidate {
        Validation.fromOptionNel[SortedMap[String, A], NonEmptyMap[String, A]](
          Constraint.number.greaterThan(reference = 1)
        )(NonEmptyMap.fromMap(_))(_.mapFilter(schema.toJson).asJson)
      }(_.toSortedMap)
  }

  val empty: Schema.Product[Unit] = Schema.Product.Empty

  def const[A](value: A): Schema.Product[A] = empty.imap(_ => value)(_ => ())
  def enumeration[A]: EnumerationBuilder[A] = new EnumerationBuilder[A]

  final class EnumerationBuilder[A] {
    def apply[B](
        schema: Schema.Primitive[B]
    )(mapping: A => B)(implicit evidence: Evidence.Enumeration[A]): Schema.Enumeration[A] = {
      val lookup = evidence.values.map(a => a -> mapping(a)).toMap
      Schema.Enumeration.apply[B, A](schema, evidence.values, lookup)
    }
  }

  val json: Schema.Any[Json] = Schema.Any[Json](
    decodeJson = _.valid,
    decodeString = parsing.json.run(_).leftMap(Errors.root)
  )(encodeJson = identity, encodeString = _.noSpaces)

  def field[A](name: String, schema: => Schema[A]): Field[A] = Field.default(name, schema)
  def branch[A](name: String, schema: => Schema[A]): Branch[A] = Branch.default(name, schema)

  object header {
    def apply[A](name: CIString, schema: => Schema[A]): Header[A] = Header.default(name, schema)
    def hist(header: Header[_]): Selection.History = Selection.History.Root / "header" / header.name.toString
  }

  object parameter {
    def apply[A](name: String, schema: => Schema.Value[A]): Parameter[A] = Parameter.default(name, schema)
    def hist(parameter: Parameter[_]): Selection.History = Selection.History.Root / "parameter" / parameter.name
  }

  object query {
    def apply[A](name: String, schema: => Schema[A]): Query[A] = Query.default(name, schema)
    def hist(query: Query[_]): Selection.History = Selection.History.Root / "query" / query.name
  }

  val constraint: Schema.Sum[Constraint] = {
    val or = (
      field("left", collection.set(constraint)) |*|
        field("right", collection.set(constraint))
    ).ximap[Constraint.Or]

    val rule = (
      field("identifier", string.imap(Constraint.Identifier.apply)(_.value)) |*|
        field("reference", optional(json)) |*|
        field("delta", optional(double)) |*|
        field("equal", optional(boolean))
    ).ximap[Constraint.Rule]

    (branch("or", or) |+| branch("rule", rule)).withoutDiscriminator.ximap
  }

  val violation: Schema.Sum[Violation] = {
    val reference = field("reference", optional(json))
    val actual = field("actual", json)

    (
      branch("validation", (field("constraint", constraint) |*| actual).ximap[Violation.Validation]) |+|
        branch("conflict", actual.toProduct.ximap[Violation.Conflict]) |+|
        branch("invalid", (reference |*| actual).ximap[Violation.Invalid]) |+|
        branch("missing", reference.toProduct.ximap[Violation.Missing]) |+|
        branch("unknown", actual.toProduct.ximap[Violation.Unknown])
    ).ximap
  }

  val history: Schema.Primitive[Selection.History] = string.ivalidate {
    Validation.fromOptionNel(Constraint(Constraint.Identifier("json-path")))(Selection.History.parse(_).toOption)
  }(_.toJsonPath)

  object errors {
    val failures: Schema.Sum[Errors.Failures] = branch(
      "failures",
      collection
        .list((field("message", string) |*| field("history", history)).ximap[Errors.Failure])
        .imap(Errors.Failures.apply)(_.values)
    ).toSum

    val validations: Schema.Sum[Errors.Validations] = branch(
      "validations",
      collection
        .nonEmptyMap(field("history", history) |*| field("errors", collection.nonEmptyList(violation)))
        .imap(Violations.apply)(_.toNem)
        .imap(Errors.Validations.apply)(_.violations)
    ).toSum

    val main: Schema.Sum[Errors] = (failures orElseAll validations).ximap
  }

  object error {
    def apply[A](history: Selection.History)(validation: Validation[Violation, A])(f: A => Violation): Schema.Sum[A] =
      errors.validations.ivalidate {
        // TODO keep track of selected fields in history
        Validation
          .ask[Errors.Validations]
          .map(_.violations.head(history))
          .required
          .andThen(validation)
      }(a => Errors.oneNel(history, f(a)))

    def string(history: Selection.History)(f: String => Violation): Schema.Sum[String] =
      error(history)(Validation.ask[Violation].map(_.toActual).required.map(_.asString).required)(f)

    def string(parameter: Parameter[_])(f: String => Violation): Schema.Sum[String] =
      string(dsl.parameter.hist(parameter))(f)
    def string(query: Query[_])(f: String => Violation): Schema.Sum[String] = string(dsl.query.hist(query))(f)
    def string(header: Header[_])(f: String => Violation): Schema.Sum[String] = string(dsl.header.hist(header))(f)
  }

  val url: Url.Root.type = Url.Root

  def input[A](method: Method, url: Url[A]): Input[A] = Input.from(method, url)
  def input[A, B](method: Method, url: Url[A], body: Schema[B]): Input[A |*| B] = Input.from(method, url, body)
  def get[A](url: Url[A]): Input[A] = input(Method.Get, url)
  def post[A, B](url: Url[A], body: Schema[B]): Input[A |*| B] = input(Method.Post, url, body)

  def result(code: Code): Output.Result[Unit] = Output.Result.empty(code, headers = Chain.empty)

  def result[A](code: Code, body: Schema[A]): Output.Result[A] =
    Output.Result.fromSchema(code, headers = Chain.empty, body)

  def output[A](results: Output.Results[A]): Output[A] = Output(
    results,
    (result(code.badRequest, errors.failures) |+| result(code.unprocessableEntity, errors.validations)).ximap[Errors]
  )

  def output[A](result: Output.Result[A]): Output[A] = output(Output.Results.fromResult(result))

  def output[A, B](errors: Output.Results[A], success: Output.Result[B]): Output[A |+| B] = output(errors |+| success)

  def endpoint[I, O](input: Input[I], output: Output[O]): Endpoint[I, O] = Endpoint(input, output)

  object code {
    val ok: Code = Code(200)
    val created: Code = Code(201)
    val accepted: Code = Code(202)
    val noContent: Code = Code(204)
    val movedPermanently: Code = Code(301)
    val found: Code = Code(302)
    val seeOther: Code = Code(303)
    val temporaryRedirect: Code = Code(307)
    val permanentRedirect: Code = Code(308)
    val badRequest: Code = Code(400)
    val unauthorized: Code = Code(401)
    val forbidden: Code = Code(403)
    val notFound: Code = Code(404)
    val conflict: Code = Code(409)
    val payloadTooLarge: Code = Code(413)
    val unprocessableEntity: Code = Code(422)
    val internalServerError: Code = Code(500)
    val serviceUnavailable: Code = Code(503)
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy