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