Skip to content

Instantly share code, notes, and snippets.

@mariussoutier
Last active December 6, 2016 07:19
Show Gist options
  • Save mariussoutier/5192209 to your computer and use it in GitHub Desktop.
Save mariussoutier/5192209 to your computer and use it in GitHub Desktop.
ReactiveMongo Play Plugin Extensions
package json
import reactivemongo.bson._
import reactivemongo.bson.handlers.DefaultBSONHandlers._
import play.api.libs.json._
import play.api.libs.json.Json._
import play.api.libs.json.util._
import play.api.libs.json.Writes._
import play.api.libs.functional.syntax._
object Formats {
val objectIDRegExFormat = "^[0-9a-fA-F]{24}$".r
def isObjectIDValid(input: String): Boolean = (objectIDRegExFormat findFirstIn input).nonEmpty
implicit object ObjectIdReads extends Format[BSONObjectID] {
def reads(json: JsValue): JsResult[BSONObjectID] = json.asOpt[JsObject] map { oid =>
(oid \ "$oid" ).asOpt[String] map { str =>
if (isObjectIDValid(str))
JsSuccess(new BSONObjectID(str))
else
JsError("Invalid ObjectId %s".format(str))
} getOrElse (JsError("Value is not an ObjectId"))
} getOrElse (JsError("Value is not an ObjectId"))
def writes(oid: BSONObjectID): JsValue = Json.obj("$oid" -> JsString(oid.stringify))
}
}
package models.mongo
import scala.concurrent.{Future, ExecutionContext}
// Reactive Mongo imports
import reactivemongo.api._
import reactivemongo.bson._
import reactivemongo.bson.handlers.DefaultBSONHandlers._
import reactivemongo.core.commands._
// Reactive Mongo plugin
import play.modules.reactivemongo._
import play.modules.reactivemongo.PlayBsonImplicits._
// Play Json imports
import play.api.libs.json._
import json.Formats._
trait MongoModel[T, ID] {
val collection: Collection
implicit val ec: ExecutionContext
/**
* Find all items for the given query.
* Implicit JsValue -> T must be in scope
*/
def find[T](q: QueryBuilder)(implicit reader: Reads[T]): Future[List[T]] = {
val jsonValuesFuture: Future[List[JsValue]] = collection.find[JsValue](q).toList
// Transform future of JSON values to future of T, but only keep the successfully parsed ones
jsonValuesFuture map { jsonValues =>
jsonValues map { json =>
Json.fromJson[T](json) // JsResult
} collect { case JsSuccess(transaction, _) => transaction }
}
}
/**
* Find all items for the given JsValue query.
* Implicit JsValue -> T must be in scope
*/
def find[T](q: JsValue)(implicit reader: Reads[T]): Future[List[T]] = find[T](QueryBuilder().query(q))
/**
* Find one item and maybe return it.
* Implicit JsValue -> T must be in scope
*/
def findOne(q: JsValue)(implicit reader: Reads[T]): Future[Option[T]] = {
val res: Future[Option[JsValue]] = collection.find[JsValue](QueryBuilder().query(q)).headOption
res map { jsValueOpt =>
jsValueOpt map { value =>
Json.fromJson[T](value).asOpt // JsResult => Option || .getOrElse directly?
} getOrElse (None)
}
}
def insert(t: T)(implicit writer: Writes[T]): Future[LastError] =
collection.insert(Json.toJson(t))
/*
* Writes an updated version of a model class to the database.
*/
//TODO def update(t: T)(implicit writer: Writes[T]): Future[LastError] =
// collection.update()
def removeById(id: ID)(implicit writer: Writes[ID]): Future[LastError] =
collection.remove(Json.obj("_id" -> id))
// -- Convenience
/**
* Find one item by its _id and maybe return it.
* Implicit JsValue -> T must be in scope
* Implicit ID -> JsValue must be in scope
*/
def findOneById(id: ID)(implicit reader: Reads[T], writer: Writes[ID]): Future[Option[T]] =
findOne(Json.obj("_id" -> id))
def count(q: Option[BSONDocument] = None): Future[Int] =
collection.db.command(Count(collectionName = collection.name, query = q))
def all(implicit reader: Reads[T]): Future[List[T]] =
find[T](QueryBuilder().query(BSONDocument()))
// For performance reasons, this is not implemented in terms of findOne, but find().limit()
def exists: Future[Boolean] =
collection.find[JsValue, JsValue](Json.obj()).headOption.map(_.isDefined)
}
package util
import play.api.mvc._
import reactivemongo.bson._
/**
* In Build.scala, add the following:
* routesImport += "util.RouteFormats._"
*/
object RouteFormats {
type BSONObjectID = reactivemongo.bson.BSONObjectID
val objectIDRegExFormat = "^[0-9a-fA-F]{24}$".r
def isObjectIDValid(input: String): Boolean = (objectIDRegExFormat findFirstIn input).nonEmpty
implicit object BSONObjectIDQueryStringBindable extends QueryStringBindable[BSONObjectID] {
def bind(key: String, params: Map[String, Seq[String]]): Option[Either[String, BSONObjectID]] =
params.get(key).flatMap(_.headOption).map { _ match {
case str: String if (isObjectIDValid(str)) => Right(BSONObjectID(str))
case _ => Left("Cannot parse parameter " + key + " as BSONObjectID")
}
}
def unbind(key: String, value: BSONObjectID): String = key + "=" + value.stringify
}
/* Allows to use BSONObjectID as a path variable */
implicit object BSONObjectIDPathBindable extends PathBindable[BSONObjectID] {
def bind(key: String, value: String) = value match {
case str: String if (isObjectIDValid(str)) => Right(BSONObjectID(str))
case _ => Left("Cannot parse parameter " + key + " as BSONObjectID")
}
def unbind(key: String, value: BSONObjectID): String = value.stringify
}
implicit def BSONObjectIDJavascriptLitteral = new JavascriptLitteral[BSONObjectID] {
def to(value: BSONObjectID) = value.stringify
}
}
@arleighdickerson
Copy link

Awesome, thanks.

@saamalik
Copy link

Really helpful! Thank you!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment