diff --git a/build.sbt b/build.sbt index f4722a0..284dd11 100755 --- a/build.sbt +++ b/build.sbt @@ -5,6 +5,7 @@ val Version = new { val Circe = "0.14.1" val Java = "11" val Munit = "0.7.29" + val OpenApi = "0.0.0+34-7853f5fb-SNAPSHOT" val Scala213 = "2.13.8" val Scala3 = "3.1.2" val ScalaJavaTime = "2.3.0" @@ -51,7 +52,7 @@ lazy val root = module(identifier = None) lazy val core = module(identifier = Some("core")) .settings( libraryDependencies ++= - "io.circe" %%% "circe-parser" % Version.Circe :: + "io.hireproof" %%% "openapi-core" % Version.OpenApi :: "org.typelevel" %%% "cats-core" % Version.Cats :: "org.scalameta" %%% "munit" % Version.Munit % "test" :: Nil diff --git a/modules/core/src/main/scala/io/hireproof/screening/CirceInstances.scala b/modules/core/src/main/scala/io/hireproof/screening/CirceInstances.scala deleted file mode 100644 index 3793ff4..0000000 --- a/modules/core/src/main/scala/io/hireproof/screening/CirceInstances.scala +++ /dev/null @@ -1,93 +0,0 @@ -package io.hireproof.screening - -import cats.data.{NonEmptyList, NonEmptyMap} -import cats.syntax.all._ -import io.circe.syntax._ -import io.circe.{CursorOp, Decoder, Encoder, Json, JsonObject, KeyDecoder, KeyEncoder} - -import java.util.concurrent.TimeUnit -import scala.concurrent.duration.FiniteDuration - -trait CirceInstances { - implicit val decoderTimeUnit: Decoder[TimeUnit] = Decoder[String].emap { value => - Either.catchOnly[IllegalArgumentException](TimeUnit.valueOf(value)).leftMap(_ => "TimeUnit") - } - - implicit val encoderTimeUnit: Encoder[TimeUnit] = Encoder[String].contramap(_.name()) - - implicit val decoderFiniteDuration: Decoder[FiniteDuration] = Decoder.instance { cursor => - for { - length <- cursor.get[Long]("length") - unit <- cursor.get[TimeUnit]("unit") - } yield FiniteDuration(length, unit) - } - - implicit val encoderFiniteDuration: Encoder[FiniteDuration] = Encoder.instance { duration => - Json.obj("length" := duration.length, "unit" := duration.unit) - } - - implicit val decoderConstraintRule: Decoder[Constraint.Rule] = Decoder.instance { cursor => - for { - identifier <- cursor.get[String]("identifier").map(Constraint.Identifier.apply) - reference <- cursor.get[Option[Json]]("reference") - delta <- cursor.get[Option[Double]]("delta") - equal <- cursor.get[Option[Boolean]]("equal") - } yield Constraint.Rule(identifier, reference, delta, equal) - } - - implicit val decoderConstraint: Decoder[Constraint] = decoderConstraintRule.or { - Decoder.instance { cursor => - for { - left <- cursor.get[Set[Constraint]]("left") - right <- cursor.get[Set[Constraint]]("right") - } yield Constraint.Or(left, right) - } - } - - implicit val encoderConstraint: Encoder.AsObject[Constraint] = Encoder.AsObject.instance { - case Constraint.Or(left, right) => - JsonObject("left" := left, "right" := right) - case Constraint.Rule(identifier, reference, delta, equal) => - JsonObject( - "identifier" := identifier.value, - "reference" := reference, - "delta" := delta, - "equal" := equal - ).filter { case (_, json) => !json.isNull } - } - - implicit val decoderViolation: Decoder[Violation] = Decoder.instance { cursor => - cursor.get[String]("type").flatMap { - case "validation" => - (cursor.as[Constraint], cursor.get[Json]("actual")).mapN(Violation.Validation.apply) - case "conflict" => cursor.get[Json]("actual").map(Violation.Conflict.apply) - case "invalid" => - (cursor.get[Option[Json]]("reference"), cursor.get[Json]("actual")).mapN(Violation.Invalid.apply) - case "missing" => cursor.get[Option[Json]]("reference").map(Violation.Missing.apply) - case "unknown" => cursor.get[Json]("actual").map(Violation.Unknown.apply) - } - } - - implicit val encoderViolation: Encoder.AsObject[Violation] = Encoder.AsObject.instance { - case Violation.Validation(constraint, actual) => - JsonObject("type" := "validation", "actual" := actual) deepMerge constraint.asJsonObject - case Violation.Conflict(actual) => JsonObject("type" := "conflict", "actual" := actual) - case Violation.Invalid(reference, actual) => - JsonObject("type" := "invalid", "reference" := reference, "actual" := actual) - case Violation.Missing(reference) => JsonObject("type" := "missing", "reference" := reference) - case Violation.Unknown(actual) => JsonObject("type" := "unknown", "actual" := actual) - } - - implicit val keyEncoderCursorHistory: KeyEncoder[List[CursorOp]] = KeyEncoder.instance(CursorOp.opsToPath) - - implicit val keyEncoderSelectionHistory: KeyEncoder[Selection.History] = KeyEncoder.instance(_.toJsonPath) - - implicit val keyDecoderSelectionHistory: KeyDecoder[Selection.History] = - KeyDecoder.instance(Selection.History.parse(_).toOption) - - implicit val decoderViolations: Decoder[Violations] = - Decoder[NonEmptyMap[Selection.History, NonEmptyList[Violation]]].map(Violations.apply) - - implicit val encoderViolations: Encoder[Violations] = - Encoder[NonEmptyMap[Selection.History, NonEmptyList[Violation]]].contramap(_.toNem) -} diff --git a/modules/core/src/main/scala/io/hireproof/screening/Constraint.scala b/modules/core/src/main/scala/io/hireproof/screening/Constraint.scala index 19cd1eb..ef1f721 100644 --- a/modules/core/src/main/scala/io/hireproof/screening/Constraint.scala +++ b/modules/core/src/main/scala/io/hireproof/screening/Constraint.scala @@ -1,8 +1,8 @@ package io.hireproof.screening import cats.syntax.all._ -import io.circe.syntax._ -import io.circe.{Encoder, Json} +import io.hireproof.openapi.{Encoder, OpenApi} +import io.hireproof.openapi.syntax._ import java.time.ZonedDateTime import scala.concurrent.duration.FiniteDuration @@ -22,7 +22,7 @@ object Constraint { final case class Rule( identifier: Constraint.Identifier, - reference: Option[Json], + reference: Option[OpenApi], delta: Option[Double], equal: Option[Boolean] ) extends Constraint { @@ -42,7 +42,6 @@ object Constraint { val Contains: Constraint.Identifier = Identifier("contains") val Email: Constraint.Identifier = Identifier("email") val GreaterThan: Constraint.Identifier = Identifier("greaterThan") - val Json: Constraint.Identifier = Identifier("json") val LessThan: Constraint.Identifier = Identifier("lessThan") val Equal: Constraint.Identifier = Identifier("equal") val Matches: Constraint.Identifier = Identifier("matches") @@ -51,38 +50,32 @@ object Constraint { } def apply[A: Encoder](identifier: Identifier, reference: A, delta: Double, equal: Boolean): Constraint = - Rule(identifier, reference.asJson.some, delta.some, equal.some) + Rule(identifier, reference.asOpenApi.some, delta.some, equal.some) def apply[A: Encoder](identifier: Identifier, reference: A, delta: Double): Constraint = - Rule(identifier, reference.asJson.some, delta.some, equal = none) + Rule(identifier, reference.asOpenApi.some, delta.some, equal = none) def apply[A: Encoder](identifier: Identifier, reference: A, equal: Boolean): Constraint = - Rule(identifier, reference.asJson.some, delta = none, equal.some) + Rule(identifier, reference.asOpenApi.some, delta = none, equal.some) def apply[A: Encoder](identifier: Identifier, reference: A): Constraint = - Rule(identifier, reference.asJson.some, delta = none, equal = none) + Rule(identifier, reference.asOpenApi.some, delta = none, equal = none) def apply(identifier: Identifier): Constraint = Rule(identifier, reference = none, delta = none, equal = none) object collection { - def contains[A: Encoder](reference: A): Constraint = Constraint(Identifier.Contains, reference.asJson) + def contains[A: Encoder](reference: A): Constraint = Constraint(Identifier.Contains, reference) } object duration { def equal(reference: FiniteDuration): Constraint = Constraint(Identifier.Equal, reference) - def greaterThan(reference: FiniteDuration, equal: Boolean = true): Constraint = Constraint(Identifier.GreaterThan, reference, equal) - def lessThan(reference: FiniteDuration, equal: Boolean = true): Constraint = Constraint(Identifier.LessThan, reference, equal) } - def json(reference: String): Constraint = Constraint(Identifier.Json, reference) - object number { def equal[A: Encoder](reference: A, delta: Double = 0d): Constraint = Constraint(Identifier.Equal, reference, delta) - def greaterThan[A: Encoder](reference: A, delta: Double = 0d, equal: Boolean = true): Constraint = Constraint(Identifier.GreaterThan, reference, delta, equal) - def lessThan[A: Encoder](reference: A, delta: Double = 0d, equal: Boolean = true): Constraint = Constraint(Identifier.LessThan, reference, delta, equal) } @@ -93,19 +86,16 @@ object Constraint { object text { val email: Constraint = Constraint(Identifier.Email) - - def equal(reference: String): Constraint = Constraint(Identifier.Equal, reference) - - def matches(regex: Regex): Constraint = Constraint(Identifier.Matches, regex.regex) + def equal(reference: String): Constraint = Constraint(Identifier.Equal, OpenApi.fromString(reference)) + def matches(regex: Regex): Constraint = Constraint(Identifier.Matches, OpenApi.fromString(regex.regex)) } val required: Constraint = Constraint(Identifier.Required) object time { def after(reference: ZonedDateTime, equal: Boolean = true): Constraint = - Constraint(Identifier.After, reference, equal) - + Constraint(Identifier.After, OpenApi.fromString(reference.toString), equal) def before(reference: ZonedDateTime, equal: Boolean = true): Constraint = - Constraint(Identifier.Before, reference, equal) + Constraint(Identifier.Before, OpenApi.fromString(reference.toString), equal) } } diff --git a/modules/core/src/main/scala/io/hireproof/screening/OpenApiInstances.scala b/modules/core/src/main/scala/io/hireproof/screening/OpenApiInstances.scala new file mode 100644 index 0000000..956345a --- /dev/null +++ b/modules/core/src/main/scala/io/hireproof/screening/OpenApiInstances.scala @@ -0,0 +1,14 @@ +package io.hireproof.screening + +import io.hireproof.openapi.syntax._ +import io.hireproof.openapi.{Encoder, OpenApi} + +import scala.concurrent.duration.{FiniteDuration, TimeUnit} + +trait OpenApiInstances { + implicit val encoderTimeUnit: Encoder[TimeUnit] = Encoder[String].contramap(_.name()) + + implicit val encoderFiniteDuration: Encoder[FiniteDuration] = Encoder.instance { duration => + OpenApi.obj("length" := duration.length, "unit" := duration.unit) + } +} diff --git a/modules/core/src/main/scala/io/hireproof/screening/Selection.scala b/modules/core/src/main/scala/io/hireproof/screening/Selection.scala index 276bf41..5138d1e 100644 --- a/modules/core/src/main/scala/io/hireproof/screening/Selection.scala +++ b/modules/core/src/main/scala/io/hireproof/screening/Selection.scala @@ -14,23 +14,14 @@ object Selection { final case class History(toChain: Chain[Selection]) extends AnyVal { def /(field: String): History = append(Field(field)) - def /(index: Int): History = append(Index(index)) - def /(selection: Selection): History = append(selection) - def append(selection: Selection): History = History(toChain append selection) - def /:(field: String): History = prepend(Field(field)) - def /:(index: Int): History = prepend(Index(index)) - def /:(selection: Selection): History = prepend(selection) - def prepend(selection: Selection): History = History(toChain prepend selection) - def ++(history: History): History = History(toChain ++ history.toChain) - def isRoot: Boolean = toChain.isEmpty def up: Selection.History = toChain.initLast match { diff --git a/modules/core/src/main/scala/io/hireproof/screening/Validation.scala b/modules/core/src/main/scala/io/hireproof/screening/Validation.scala index 4cf7b30..1a743bf 100644 --- a/modules/core/src/main/scala/io/hireproof/screening/Validation.scala +++ b/modules/core/src/main/scala/io/hireproof/screening/Validation.scala @@ -4,7 +4,7 @@ import cats.Applicative import cats.arrow.Arrow import cats.data.{NonEmptyList, Validated, ValidatedNel} import cats.syntax.all._ -import io.circe.{Encoder, Json} +import io.hireproof.openapi.{Encoder, OpenApi} import io.hireproof.screening.validations._ import scala.reflect.ClassTag @@ -33,7 +33,7 @@ abstract class Validation[-I, +O] { final def withConstraint(constraint: Constraint): Validation[I, O] = Validation(Set(constraint))(run(_).leftMap { violations => - val actual = violations.collectFirstSome(_.toActual).getOrElse(Json.Null) + val actual = violations.collectFirstSome(_.toActual).getOrElse(OpenApi.Null) NonEmptyList.one(Violation(constraint, actual)) }) diff --git a/modules/core/src/main/scala/io/hireproof/screening/Violation.scala b/modules/core/src/main/scala/io/hireproof/screening/Violation.scala index 06c6fb5..52d3759 100644 --- a/modules/core/src/main/scala/io/hireproof/screening/Violation.scala +++ b/modules/core/src/main/scala/io/hireproof/screening/Violation.scala @@ -1,46 +1,46 @@ package io.hireproof.screening import cats.syntax.all._ -import io.circe.syntax._ -import io.circe.{Encoder, Json} +import io.hireproof.openapi.{Encoder, OpenApi} +import io.hireproof.openapi.syntax._ sealed abstract class Violation extends Product with Serializable { def toConstraint: Option[Constraint] - - def toActual: Option[Json] + def toActual: Option[OpenApi] } object Violation { - final case class Validation(constraint: Constraint, actual: Json) extends Violation { + final case class Validation(constraint: Constraint, actual: OpenApi) extends Violation { override def toConstraint: Option[Constraint] = constraint.some - override def toActual: Option[Json] = actual.some + override def toActual: Option[OpenApi] = actual.some } - final case class Conflict(actual: Json) extends Violation { + final case class Conflict(actual: OpenApi) extends Violation { override def toConstraint: Option[Constraint] = none - override def toActual: Option[Json] = actual.some + override def toActual: Option[OpenApi] = actual.some } - final case class Invalid(reference: Option[Json], actual: Json) extends Violation { + final case class Invalid(reference: Option[OpenApi], actual: OpenApi) extends Violation { override def toConstraint: Option[Constraint] = none - override def toActual: Option[Json] = actual.some + override def toActual: Option[OpenApi] = actual.some } - final case class Missing(reference: Option[Json]) extends Violation { + final case class Missing(reference: Option[OpenApi]) extends Violation { override def toConstraint: Option[Constraint] = none - override def toActual: Option[Json] = none + override def toActual: Option[OpenApi] = none } - final case class Unknown(actual: Json) extends Violation { + final case class Unknown(actual: OpenApi) extends Violation { override def toConstraint: Option[Constraint] = none - override def toActual: Option[Json] = actual.some + override def toActual: Option[OpenApi] = actual.some } - def apply[A: Encoder](constraint: Constraint, actual: A): Violation = Validation(constraint, actual.asJson) - def conflict[A: Encoder](actual: A): Violation = Conflict(actual.asJson) - def invalid[A: Encoder](reference: A, actual: A): Violation = Invalid(reference.asJson.some, actual.asJson) - def invalid[A: Encoder](actual: A): Violation = Invalid(reference = none, actual.asJson) + def apply[A: Encoder](constraint: Constraint, actual: A): Violation = Validation(constraint, actual.asOpenApi) + def conflict[A: Encoder](actual: A): Violation = Conflict(actual.asOpenApi) + def invalid[A: Encoder, B: Encoder](reference: A, actual: B): Violation = + Invalid(reference.asOpenApi.some, actual.asOpenApi) + def invalid[A: Encoder](actual: A): Violation = Invalid(reference = none, actual.asOpenApi) val missing: Violation = Missing(reference = none) - def missing[A: Encoder](reference: A): Violation = Missing(reference.asJson.some) - def unknown[A: Encoder](actual: A): Violation = Unknown(actual.asJson) + def missing[A: Encoder](reference: A): Violation = Missing(reference.asOpenApi.some) + def unknown[A: Encoder](actual: A): Violation = Unknown(actual.asOpenApi) } diff --git a/modules/core/src/main/scala/io/hireproof/screening/package.scala b/modules/core/src/main/scala/io/hireproof/screening/package.scala index 64ace35..21971e9 100644 --- a/modules/core/src/main/scala/io/hireproof/screening/package.scala +++ b/modules/core/src/main/scala/io/hireproof/screening/package.scala @@ -3,7 +3,7 @@ package io.hireproof import cats.data.ValidatedNel import cats.syntax.all._ -package object screening extends CirceInstances { +package object screening extends OpenApiInstances { val hist: Selection.History = Selection.History.Root implicit class RichValidatedNel[E, A](val validated: ValidatedNel[E, A]) extends AnyVal { diff --git a/modules/core/src/main/scala/io/hireproof/screening/validations.scala b/modules/core/src/main/scala/io/hireproof/screening/validations.scala index fc69ee5..05ab392 100644 --- a/modules/core/src/main/scala/io/hireproof/screening/validations.scala +++ b/modules/core/src/main/scala/io/hireproof/screening/validations.scala @@ -1,10 +1,8 @@ package io.hireproof.screening -import cats.data.Chain import cats.syntax.all._ import cats.{Eq, Traverse, UnorderedFoldable} -import io.circe.{Decoder, Encoder, Json, JsonObject} -import io.circe.parser.parse +import io.hireproof.openapi.Encoder import java.time._ import java.time.format.DateTimeParseException @@ -85,26 +83,6 @@ object validations { Validation.condNel(Constraint.duration.equal(reference))(_ == reference) } - object json { - def apply[A: Decoder](reference: String): Validation[Json, A] = - Validation.fromOptionNel(Constraint.json(reference))(_.as[A].toOption) - - def field(name: String): Validation[Json, Json] = - Validation.fromOptionNel(Constraint.json(reference = name))(_.hcursor.downField(name).focus) - - def index(i: Int): Validation[Json, Json] = - Validation.fromOptionNel(Constraint.json(reference = String.valueOf(i)))(_.hcursor.downN(i).focus) - - val array: Validation[Json, Chain[Json]] = - Validation.fromOptionNel(Constraint.json(reference = "[]"))(_.as[Chain[Json]].toOption) - - val obj: Validation[Json, JsonObject] = - Validation.fromOptionNel(Constraint.json(reference = "{}"))(_.as[JsonObject].toOption) - - val string: Validation[Json, String] = - Validation.fromOptionNel(Constraint.json(reference = "String"))(_.asString) - } - object number { def equal[I: Numeric: Encoder](reference: I, delta: I): Validation[I, Unit] = Validation.condNel[I](Constraint.number.equal(reference, delta.toDouble)) { input => @@ -183,7 +161,6 @@ object validations { val offsetDateTime: Validation[String, OffsetDateTime] = catchOnly[DateTimeParseException]("offsetDateTime")(OffsetDateTime.parse) val offsetTime: Validation[String, OffsetTime] = catchOnly[DateTimeParseException]("offsetTime")(OffsetTime.parse) - val json: Validation[String, Json] = parsing("json")(parse(_).toOption) val short: Validation[String, Short] = parsing("short")(_.toShortOption) val uuid: Validation[String, UUID] = catchOnly[IllegalArgumentException]("uuid")(UUID.fromString) val zonedDateTime: Validation[String, ZonedDateTime] =