Created
August 25, 2011 19:49
-
-
Save casualjim/1171661 to your computer and use it in GitHub Desktop.
A versioning trait for lift-mongodb-record models with rogue
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
package mojolly | |
package mongo | |
package tests | |
import org.specs2.Specification | |
import org.specs2.specification.{ Step, Fragments } | |
import net.liftweb.mongodb._ | |
import mongo.MongoConfig.MongoConfiguration | |
trait LiftMongoDbSpecification extends Specification { | |
override def map(fs: ⇒ Fragments) = Step(openDatabase) ^ super.map(fs) ^ Step(stopDatabase) | |
def databaseName: String | |
def mongoHost = "127.0.0.1" | |
def mongoPort = 27017 | |
MongoConfig.config = MongoConfiguration(mongoHost, mongoPort, databaseName, queryLogLevel = "TRACE") | |
def openDatabase { | |
MongoDB.defineDb(DefaultMongoIdentifier, MongoConfig.config.asMongoAddress) | |
MongoDB.use(DefaultMongoIdentifier) { _.dropDatabase() } | |
} | |
def stopDatabase { | |
MongoDB.close | |
} | |
} |
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
package mojolly | |
package mongo | |
import LibraryImports._ | |
import net.liftweb.record.{ LifecycleCallbacks } | |
import net.liftweb.record.field.LongField | |
import collection.JavaConversions._ | |
import net.liftweb.mongodb.record.MongoId | |
import net.liftweb.json._ | |
import net.liftweb.json.JsonDSL._ | |
import com.mongodb.DB | |
import com.mongodb.casbah.Imports._ | |
import MongoDBImports._ | |
object ModelVersion { | |
implicit val formats = MongoConfig.formats | |
def apply(dbo: DBObject): ModelVersion = ModelVersion( | |
dbo.as[Long]("version"), | |
dbo.as[DateTime]("timestamp"), | |
Extraction.decompose(dbo.as[DBObject]("fields"))) | |
} | |
case class ModelVersion(version: Long, timestamp: DateTime, fields: JValue) { | |
import ModelVersion._ | |
def asJValue = ("version" -> version) ~ ("timestamp" -> timestamp.toString(ISO8601_DATE)) ~ ("fields" -> fields) | |
def asDBObject = MongoDBObject("version" -> version, "timestamp" -> timestamp.toDate, "fields" -> fields.extract[DBObject]) | |
} | |
trait VersionedModel[BaseModel <: VersionedModel[BaseModel]] extends MongoModel[BaseModel] with MongoId[BaseModel] { self: BaseModel ⇒ | |
object rev extends LongField(this) with LifecycleCallbacks { | |
private var changes: DBObject = null | |
override def beforeSave { | |
changes = owner.createNewVersion() | |
set(value + 1) | |
} | |
override def afterSave { | |
if (changes != null && changes.toMap.nonEmpty) { | |
owner.meta.update(Map("_id" -> owner.id), changes) | |
} | |
} | |
} | |
def meta: VersionedModelMeta[BaseModel] | |
def createNewVersion() = meta.createNewVersion(this) | |
} | |
trait VersionedModelMeta[BaseModel <: VersionedModel[BaseModel]] extends MongoMetaModel[BaseModel] { self: BaseModel ⇒ | |
def createNewVersion(model: BaseModel): DBObject = { | |
val original = (find(model.id).map(_.asJValue.camelizeKeys) openOr JNothing.asInstanceOf[JValue]) | |
if (original != JNothing) { | |
val Diff(mod, add, del) = model.asJValue diff original | |
$push ("versions" -> ModelVersion(model.rev.value, DateTime.now, mod merge add merge del).asDBObject) | |
} else $set ("versions" -> List.empty[DBObject]) | |
} | |
def findVersions(id: AnyRef): DBObject = | |
conf.db(collectionName).findOneByID(id, Map("rev" -> 1, "versions" -> 1)) getOrElse MongoDBObject() | |
override def save(inst: BaseModel, concern: WriteConcern): Boolean = saveOp(inst) { | |
useColl { coll ⇒ | |
val newObj = $set(inst.asDBObject.filterKeys(k ⇒ !(List("_id", "versions") contains k)).toSeq: _*) | |
coll.findAndModify(Map("_id" -> inst.id), MongoDBObject(), MongoDBObject(), false, newObj, true, true) | |
} | |
} | |
override def save(inst: BaseModel, db: DB, concern: WriteConcern): Boolean = saveOp(inst) { | |
val newObj = $set(inst.asDBObject.filterKeys(k ⇒ !(List("_id", "versions") contains k)).toSeq: _*) | |
conf.db(collectionName).findAndModify(Map("_id" -> inst.id), MongoDBObject(), MongoDBObject(), false, newObj, true, true) | |
} | |
} |
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
package mojolly | |
package mongo | |
package tests | |
import net.liftweb.record.field._ | |
import net.liftweb.mongodb.record.field._ | |
import org.specs2.specification.After | |
import mongo.MongoConfig.MongoConfiguration | |
import com.mongodb.casbah.Imports._ | |
import LibraryImports._ | |
import net.liftweb.mongodb.{ JObjectParser, MongoDB } | |
object ModelVersioningSpec { | |
class Note extends VersionedModel[Note] { | |
object content extends StringField(this, 300) | |
object tags extends MongoListField[Note, String](this) | |
object title extends StringField(this, 100) | |
object name extends StringField(this, 50) | |
def meta = Note | |
} | |
object Note extends Note with VersionedModelMeta[Note] { | |
def buildNote(name: String = "the name", title: String = "the title", content: String = "the content", tags: List[String] = List("a", "b", "c", "d"))(modifier: String) = { | |
val note = Note.createRecord | |
note.name set (name + " " + modifier) | |
note.title set (title + " " + modifier) | |
note.content set (content + " " + modifier) | |
note.tags set tags | |
note | |
} | |
} | |
} | |
class ModelVersioningSpec extends LiftMongoDbSpecification { | |
import ModelVersioningSpec._ | |
def databaseName = "model_versioning_spec" | |
def is = sequential ^ | |
"A versioned model should" ^ | |
"collect multiple version" ! context.collectMultipleVersions ^ | |
"make a new version on save" ! context.makeANewVersionOnSave ^ | |
"only include the changed fields" ! context.onlyIncludeChangedFields ^ | |
"retrieve versions as seq" ! context.retrieveVersionsAsSeq ^ | |
"be able to retrieve a version by version number" ! context.retrieveVersionByNumber ^ | |
"be able to retrieve a version by date" ! context.retrieveVersionByDate ^ end | |
object context { | |
def collectMultipleVersions = { | |
val note3 = makeNotes("foo") | |
val ver3 = findVersions(note.id) | |
(ver3 must haveSize(2)) and (note3.rev.value must_== 3) | |
} | |
def makeANewVersionOnSave = { | |
val note = Note.buildNote()("").save | |
val ver1 = findVersions(note.id) | |
val note2 = Note.find(note.id).open_! | |
note2.name set "another name" | |
note2.save | |
val ver2 = findVersions(note2.id) | |
(ver1 must beEmpty) and (ver2 must not beEmpty) | |
} | |
def onlyIncludeChangedFields = { | |
val note = Note.buildNote()("blah").save | |
val dbo = Note.find(note.id).open_! | |
dbo.name set "another name in blah" | |
dbo.tags set List("d", "e", "f", "g") | |
val res = Note createNewVersion dbo | |
val ver = res.as[DBObject]("$push").as[DBObject]("versions").as[DBObject]("fields") | |
((ver.keys must contain("name")) and (ver.keys must contain("tags")) and (ver.keys must not contain ("title")) and | |
(ver.keys must not contain ("content"))) | |
} | |
def retrieveVersionByNumber = { | |
val note3 = makeNotes("bar") | |
val rev = note3.revision(2) | |
((rev must beSome[ModelVersion]) and (rev.get.version must_== 2) and | |
((rev.get.fields \\ "title").extract[String] must_== "the title bar")) | |
} | |
def retrieveVersionByDate = { | |
val note3 = makeNotes("baz") | |
val rev = note3.revision(2) | |
note3.revisionFor(rev.get.timestamp) must_== rev | |
} | |
def retrieveVersionsAsSeq = { | |
val note3 = makeNotes("foos") | |
(note3.revisions must haveSize(2)) | |
} | |
def makeNotes(suff: String) = { | |
val note = Note.buildNote()(suff).save | |
val note2 = Note.find(note.id).open_! | |
note2.name set ("another name in " + suff) | |
note2.tags set List("d", "e", "f", "g") | |
note2.save | |
val note3 = Note.find(note.id).open_! | |
note3.title set "the newer title" | |
note3.save | |
} | |
def findVersions(id: Any): MongoDBList = | |
(MongoDB.useCollection(Note.collectionName) { _.findOne(id, Map("versions" -> 1)) }).getAsOrElse[BasicDBList]("versions", new BasicDBList) | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment