480 likes | 600 Views
Leveraging Scala Macros for Better Validation. Tomer Gabel, Wix JavaOne 2014. I Have a Dream. Definition: case class Person ( firstName : String , lastName : String ) implicit val personValidator = validator[ Person ] { p ⇒ p.firstName is notEmpty p.lastName is notEmpty }.
E N D
Leveraging Scala Macros for Better Validation Tomer Gabel, Wix JavaOne2014
I Have a Dream • Definition: caseclassPerson( firstName: String, lastName: String ) implicitvalpersonValidator= validator[Person] { p ⇒ p.firstName is notEmpty p.lastName is notEmpty }
I Have a Dream • Usage: validate(Person("Wernher", "von Braun”)) == Success validate(Person("", "No First Name”)) == Failure(Set(RuleViolation( value= "", constraint = "mustnotbeempty", description = "firstName" )))
The Accord API • Validation can succeedor fail • A failurecomprises one or more violations sealedtraitResult caseobjectSuccessextendsResult caseclassFailure(violations: Set[Violation])extendsResult • The validatortypeclass: traitValidator[-T] extends (T ⇒Result)
Why Macros? • Quick refresher: implicitvalpersonValidator= validator[Person] { p ⇒ p.firstName is notEmpty p.lastName is notEmpty } Implicit “and” Automatic description generation
Full Disclosure Macros are experimental Macros are hard I will gloss over a lot of details … and simplifya lot of things
Abstract Syntax Trees • An intermediate representation of code • Structure(semantics) • Metadata(e.g. types) – optional! • Provided by the reflection API • Alas, mutable • Until Dotty comes along
Abstract Syntax Trees defmethod(param: String) = param.toUpperCase
Abstract Syntax Trees defmethod(param: String) = param.toUpperCase • Apply( • Select( • Ident(newTermName("param")), • newTermName("toUpperCase") • ), • List() • )
Abstract Syntax Trees defmethod(param: String) = param.toUpperCase • ValDef( • Modifiers(PARAM), • newTermName("param"), • Select( • Ident(scala.Predef), • newTypeName("String") • ), • EmptyTree// Value • )
Abstract Syntax Trees defmethod(param: String) = param.toUpperCase • DefDef( • Modifiers(), • newTermName("method"), • List(), // Typeparameters • List( // Parameter lists • List(parameter) • ), • TypeTree(), // Return type • implementation • )
Def Macro 101 • Looks and acts like a normal function defradix(s: String, base: Int): Long valresult = radix("2710", 16) // result == 10000L • Two fundamental differences: • Invoked at compile time instead of runtime • Operates on ASTsinstead of values
Def Macro 101 • Needs a signature& implementation def radix(s: String, base: Int): Long= macro radixImpl defradixImpl (c: Context) (s: c.Expr[String], base: c.Expr[Int]): c.Expr[Long] Values ASTs
Def Macro 101 • What’s in a context? • Enclosures (position) • Error handling • Logging • Infrastructure
Overview implicitvalpersonValidator= validator[Person] { p ⇒ p.firstName is notEmpty p.lastName is notEmpty } • The validatormacro: • Rewrites each ruleby addition a description • Aggregates rules with an and combinator Macro Application Validation Rules
Signature defvalidator[T](v: T⇒ Unit): Validator[T] = macro ValidationTransform.apply[T] defapply[T : c.WeakTypeTag] (c: Context) (v: c.Expr[T ⇒Unit]): c.Expr[Validator[T]]
Brace yourselves Here be dragons
Search for Rule • A rule is an expression of type Validator[_] • We search by: • Recursively pattern matching over an AST • On match, apply a function on the subtree • Encoded as a partial function from Tree to R
Search for Rule defcollectFromPattern[R] (tree: Tree) (pattern: PartialFunction[Tree, R]): List[R] = { varfound: Vector[R] = Vector.empty newTraverser { overridedeftraverse(subtree: Tree) { if(pattern isDefinedAtsubtree) found = found :+ pattern(subtree) else super.traverse(subtree) } }.traverse(tree) found.toList }
Search for Rule • Putting it together: caseclassRule(ouv: Tree, validation: Tree) defprocessRule(subtree: Tree): Rule = ??? deffindRules(body: Tree): Seq[Rule] = { valvalidatorType = typeOf[Validator[_]] collectFromPattern(body) { casesubtreeifsubtree.tpe <:< validatorType ⇒ processRule(subtree) } }
Process Rule • The user writes: p.firstName is notEmpty • The compiler emits: Contextualizer(p.firstName).is(notEmpty) Type: Validator[_] Object Under Validation (OUV) Validation
Process Rule Contextualizer(p.firstName).is(notEmpty) • This is effectively an Apply AST node • The left-hand side is the OUV • The right-hand side is the validation • But we can use the entire expression! • Contextualizeris our entry point
Process Rule Contextualizer(p.firstName).is(notEmpty)
Process Rule Contextualizer(p.firstName).is(notEmpty)
Process Rule caseApply(TypeApply(Select(_, `term`), _), ouv:: Nil) ⇒
Process Rule • Putting it together: valterm= newTermName("Contextualizer") defprocessRule(subtree: Tree): Rule = extractFromPattern(subtree) { caseApply(TypeApply(Select(_, `term`), _), ouv:: Nil) ⇒ Rule(ouv, subtree) } getOrElseabort(subtree.pos, "Not a valid rule")
Generate Description Contextualizer(p.firstName).is(notEmpty) • Consider the object under validation • In this example, it is a field accessor • The function prototypeis the entry point validator[Person] { p ⇒ ... }
Generate Description • How to get at the prototype? • The macro signature includes the rule block: defapply[T : c.WeakTypeTag] (c: Context) (v: c.Expr[T ⇒Unit]): c.Expr[Validator[T]] • To extract the prototype: valFunction(prototype :: Nil, body) = v.tree// prototype: ValDef
Generate Description • Putting it all together: defdescribeRule(rule: ValidationRule) = { valpara = prototype.name valSelect(Ident(`para`), description) = rule.ouv description.toString }
Rewrite Rule • We’re constructing a Validator[Person] • A rule is itself a Validator[T]. For example: Contextualizer(p.firstName).is(notEmpty) • We need to: • Liftthe rule to validate the enclosing type • Apply the descriptionto the result
Quasiquotes • Provide an easy way to construct ASTs: Apply( Select( Ident(newTermName"x"), newTermName("$plus") ), List( Ident(newTermName("y")) ) ) • q"x + y"
Quasiquotes • Quasiquotes also let you splicetrees: defgreeting(whom: c.Expr[String]) = q"Hello\"$whom\"!" • And can be used in pattern matching: valq"$x + $y" = tree
Rewrite Rule Contextualizer(p.firstName).is(notEmpty) newValidator[Person] { defapply(p: Person) = { valvalidation = Contextualizer(p.firstName).is(notEmpty) validation(p.firstName) withDescription"firstName" } }
Rewrite Rule • Putting it all together: defrewriteRule(rule: ValidationRule) = { valdesc = describeRule(rule) valtree = Literal(Constant(desc)) q""" new com.wix.accord.Validator[${weakTypeOf[T]}] { defapply($prototype) = { val validation = ${rule.validation} validation(${rule.ouv}) withDescription$tree } } """ }
Epilogue • The finishing touch: and combinator defapply[T : c.WeakTypeTag] (c: Context) (v: c.Expr[T ⇒ Unit]): c.Expr[Validator[T]] = { valFunction(prototype :: Nil, body) = v.tree // ... all the stuff we just discussed valrules = findRules(body) map rewriteRule valresult= q"newcom.wix.accord.combinators.And(..$rules)" c.Expr[Validator[T]](result) }
tomer@tomergabel.com@tomerg http://il.linkedin.com/in/tomergabel Check out Accord at: http://github.com/wix/accord Thank you for listening We’re done here!