Skip to content

Commit

Permalink
Extract databinding concerns from Context
Browse files Browse the repository at this point in the history
  • Loading branch information
timcharper committed Jun 4, 2014
1 parent d7d2b65 commit a95dd44
Show file tree
Hide file tree
Showing 13 changed files with 276 additions and 300 deletions.
2 changes: 0 additions & 2 deletions build.sbt
Expand Up @@ -49,8 +49,6 @@ publishTo <<= version { (v: String) =>

publishArtifact in Test := false

testOptions in Test += Tests.Argument("-oD")

pomIncludeRepository := { _ => false }

licenses := Seq("BSD-style" -> url("http://www.opensource.org/licenses/bsd-license.php"))
Expand Down
50 changes: 50 additions & 0 deletions src/main/scala/com/gilt/handlebars/context/Binding.scala
@@ -0,0 +1,50 @@
package com.gilt.handlebars.context

trait Binding[T] {
val isUndefined: Boolean
def filterEmptyLike: Binding[T]
protected def isValueless: Boolean
def get: T
def toOption: Option[T]
def renderString: String
def isTruthy: Boolean
def isCollection: Boolean
def isDictionary: Boolean
def isPrimitive = ! isCollection && ! isDictionary
def asCollection: Iterable[Binding[T]]
def traverse(key: String, args: List[Any]): Binding[T] // If traversing a function-like, args are optionally provided
}

trait FullBinding[T] extends Binding[T] {
protected val data: T
if (data.isInstanceOf[Binding[_]]) {
throw new Exception("Bug! You tried to wrap a binding with a binding. Don't do that!")
}
def toOption: Option[T] = Some(data)
def get = data
override def toString = s"FullBinding(${data})"
def filterEmptyLike =
if (isValueless)
VoidBinding[T]
else
this
}

trait VoidBinding[T] extends Binding[T] {
val isUndefined = true
def toOption: Option[T] = None
def renderString = ""
def isValueless = true
def filterEmptyLike = this
def traverse(key: String, args: List[Any]) = this
lazy val asCollection = Seq()
def get = throw new RuntimeException("Tried to get value where not defined")
def isCollection = false
def isDictionary = false
def isTruthy = false
override def toString = "VoidBinding"
}

object VoidBinding extends VoidBinding[Any] {
def apply[T]: VoidBinding[T] = this.asInstanceOf[VoidBinding[T]]
}
11 changes: 0 additions & 11 deletions src/main/scala/com/gilt/handlebars/context/CacheableContext.scala

This file was deleted.

This file was deleted.

214 changes: 70 additions & 144 deletions src/main/scala/com/gilt/handlebars/context/Context.scala
@@ -1,45 +1,8 @@
package com.gilt.handlebars.context

import com.gilt.handlebars.logging.Loggable
import java.lang.reflect.Method
import com.gilt.handlebars.parser.{IdentifierNode, Identifier}

/**
* User: chicks
* Date: 5/30/13
*/
trait DefaultContextFactory extends ContextFactory { factory =>
def createUndefined[T]: Context[T] = {
new Context[T] {
override val isRoot = false
override val isUndefined = true
val contextFactory = factory
val model: T = null.asInstanceOf[T]
val parent: Context[T] = null.asInstanceOf[Context[T]]
}
}

def createRoot[T](_model: T): Context[T] = {
new Context[T] {
val model: T = _model
val isUndefined: Boolean = false
val contextFactory = factory
val isRoot: Boolean = true
val parent: Context[T] = createUndefined
}
}

def createChild[T](_model: T, _parent: Context[T]): Context[T] = {
new Context[T] {
val model: T = _model
val isUndefined: Boolean = false
val contextFactory = factory
val isRoot: Boolean = false
val parent: Context[T] = _parent
}
}
}

object ParentIdentifier {
def unapply(s: String): Option[String] = {
if ("..".equals(s)) Some(s) else None
Expand All @@ -52,27 +15,41 @@ object ThisIdentifier {
}
}

trait Context[+T] extends Loggable {

class ChildContext[T](val binding: Binding[T], val parent: Context[T]) extends Context[T] {
val isRoot = false
val isUndefined = binding.isUndefined

override def toString = "Child context: model[%s] parent[%s]".format(binding, parent)
}

class RootContext[T](val binding: Binding[T]) extends Context[T] {
val isRoot = true
val isUndefined = binding.isUndefined
val parent = VoidContext[T]
override def toString = "Root context: model[%s]".format(binding)
}

trait Context[T] extends Loggable {
val isRoot: Boolean
val isUndefined: Boolean
val model: Any // TODO - make protected
val binding: Binding[T]
val parent: Context[T]
val contextFactory: ContextFactory

def asOption: Option[Context[T]] = if (isUndefined || model == null) None else Some(this)
def asOption: Option[Context[T]] = binding.filterEmptyLike.toOption map { t => this }

def render: String = if (isUndefined || model == null) "" else model.toString
def render: String = binding.renderString

def notEmpty[A](fallback: Context[A]): Context[A] = if (isUndefined) fallback else this.asInstanceOf[Context[A]]

override def toString = "Context model[%s] parent[%s]".format(model, parent)

def lookup(path: IdentifierNode, args: List[Any] = List.empty): Context[Any] = {
def lookup(path: IdentifierNode, args: List[Any] = List.empty): Context[T] = {
args match {
case identifiers: List[_] =>
lookup(path.value, identifiers.collect {
case n: IdentifierNode => lookup(n).model
})
case identifiers: List[_] => {
val newArgs = identifiers.collect {
case n: IdentifierNode => lookup(n).binding.get
}
lookup(path.value, newArgs)
}
case _ =>
lookup(path.value, args)
}
Expand All @@ -82,119 +59,68 @@ trait Context[+T] extends Loggable {
* @param a
* @return
*/
def truthValue: Boolean = model match {
case /* UndefinedValue |*/ None | false | Nil | null | "" => false
case _ => true
}

def truthValue = binding.isTruthy

/**
* Returns the parent of the provided context, but skips artificial levels in the hierarchy
* introduced by Iterable, Option, etc.
*/
def safeParent: Context[Any] = {
if (this.isRoot || this.isUndefined) {
def safeParent: Context[T] = {
if (isRoot || isUndefined)
this
} else {
this.parent.model match {
case map:Map[_,_] => this.parent
case list:Iterable[_] => this.parent.parent.safeParent
case _ => this.parent
else if (parent.binding.isDictionary)
this.parent
else if (parent.binding.isCollection)
this.parent.safeParent
else
this.parent
}

// dictionaryFallbackFlag is work-around for a case in which a context is used to iterate a dictionary
// It'd be preferable to not create a context for the dictionary (thus preventing the need to skip it), or
// to capture signal somehow that the model is being used that way
def lookup(path: List[String], args: List[Any], dictionaryFallbackFlag: Boolean): Context[T] = {
path match {
case Nil => this
case _ if isUndefined => this
case ParentIdentifier(p) :: tail =>
safeParent.lookup(tail, args, true)

case ThisIdentifier(p) :: tail => if (tail.isEmpty) this else lookup(tail, args)
case head :: tail => {
val nextChild = childContext(binding.traverse(head, args))
if (dictionaryFallbackFlag && nextChild.isUndefined && binding.isDictionary)
safeParent.lookup(head :: tail, args)
else
nextChild.lookup(tail, args)
}
}
}
def lookup(path: List[String], args: List[Any]): Context[T] = lookup(path, args, false)

def lookup(path: List[String], args: List[Any]): Context[Any] = {
path.head match {
case p if isUndefined => this
case ParentIdentifier(p) =>
if (isRoot) {
// Too many '..' in the path so return this context, or drop the '..' and
// continue to look up the rest of the path
if (path.tail.isEmpty) this else lookup(path.tail, args)
} else {
if (path.tail.isEmpty) {
// Just the parent, '..'. Path doesn't access any property on it.
safeParent
} else {
safeParent.lookup(path.tail, args)
}
}

case ThisIdentifier(p) => if (path.tail.isEmpty) this else lookup(path.tail, args)
case _ =>
model match {
case Some(m) => contextFactory.createChild(m, parent).lookup(path, args)
case map:Map[_, _] =>
invoke(path.head, args).asOption.map {
ctx => if (path.tail.isEmpty) ctx else ctx.lookup(path.tail, args)
}.getOrElse(parent.lookup(path, args))
case list:Iterable[_] => {
if (isRoot) this else parent.lookup(path, args)
}
case _ => if (path.tail.isEmpty) invoke(path.head, args) else invoke(path.head, args).lookup(path.tail, args)
}
}
}
def childContext(model: Binding[T]): Context[T] =
new ChildContext[T](model, this)

def isCollection = {
model.isInstanceOf[Iterable[_]]
binding.isCollection
}

def map[R]( mapFn: (Context[T], Option[Int]) => R): Iterable[R] = {
model match {
case l:Iterable[_] => l.zipWithIndex.map {
case (item, idx) => mapFn(contextFactory.createChild(item.asInstanceOf[T], this), Some(idx))
if (binding.isCollection)
binding.asCollection.zipWithIndex.map {
case (item, idx) => mapFn(childContext(item), Some(idx))
}
case _ =>
Seq(mapFn(this, None))
}
}

protected def invoke(methodName: String, args: List[Any] = Nil): Context[Any] = {
getMethods(model.getClass)
.get(methodName + args.length)
.flatMap(invoke(_, args)).map {
value =>
contextFactory.createChild(value, this)
}.orElse {
model match {
case map:Map[_, _] =>
map.asInstanceOf[Map[String, _]].get(methodName).map( v => contextFactory.createChild(v, this))
case _ => None
}
}.getOrElse(contextFactory.createUndefined)
}


protected def invoke(method: Method, args: List[Any]): Option[Any] = {
debug("Invoking method: '%s' with arguments: [%s].".format(method.getName, args.mkString(",")))

try {
val result = method.invoke(model, args.map(_.asInstanceOf[AnyRef]): _*)

result match {
case Some(o) => if (isPrimitiveType(o)) Some(o) else Some(result)
case None => Some("")
case _ => Some(result)
}
} catch {
case e: java.lang.IllegalArgumentException => None
}
else
Seq(mapFn(this, None))
}
}

/**
* Returns a map containing the methods of the class - the reflection calls to generate this map
* have been memoized so this should be performant. The method uses a read-write lock to ensure thread-safe
* access to the map.
*
* @param clazz
* @return
*/
protected def getMethods(clazz: Class[_]): Map[String, Method] = {
clazz.getMethods.map(m => (m.getName + m.getParameterTypes.length, m)).toMap
}
trait ContextFactory[T] {
def apply(model: T): Context[T]
}

protected def isPrimitiveType(obj: Any) = obj.isInstanceOf[Int] || obj.isInstanceOf[Long] || obj.isInstanceOf[Float] ||
obj.isInstanceOf[BigDecimal] || obj.isInstanceOf[Double] || obj.isInstanceOf[String]
object Context extends ContextFactory[Any] {
def apply(_model: Any): Context[Any] =
new RootContext(DynamicBinding(_model))
}

0 comments on commit a95dd44

Please sign in to comment.