Skip to content

Instantly share code, notes, and snippets.

@clessg
Last active December 14, 2015 16:29
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save clessg/5115225 to your computer and use it in GitHub Desktop.
Save clessg/5115225 to your computer and use it in GitHub Desktop.
RecordBinding trait for lift-record (mainly lift-mongo-record) that binds Record fields to markup.
import net.liftweb.common.Full
import net.liftweb.record.field.IntField
import net.liftweb.util.Helpers._
import net.tpmrpg.base.record.RecordBinding._
class Boot {
def boot() {
// add a custom binding for the Monster Record; read below.
// the second argument is a function that is passed the Monster instance
// MonsterBinding is a case class, so MonsterBinding.apply(monsterInstance)
// is called.
addBinding(Monster)(MonsterBinding)
}
}
/**
* You can create custom bindings and register them in Boot.scala for specific Records.
* For example, Monster doesn't have a field called 'neededExp' on the Record class,
* since it is calculated dynamically (i.e. we don't want to store the value in the
* database). So, we can use a custom Binding to dynamically calculate the value
* or mess around with the binding mechanism.
*
* If you do not provide a custom binding, the default behavior is used: simply
* allow binding of all the Record fields.
**/
case class MonsterBinding(record: MonsterBinding) extends RecordBinding[MonsterBinding] {
override def extraFields = Map(
"neededExp" -> new IntField(record) {
override def valueBox = Full(record.level * 5) // some complex logic here
}
)
override def bind =
super.bind andThen
".something" #> "Something, who knows."
}
import net.tpmrpg.base.record.RecordBinding._
class MonsterSummary(monster: Monster) {
// import above adds a "bind" method to all Records. this one will use the custom
// binding we made in Boot.scala.
def render = monster.bind
}
<!-- assume we used Monster.bind and passed in this HTML -->
<div class="monster-summary">
<p><b><span data-field="monsterInfo.name"></span></b> <span data-field="level">lv. </span> (<span data-field="exp"></span>/<span data-field="neededExp"></span>)</p>
<p><span data-field="user.username">Owner: </span></p>
<p><span data-field="species"></span></p>
<span class="something"></span>
<!-- BsonRecordField. Will be removed from the markup if valueBox returned Empty. -->
<div data-field="monsterInfo.genderRatio">
<b>Gender Ratio:</b>
<div data-field="male">Male: </div> <!-- MonsterGenderRatio.male -->
<div data-field="female">Female: </div> <!-- MonsterGenderRatio.female -->
</div>
<!-- BsonRecordListField on Monster.monsterInfo -->
<ul data-field="monsterInfo.abilities">
<li><span data-field="ability.name"></span></li> <!-- Ability.name -->
</ul>
<!-- BsonRecordListField -->
<ul data-field="moves">
<li> <!-- ListFields repeat the inner HTML for each record -->
<span data-field="move.name"></span>
</li>
</ul>
<!-- MongoRefField -->
<span data-field="user">
<!-- You can use %*% in attributes or in the markup to insert the field value
for some types of fields. If you don't, the value is appended. -->
<span data-field="toString"></span> <!-- User.toString -->
<span data-field="toXHtml"></span> <!-- User.toXHtml -->
<span data-field="username">User info for %*%.</span>
<img data-field="avatar" src="%*%"> <!-- User.avatar could be, for example, /images/avatars/1.png -->
<span data-field="rank">Rank </span>
<!-- User.isInCaptcha is a BooleanField. If true, the first one is displayed.
If false, the other is displayed. -->
<span data-field="isInCaptcha">Is in captcha</span>
<span data-field="isInCaptcha.!">Is not in captcha</span>
<!-- MongoRefField -->
<ul data-field="roles">
<span data-field="_id">%*%:</span>
<li>
<!-- ListField -->
<ul data-field="permissions">
<li>* %*%</li>
</ul>
</li>
</ul>
</span>
</div>
package net.tpmrpg.base.record
import net.liftweb.common.{Empty, Box, Full}
import net.liftweb.mongodb.record.field._
import net.liftweb.record.{MetaRecord, Field, Record}
import net.liftweb.record.field.BooleanField
import net.liftweb.util.Helpers._
import scala.xml._
object RecordBinding {
val fieldAttr = "data-field"
val fieldSeparator = '.'
val injectionString = "%*%"
trait RecordBinding[RecordType <: Record[RecordType]] {
val record: RecordType
def extraFields: Map[String, Field[_, RecordType]] = Map.empty
def findField(name: String) =
Box(extraFields.get(name)).or(record.fieldByName(name))
def bind: NodeSeq => NodeSeq = {
var curChild = NodeSeq.Empty
var furtherBindKids = true
def bindFields(nodes: Seq[Node]): NodeSeq = nodes.map {
case elem: scala.xml.Elem if (elem \ s"@$fieldAttr").isDefined =>
curChild = elem.child
furtherBindKids = true // might be changed by fieldValue()
val field = elem \ s"@$fieldAttr"
val fieldNodes = fieldValue(field.text.charSplit(fieldSeparator), this)
def newChild(newNodes: Either[List[NodeSeq], Box[NodeSeq]]): NodeSeq = newNodes match {
case Left(nodesToInject) =>
val source = elem.copy(
attributes = dropFieldAttribute(elem),
child = furtherBindKids.fullIf(true)(bindFields(elem.child)).openOr(Text(injectionString))
)
injectValue(source) {
nodesToInject.makeNodeSeq
}
case Right(Full(nodesToInject)) =>
newChild(Left(List(nodesToInject)))
case _ =>
NodeSeq.Empty
}
newChild(fieldNodes)
case elem: scala.xml.Elem =>
elem.copy(child = bindFields(elem.child))
case elem =>
elem
}.makeNodeSeq
def injectValue(nodes: NodeSeq)(value: NodeSeq): NodeSeq =
nodes.text.contains(injectionString) ||
nodes.flatMap(_.attributes.flatMap(_.value)).text.contains(injectionString) match {
case true =>
replaceInjectionPoints(nodes)(value)
case false =>
nodes.map {
case elem: Elem => elem.copy(child = elem.child ++ value)
case elem => elem
}.makeNodeSeq
}
def replaceInjectionPoints(nodes: NodeSeq)(value: NodeSeq): NodeSeq =
nodes.map {
case elem: Text if elem.text.contains(injectionString) =>
Text(elem.text.takeBefore(injectionString)) ++
value ++
replaceInjectionPoints(
Text(elem.text.takeAfter(injectionString))
)(value)
case elem: Elem =>
val newNodes = elem.copy(child = replaceInjectionPoints(elem.child)(value))
elem.attributes.filter(_.value.text.contains(injectionString)).map { attr =>
attr.key -> replaceInjectionPoints(attr.value)(value)
}.foldLeft(newNodes)(_ % _)
case elem =>
elem
}.makeNodeSeq
def dropFieldAttribute(n: Node) =
n.attributes.remove(fieldAttr)
/**
* If Right is returned, then the current node will be completely removed
* from the final markup. This is helpful for optional fields. Left is
* returned if the current node will be rendered regardless of whether
* or not it is full.
*/
def fieldValue(fieldsLeft: List[String],
binding: RecordBinding[RecordType]): Either[List[NodeSeq], Box[NodeSeq]] =
fieldsLeft match {
case List("toString") =>
Left(List(Text(binding.record.toString)))
case List("toXHtml") =>
Left(List(binding.record.toXHtml))
case List(field, "!") =>
binding.findField(field) match {
case Full(f: BooleanField[RecordType]) =>
Right(f.valueBox.flatMap { value =>
emptyNodeIf(value == false)
})
case _ =>
Left(invalidFieldSelector(fieldsLeft, binding))
}
case List(field) =>
binding.findField(field) match {
case Full(f: MongoRefField[RecordType, RecordType]) =>
furtherBindKids = false
Left(f.obj.map { _.bind(curChild) } toList)
case Full(f: BsonRecordField[RecordType, RecordType]) =>
furtherBindKids = false
Left(f.valueBox.map { _.bind(curChild) } toList)
case Full(f: MongoRefListField[RecordType, RecordType, _]) =>
furtherBindKids = false
Left(f.objs.map { _.bind(curChild) })
case Full(f: BsonRecordListField[RecordType, RecordType]) =>
furtherBindKids = false
Left(f.get.map { _.bind(curChild) })
case Full(f: MongoListField[RecordType, _]) =>
furtherBindKids = false
Left(f.get.flatMap { value =>
bind(injectValue(curChild)(Text(value.toString)))
})
case Full(f: BooleanField[RecordType]) =>
Right(f.valueBox.flatMap { value =>
emptyNodeIf(value == true)
})
case Full(f) =>
Right(f.valueBox.map { value =>
Text(value.toString)
})
case _ =>
Left(invalidFieldSelector(fieldsLeft, binding))
}
case List(field, _*) =>
binding.findField(field) match {
case Full(f: MongoRefField[RecordType, RecordType]) =>
f.obj.map { obj =>
fieldValue(fieldsLeft.drop(1), obj)
} openOr { Left(Nil) }
case Full(f: BsonRecordField[RecordType, RecordType]) =>
fieldValue(fieldsLeft.drop(1), f.get)
case _ =>
Left(invalidFieldSelector(fieldsLeft, binding))
}
}
def emptyNodeIf(predicate: Boolean) =
predicate.fullIf(true)(NodeSeq.Empty)
def invalidFieldSelector(fieldsLeft: Seq[String], rec: RecordBinding[RecordType]) =
List(Text(
s"Invalid field selector ${fieldsLeft.mkString(fieldSeparator.toString)} on ${rec.record.meta}."
))
bindFields _
}
}
private[record] var bindings: Map[MetaRecord[_], Record[_] => RecordBinding[_]] = Map.empty
def addBinding[RecordType <: Record[RecordType]](meta: MetaRecord[RecordType])
(bindFunc: RecordType => RecordBinding[RecordType]) {
bindings += meta -> bindFunc.asInstanceOf[Record[_] => RecordBinding[_]]
}
implicit def findBinding[RecordType <: Record[RecordType]](rec: RecordType) =
bindings.get(rec.meta).getOrElse({ givenRec: RecordType =>
new RecordBinding[RecordType] {
val record = givenRec
}
}).apply(rec).asInstanceOf[RecordBinding[RecordType]]
protected implicit class AdvancedBoolean(val repr: Boolean) extends AnyVal {
def fullIf[T](expected: Boolean)(valueIfFull: => T) = repr match {
case e if e == expected => Full(valueIfFull)
case _ => Empty
}
}
protected implicit class AdvancedNodes(val nodes: Seq[NodeSeq]) extends AnyVal {
def makeNodeSeq =
nodes.foldLeft(NodeSeq.Empty)(_ ++ _)
}
protected implicit class AdvancedString(val repr: String) extends AnyVal {
def takeAfter(str: String) =
repr.substring(repr.indexOf(str) + str.length)
def takeBefore(str: String) =
repr.substring(0, repr.indexOf(str))
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment