Last active
December 14, 2015 16:29
-
-
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.
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 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." | |
} |
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 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 | |
} |
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
<!-- 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> |
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
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