-
Notifications
You must be signed in to change notification settings - Fork 708
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
introduce scalding-quotation sub-project
- Loading branch information
Showing
17 changed files
with
1,091 additions
and
3 deletions.
There are no files selected for viewing
This file contains 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
This file contains 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
This file contains 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
41 changes: 41 additions & 0 deletions
41
scalding-quotation/src/main/scala/com/twitter/scalding/quotation/Liftables.scala
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
package com.twitter.scalding.quotation | ||
|
||
import scala.reflect.macros.blackbox.Context | ||
|
||
trait Liftables { | ||
val c: Context | ||
import c.universe.{ TypeName => _, _ } | ||
|
||
protected implicit val sourceLiftable: Liftable[Source] = Liftable { | ||
case Source(path, line) => q"_root_.com.twitter.scalding.quotation.Source($path, $line)" | ||
} | ||
|
||
protected implicit val projectionsLiftable: Liftable[Projections] = Liftable { | ||
case p => q"_root_.com.twitter.scalding.quotation.Projections(${p.set})" | ||
} | ||
|
||
protected implicit val typeNameLiftable: Liftable[TypeName] = Liftable { | ||
case TypeName(name) => q"_root_.com.twitter.scalding.quotation.TypeName($name)" | ||
} | ||
|
||
protected implicit val accessorLiftable: Liftable[Accessor] = Liftable { | ||
case Accessor(name) => q"_root_.com.twitter.scalding.quotation.Accessor($name)" | ||
} | ||
|
||
protected implicit val quotedLiftable: Liftable[Quoted] = Liftable { | ||
case Quoted(source, call, fa) => q"_root_.com.twitter.scalding.quotation.Quoted($source, $call, $fa)" | ||
} | ||
|
||
protected implicit val projectionLiftable: Liftable[Projection] = Liftable { | ||
case p: Property => q"$p" | ||
case p: TypeReference => q"$p" | ||
} | ||
|
||
protected implicit val propertyLiftable: Liftable[Property] = Liftable { | ||
case Property(path, accessor, tpe) => q"_root_.com.twitter.scalding.quotation.Property($path, $accessor, $tpe)" | ||
} | ||
|
||
protected implicit val typeReferenceLiftable: Liftable[TypeReference] = Liftable { | ||
case TypeReference(name) => q"_root_.com.twitter.scalding.quotation.TypeReference($name)" | ||
} | ||
} |
146 changes: 146 additions & 0 deletions
146
scalding-quotation/src/main/scala/com/twitter/scalding/quotation/Projection.scala
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,146 @@ | ||
package com.twitter.scalding.quotation | ||
|
||
import scala.annotation.tailrec | ||
|
||
case class Accessor(asString: String) extends AnyVal | ||
case class TypeName(asString: String) extends AnyVal | ||
|
||
sealed trait Projection { | ||
def andThen(accessor: Accessor, typeName: TypeName): Projection = | ||
Property(this, accessor, typeName) | ||
} | ||
|
||
/** | ||
* A reference of a type. If not nested within a `Property`, it means that all fields are used. | ||
*/ | ||
final case class TypeReference(typeName: TypeName) extends Projection { | ||
override def toString = typeName.asString.split('.').last | ||
} | ||
|
||
/** | ||
* A projection property (e.g. `Person.name`) | ||
*/ | ||
final case class Property(path: Projection, accessor: Accessor, typeName: TypeName) extends Projection { | ||
override def toString = s"$path.${accessor.asString}" | ||
} | ||
|
||
/** | ||
* Utility class to deal with a collection of projections. | ||
*/ | ||
final class Projections private (val set: Set[Projection]) extends Serializable { | ||
|
||
/** | ||
* Returns the projections that are based on `tpe` and limits projections | ||
* to only properties that extend from `superClass`. | ||
*/ | ||
def of(typeName: TypeName, superClass: Class[_]): Projections = { | ||
|
||
def byType(p: Projection) = { | ||
@tailrec def loop(p: Projection): Boolean = | ||
p match { | ||
case TypeReference(`typeName`) => true | ||
case TypeReference(_) => false | ||
case Property(p, _, _) => loop(p) | ||
} | ||
loop(p) | ||
} | ||
|
||
def bySuperClass(p: Projection): Option[Projection] = { | ||
|
||
def isSubclass(c: TypeName) = | ||
try | ||
superClass.isAssignableFrom(Class.forName(c.asString)) | ||
catch { | ||
case _: ClassNotFoundException => | ||
false | ||
} | ||
|
||
def loop(p: Projection): Either[Projection, Option[Projection]] = | ||
p match { | ||
case TypeReference(tpe) => | ||
Either.cond(!isSubclass(tpe), None, p) | ||
case p @ Property(path, name, tpe) => | ||
loop(path) match { | ||
case Left(_) => | ||
Either.cond(!isSubclass(tpe), Some(p), p) | ||
case Right(path) => | ||
Right(path) | ||
} | ||
} | ||
|
||
loop(p) match { | ||
case Left(path) => Some(path) | ||
case Right(opt) => opt | ||
} | ||
} | ||
|
||
Projections(set.filter(byType).flatMap(bySuperClass)) | ||
} | ||
|
||
/** | ||
* Given a set of base projections, returns the projections based on them. | ||
* | ||
* For instance, given a quoted function | ||
* `val contact = Quoted.function { (c: Contact) => c.contact }` | ||
* and a call | ||
* `(p: Person) => contact(p.name)` | ||
* returns the projection | ||
* `Person.name.contact` | ||
*/ | ||
def basedOn(base: Set[Projection]): Projections = { | ||
def loop(base: Projection, p: Projection): Option[Projection] = | ||
p match { | ||
case TypeReference(tpe) => | ||
base match { | ||
case TypeReference(`tpe`) => Some(p) | ||
case Property(_, _, `tpe`) => Some(base) | ||
case other => None | ||
} | ||
case Property(path, name, tpe) => | ||
loop(base, path).map(Property(_, name, tpe)) | ||
} | ||
Projections { | ||
set.flatMap { p => | ||
base.flatMap(loop(_, p)) | ||
} | ||
} | ||
} | ||
|
||
def ++(p: Projections) = | ||
Projections(set ++ p.set) | ||
|
||
override def toString = | ||
s"Projections(${set.mkString(", ")})" | ||
|
||
override def equals(other: Any) = | ||
other match { | ||
case other: Projections => set == other.set | ||
case other => false | ||
} | ||
|
||
override def hashCode = | ||
31 * set.hashCode | ||
} | ||
|
||
object Projections { | ||
val empty = apply(Set.empty) | ||
|
||
/** | ||
* Creates a normalized projections collection. For instance, | ||
* given two projections `Person.contact` and `Person.contact.phone`, | ||
* creates a collection with only `Person.contact`. | ||
*/ | ||
def apply(set: Set[Projection]) = { | ||
@tailrec def isNested(p: Projection): Boolean = | ||
p match { | ||
case Property(path, acessor, property) => | ||
set.contains(path) || isNested(path) | ||
case _ => | ||
false | ||
} | ||
new Projections(set.filter(!isNested(_))) | ||
} | ||
|
||
def flatten(list: Iterable[Projections]): Projections = | ||
list.foldLeft(empty)(_ ++ _) | ||
} |
106 changes: 106 additions & 0 deletions
106
scalding-quotation/src/main/scala/com/twitter/scalding/quotation/ProjectionMacro.scala
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,106 @@ | ||
package com.twitter.scalding.quotation | ||
|
||
import scala.reflect.macros.blackbox.Context | ||
|
||
trait ProjectionMacro extends TreeOps with Liftables { | ||
val c: Context | ||
import c.universe.{ TypeName => _, _ } | ||
|
||
def projections(params: List[Tree]): Tree = { | ||
|
||
def typeName(t: Tree) = | ||
TypeName(t.symbol.typeSignature.typeSymbol.fullName) | ||
|
||
def accessor(m: TermName) = | ||
Accessor(m.decodedName.toString) | ||
|
||
def typeReference(tpe: Type) = | ||
TypeReference(TypeName(tpe.typeSymbol.fullName)) | ||
|
||
def isFunction(t: Tree) = | ||
Option(t.symbol).map { | ||
_.typeSignature | ||
.erasure | ||
.typeSymbol | ||
.fullName | ||
.contains("scala.Function") | ||
}.getOrElse(false) | ||
|
||
val nestedList = | ||
params.flatMap { | ||
case param @ q"(..$inputs) => $body" => | ||
|
||
val inputSymbols = inputs.map(_.symbol).toSet | ||
|
||
object ProjectionExtractor { | ||
def unapply(t: Tree): Option[Tree] = | ||
t match { | ||
|
||
case q"$v.$m(..$params)" => unapply(v) | ||
|
||
case q"$v.$m" if t.symbol.isMethod => | ||
|
||
if (inputSymbols.contains(v.symbol)) { | ||
val p = | ||
TypeReference(typeName(v)) | ||
.andThen(accessor(m), typeName(t)) | ||
Some(q"$p") | ||
} else | ||
unapply(v).map { n => | ||
q"$n.andThen(${accessor(m)}, ${typeName(t)})" | ||
} | ||
|
||
case t if inputSymbols.contains(t.symbol) => | ||
Some(q"${TypeReference(typeName(t))}") | ||
|
||
case _ => None | ||
} | ||
} | ||
|
||
def functionCall(func: Tree, params: List[Tree]) = { | ||
val paramProjections = params.flatMap(ProjectionExtractor.unapply) | ||
q""" | ||
$func match { | ||
case f: _root_.com.twitter.scalding.quotation.QuotedFunction => | ||
f.quoted.projections.basedOn($paramProjections.toSet) | ||
case _ => | ||
_root_.com.twitter.scalding.quotation.Projections(Set(..$paramProjections)) | ||
} | ||
""" | ||
} | ||
|
||
collect(body) { | ||
case q"$func.apply[..$t](..$params)" => | ||
functionCall(func, params) | ||
case q"$func(..$params)" if isFunction(func) => | ||
functionCall(func, params) | ||
case t @ ProjectionExtractor(p) => | ||
q"_root_.com.twitter.scalding.quotation.Projections(Set($p))" | ||
} | ||
|
||
case func if isFunction(func) => | ||
val paramProjections = | ||
func.symbol.typeSignature.typeArgs.dropRight(1) | ||
.map(typeReference) | ||
q""" | ||
$func match { | ||
case f: _root_.com.twitter.scalding.quotation.QuotedFunction => | ||
f.quoted.projections | ||
case _ => | ||
_root_.com.twitter.scalding.quotation.Projections(Set(..$paramProjections)) | ||
} | ||
""" :: Nil | ||
|
||
case method if method.symbol != null && method.symbol.isMethod => | ||
val paramRefs = | ||
method.symbol.asMethod.paramLists.flatten | ||
.map(param => typeReference(param.typeSignature)) | ||
q"${Projections(paramRefs.toSet)}" :: Nil | ||
|
||
case other => | ||
Nil | ||
} | ||
|
||
q"_root_.com.twitter.scalding.quotation.Projections.flatten($nestedList)" | ||
} | ||
} |
32 changes: 32 additions & 0 deletions
32
scalding-quotation/src/main/scala/com/twitter/scalding/quotation/Quoted.scala
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
package com.twitter.scalding.quotation | ||
|
||
import java.io.File | ||
|
||
/** | ||
* Meta information about a method call. | ||
*/ | ||
case class Quoted(position: Source, text: Option[String], projections: Projections) { | ||
override def toString = s"$position ${text.getOrElse("")}" | ||
} | ||
|
||
object Quoted { | ||
import language.experimental.macros | ||
implicit def method: Quoted = macro QuotedMacro.method | ||
|
||
private[scalding] def internal: Quoted = macro QuotedMacro.internal | ||
|
||
def function[T1, U](f: T1 => U): Function1[T1, U] with QuotedFunction = macro QuotedMacro.function | ||
def function[T1, T2, U](f: (T1, T2) => U): Function2[T1, T2, U] with QuotedFunction = macro QuotedMacro.function | ||
def function[T1, T2, T3, U](f: (T1, T2, T3) => U): Function3[T1, T2, T3, U] with QuotedFunction = macro QuotedMacro.function | ||
def function[T1, T2, T3, T4, U](f: (T1, T2, T3, T4) => U): Function4[T1, T2, T3, T4, U] with QuotedFunction = macro QuotedMacro.function | ||
def function[T1, T2, T3, T4, T5, U](f: (T1, T2, T3, T4, T5) => U): Function5[T1, T2, T3, T4, T5, U] with QuotedFunction = macro QuotedMacro.function | ||
} | ||
|
||
case class Source(path: String, line: Int) { | ||
def classFile = path.split(File.separator).last | ||
override def toString = s"$classFile:$line" | ||
} | ||
|
||
trait QuotedFunction { | ||
def quoted: Quoted | ||
} |
Oops, something went wrong.