Skip to content

Instantly share code, notes, and snippets.

@eirirlar
Created October 27, 2016 08:21
Show Gist options
  • Save eirirlar/b40bd07a71044d3776bc069f210798c6 to your computer and use it in GitHub Desktop.
Save eirirlar/b40bd07a71044d3776bc069f210798c6 to your computer and use it in GitHub Desktop.
import shapeless._
import shapeless.ops.hlist.{Intersection, Length, Mapped, Mapper, ToTraversable, Zip, ZipWithKeys}
import shapeless.ops.record.{Keys, SelectAll, UnzipFields}
object checker {
sealed trait CheckedT {
val valid: Boolean
val messages: List[String]
}
case class Checked(path: List[String] = Nil, valid: Boolean = true, messages: List[String] = Nil) extends CheckedT
case class CheckedNoPath(valid: Boolean = true, messages: List[String] = Nil) extends CheckedT
sealed trait Check[C] {
type OptionalFields <: HList
def &&(cc: Check[C]): Check[C] = CheckAnd(this, cc)
def ||(cc: Check[C]): Check[C] = CheckOr(this, cc)
def apply(c: C, path: Option[String]): List[Checked]
def apply(c: C): List[Checked] = apply(c, None)
def apply(c: OptionalFields, path: Option[String]): List[Checked] = Nil
def apply(c: OptionalFields): List[Checked] = apply(c, None)
}
case class CheckOne[C](func: C => Boolean, msg: String = "") extends Check[C] {
override def &&(cc: Check[C]) =
CheckAnd(this, cc)
override def apply(c: C, path: Option[String]): List[Checked] = {
val r = func(c)
if (!r) List(Checked(path.toList, r, List(msg)))
else List(Checked(path = path.toList))
}
}
case class CheckAnd[C](cc0: Check[C], cc1: Check[C]) extends Check[C] {
override def apply(c: C, path: Option[String]): List[Checked] = {
val c0 = cc0(c, path)
val c1 = cc1(c, path)
List(Checked(path.toList,
c0.forall(_.valid) && c1.forall(_.valid),
c0.flatMap(_.messages) ++ c1.flatMap(_.messages)
))
}
}
case class CheckOr[C](cc0: Check[C], cc1: Check[C]) extends Check[C] {
override def apply(c: C, path: Option[String]): List[Checked] = {
val c0 = cc0(c, path)
if (c0.forall(_.valid)) c0
else {
val c1 = cc1(c, path)
if (c1.forall(_.valid)) c1
else CheckAnd(cc0, cc1)(c, path)
}
}
}
object Check {
type Aux[Checkable, OptionalFields0] = Check[Checkable] {
type OptionalFields = OptionalFields0
}
object check extends Poly1 {
implicit def apply[S <: Symbol, C, CC <: Check[C]] = at[(S, C, CC)] {
case (key, c, check) =>
check(c, Some(key.name))
}
}
object optionalCheck extends Poly1 {
implicit def apply[S <: Symbol, C, CC <: Check[C]] = at[(S, Option[C], CC)] {
case (key, oc, check) =>
oc.map(c => check(c, Some(key.name)))
}
}
def apply[C](func: C => Boolean, msg: String) = new CheckOne[C](func, msg)
def apply[Checkable] = new MkChecker[Checkable]
class MkChecker[Checkable] extends RecordArgs {
def applyRecord[
CheckableFields <: HList,
CheckableKeys <: HList,
CheckableValues <: HList,
CheckableValuesOptions <: HList,
OptionalFields0 <: HList,
Spec <: HList,
SpecKeys <: HList,
SpecValues <: HList,
KeysIntersect <: HList,
KeysIntersectLength <: Nat,
SpecCheckable <: HList,
SpecCheckableZip <: HList,
Checkeds <: HList,
SpecOptional <: HList,
SpecOptionalZip <: HList,
OptionalCheckeds <: HList
](spec: Spec)
(implicit
checkableLG: LabelledGeneric.Aux[Checkable, CheckableFields],
checkableUnzip: UnzipFields.Aux[CheckableFields, CheckableKeys, CheckableValues],
checkableValuesOptions: Mapped.Aux[CheckableValues, Option, CheckableValuesOptions],
checkableOptions: ZipWithKeys.Aux[CheckableKeys, CheckableValuesOptions, OptionalFields0],
specUnzip: UnzipFields.Aux[Spec, SpecKeys, SpecValues],
keysIntersect: Intersection.Aux[SpecKeys, CheckableKeys, KeysIntersect],
keysIntersectLength: Length.Aux[KeysIntersect, KeysIntersectLength],
keysLength: Length.Aux[Spec, KeysIntersectLength],
specCheckable: SelectAll.Aux[CheckableFields, SpecKeys, SpecCheckable],
specCheckableZip: Zip.Aux[SpecKeys :: SpecCheckable :: SpecValues :: HNil, SpecCheckableZip],
checkeds: Mapper.Aux[check.type, SpecCheckableZip, Checkeds],
checkedList: ToTraversable.Aux[Checkeds, List, List[Checked]],
specOptional: SelectAll.Aux[OptionalFields0, SpecKeys, SpecOptional],
specOptionalZip: Zip.Aux[SpecKeys :: SpecOptional :: SpecValues :: HNil, SpecOptionalZip],
optionalCheckeds: Mapper.Aux[optionalCheck.type, SpecOptionalZip, OptionalCheckeds],
optionalCheckedList: ToTraversable.Aux[OptionalCheckeds, List, Option[List[Checked]]]
): Check.Aux[Checkable, OptionalFields0] = {
new Check[Checkable] {
override type OptionalFields = OptionalFields0
override def apply(checkable: Checkable, path: Option[String]): List[Checked] = {
val specCheckable0 = specCheckable(checkableLG.to(checkable))
val specCheckableZip0 = specCheckableZip(specUnzip.keys() :: specCheckable0 :: specUnzip.values(spec) :: HNil)
val checkeds0 = checkeds(specCheckableZip0)
val checkedList0 = checkedList(checkeds0).flatten.map { c =>
c.copy(path = path.toList ++ c.path)
}
checkedList0
}
override def apply(of: OptionalFields0, path: Option[String]): List[Checked] = {
val specOptional0 = specOptional(of)
val specOptionalZip0 = specOptionalZip(specUnzip.keys() :: specOptional0 :: specUnzip.values(spec) :: HNil)
val optionalCheckeds0 = optionalCheckeds(specOptionalZip0)
val optionalCheckedList0 = optionalCheckedList(optionalCheckeds0).flatten.flatten.map { c =>
c.copy(path = path.toList ++ c.path)
}
optionalCheckedList0
}
}
}
}
}
object syntax {
class AsMap(cs: List[Checked]) {
def asMap =
cs.map { c =>
c.path.mkString(".") ->
CheckedNoPath(c.valid, c.messages)
}.toMap
}
implicit def toAsMap(cs: List[Checked]) = new AsMap(cs)
}
}
import checker.Check
import org.junit.{Assert, Test}
import shapeless.{Witness, _}
import shapeless.labelled.FieldType
class CheckerTest {
case class Pond(
name: String,
depth: Int = 4,
color: Double = 3.2
)
case class Duck(
name: String,
pond: Pond,
age: Int = 0,
walkingStyle: Int = -1,
canQuack: Boolean = false,
canFly: Boolean = false
)
//custom checkers typically defined in common package
object IsEvenNumber {
def apply(msg: String = "number must be even"): Check[Int] =
Check(0 == (_: Int) % 2, msg)
}
object IsPositiveNumber {
def apply(msg: String = "number must be positive"): Check[Int] =
Check(0 <= (_: Int), msg)
}
object IsUppercase {
def apply(msg: String = "names must be UPPERCASE"): Check[String] =
Check((_: String).forall(_.isUpper), msg)
}
val duckChecker = Check[Duck].apply(
name = IsUppercase(),
walkingStyle = IsEvenNumber() && IsPositiveNumber(),
age = IsPositiveNumber(),
pond = Check[Pond].apply(
name = IsUppercase()
)
)
@Test
def testChecker {
import checker.syntax._
val duckChecked = duckChecker(Duck("ducky", Pond("ponder"))).asMap
import io.circe.generic.auto._
import io.circe.syntax._
println("validated duck:\n" + duckChecked.asJson.spaces2)
Assert.assertTrue(duckChecked.contains("pond.name"))
}
@Test
def testIncomplete {
val duck =
None.asInstanceOf[FieldType[Witness.`'name`.T, Option[String]]] ::
Some(Pond("ponder")).asInstanceOf[FieldType[Witness.`'pond`.T, Option[Pond]]] ::
Some(12).asInstanceOf[FieldType[Witness.`'age`.T, Option[Int]]] ::
Some(5).asInstanceOf[FieldType[Witness.`'walkingStyle`.T, Option[Int]]] ::
None.asInstanceOf[FieldType[Witness.`'canQuack`.T, Option[Boolean]]] ::
None.asInstanceOf[FieldType[Witness.`'canFly`.T, Option[Boolean]]] ::
HNil
import checker.syntax._
val duckChecked = duckChecker.apply(duck).asMap
import io.circe.generic.auto._
import io.circe.syntax._
println("validated incomplete duck:\n" + duckChecked.asJson.spaces2)
Assert.assertTrue(duckChecked.contains("pond.name"))
Assert.assertFalse(duckChecked.contains("name"))
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment