Created
October 27, 2016 08:21
-
-
Save eirirlar/b40bd07a71044d3776bc069f210798c6 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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