Acolyte API for ReactiveMongo (0.10.0).
Wherever in your code you use ReactiveMongo driver, you can pass Acolyte Mongo driver instead during tests.
Then any connection created will be managed by your Acolyte (query & writer) handlers.
-
- Configure connection handler according expected behaviour: which response to which query, which result for which write request.
-
- Create a custom
MongoDriver
instance, set up with prepared connection handler.
- Create a custom
import resource.ManagedResource
import reactivemongo.api.MongoDriver
import acolyte.reactivemongo.AcolyteDSL.withDriver
val res: Future[String] = withDriver(yourConnectionHandler) { d =>
val driver: MongoDriver = d // configured with `yourConnectionHandler`
val s: String = yourFunctionUsingMongo(driver)
// ... dispatch query and write request as you want using pattern matching
s
}
When result Future is complete, Mongo resources initialized by Acolyte are released (driver and connections).
As in previous example, main API object is AcolyteDSL.
Dependency can be added to SBT project with "org.eu.acolyte" %% "reactive-mongo" % "1.0.27"
, and in a Maven one as following:
<dependency>
<groupId>org.eu.acolyte</groupId>
<artifactId>reactive-mongo</artifactId>
<version>1.0.27</version>
</dependency>
Driver behaviour is configured using a connection handler, itself based on query and write handler, managing respectively Mongo queries or write operations, and returning appropriate result.
You can start looking at empty/no-op connection handler. With driver configured in this way, there is no query or write handler. So as no response is provided wherever is the command performed, it will raise explicit error No response: ...
for every request.
import reactivemongo.api.MongoDriver
import acolyte.reactivemongo.AcolyteDSL
AcolyteDSL.withDriver(AcolyteDSL handle/*ConnectionHandler.empty*/) { d =>
val noOpDriver: MongoDriver = d
}
Acolyte provides several ways to initialize Mongo resources (driver, connection, DB and collection) your code could expect.
withDriver
andwithFlatDriver
,withConnection
andwithFlatConnection
,withDB
andwithFlatDB
,withCollection
andwithFlatCollection
,withQueryHandler
andwithFlatQueryHandler
,withQueryResult
andwithFlatQueryResult
,withWriteHandler
andwithFlatWriteHandler
,withWriteResult
andwithFlatWriteResult
.
Naming convention is
withX(...) { a => b }
to use with your Mongo function which doesn't returnFuture
result, andwithFlatX(...) { a => b }
when your Mongo function return result (to flattenwithFlatX
result asFuture[YourReturnType]
, not having for exampleFuture[Future[YourReturnType]]
).
import reactivemongo.api.{ MongoConnection, MongoDriver }
import reactivemongo.bson.BSONDocument
import acolyte.reactivemongo.{
AcolyteDSL, QueryResponse, PreparedResponse, Request, WriteOp
}
// Simple cases
AcolyteDSL.withDriver(yourHandler) { d =>
yourFunctionWorkingWithDriver(d)
}
AcolyteDSL.withConnection(yourHandler) { c =>
yourFunctionWorkingWithConnection(c)
}
AcolyteDSL.withDB(yourHandler) { db =>
yourFunctionWorkingWithDB(db)
}
AcolyteDSL.withCollection(yourHandler, "colName") { col =>
yourFunctionWorkingWithCol(col)
}
AcolyteDSL.withQueryHandler({ req: Request =>
val resp: PreparedResponse = QueryResponse.empty // empty doc list
resp
}) { d => yourFunctionWorkingWithDriver(d) }
AcolyteDSL.withQueryResult(queryResultForAll) { d =>
yourFunctionWorkingWithDriver(d)
}
AcolyteDSL.withWriteHandler({ cmd: (WriteOp, Request) => aResp }) { d =>
yourFunctionWorkingWithDriver(d)
}
AcolyteDSL.withWriteResult(writeResultForAll) { d =>
yourFunctionWorkingWithDriver(d)
}
// More complexe case
AcolyteDSL.withFlatDriver(yourHandler) { d => // expect a Future
AcolyteDSL.withConnection(d) { c1 =>
if (yourFunction1WorkingWithConnection(c1))
yourFunction2WorkingWithConnection(c1)
}
AcolyteDSL.withFlatConnection(d) { c2 => // expect a Future
yourFunction3WorkingWithConnection(c2) // return a Future
}
AcolyteDSL.withFlatConnection(d) { c3 => // expect a Future
AcolyteDSL.withFlatDB(c3) { db => // expect a Future
AcolyteDSL.withFlatCollection(db, "colName") { // expect Future
yourFunction4WorkingWithDB(c3) // return a Future
}
}
}
}
Many other combinations are possible: see complete test cases.
At this point we can focus on playing handlers. To handle Mongo query and to return the kind of result your code should work with, you can do as following.
import reactivemongo.api.MongoDriver
import acolyte.reactivemongo.{ AcolyteDSL, Request }
AcolyteDSL.withDriver(
AcolyteDSL handleQuery { req: Request => aResponse }) { d =>
val readOnlyDriver: MongoDriver = d
// work with configured driver
}
// Then when Mongo code is given this driver instead of production one ...
// (see DI or cake pattern) and resolve a BSON collection `col` by this way:
col.find(BSONDocument("anyQuery" -> 1).cursor[BSONDocument].toList().
onComplete {
case Success(res) => ??? // In case of response given by provided handler
case Failure(err) => ??? // "No response: " if case not handled
}
In the same way, write operations can be responded with appropriate result.
import reactivemongo.api.MongoDriver
import acolyte.reactivemongo.{ AcolyteDSL, Request, WriteOp }
AcolyteDSL.withDriver(
AcolyteDSL handleWrite { (op: WriteOp, req: Request) => aResponse }) { d =>
val writeOnlyDriver: MongoDriver = d
// work with configured driver
}
// Then when Mongo code is given this driver instead of production one ...
// (see DI or cake pattern) and resolve a BSON collection `col` by this way:
col.insert(BSONDocument("prop" -> "value")).onComplete {
case Success(res) => ??? // In case or response given by provided handler
case Failure(err) => ??? // "No response: " if case not handled
}
Obviously connection handler can manage both query and write:
import acolyte.reactivemongo.{ AcolyteDSL, Request, WriteOp }
val completeHandler =
AcolyteDSL handleQuery { req: Request =>
// First define query handling
aQueryResponse
} withWriteHandler { (op: WriteOp, req: Request) =>
// Then define write handling
aWriteResponse
}
AcolyteDSL.withDriver(completeHandler) { d =>
// work with configured driver
}
Pattern matching can be used in handler to dispatch result accordingly.
import reactivemongo.bson.{ BSONInteger, BSONString }
import acolyte.reactivemongo.{
CollectionName, QueryHandler, RequestBody, Property, &
}
val queryHandler = QueryHandler { queryRequest =>
queryRequest match {
case RequestBody("a-mongo-db.a-col-name", _) =>
// Any request on collection "a-mongo-db.a-col-name"
resultA
case RequestBody(colNameOfAnyOther, _) => resultB // Any request
case RequestBody(colName, (k1, v1) :: (k2, v2) :: Nil) =>
// Any request with exactly 2 BSON properties
resultC
case RequestBody("db.col", ("email", BSONString(v)) :: _) =>
// Request on db.col starting with email string property
resultD
case RequestBody("db.col", ("name", BSONString("eman")) :: _) =>
// Request on db.col starting with an "name" string property,
// whose value is "eman"
resultE
case RequestBody(_, ("age": ValueDocument(
("$gt", BSONInteger(minAge)) :: Nil))) =>
// Request on any collection, with an "age" document as property,
// itself with exactly one integer "$gt" property
// e.g. `{ 'age': { '$gt', 10 } }`
resultF
case RequestBody("db.col", ~(Property("email"), BSONString(e))) =>
// Request on db.col with an "email" string property,
// anywhere in properties (possible with others which are ignored there)
resultG
case RequestBody("db.col", ~(Property("name"), BSONString("eman"))) =>
// Request on db.col with an "name" string property with "eman" as value,
// anywhere in properties (possibly with others which are ignored there).
resultH
case RequestBody(colName,
~(Property("age"), BSONInteger(age)) &
~(Property("email"), BSONString(v))) =>
// Request on any collection, with an "age" integer property
// and an "email" string property, possibly not in this order.
resultI
case RequestBody(colName,
~(Property("age"), ValueDocument(
~(Property("$gt"), BSONInteger(minAge)))) &
~(Property("email"), BSONString(email))) =>
// Request on any collection, with an "age" property with itself
// a operator property "$gt" having an integer value, and an "email"
// property (at the same level as age), without order constraint.
resultJ
case CountRequest(colName, ("email", "em@il.net") :: Nil) =>
// Matching on count query
resultK
case RequestBody("col1", ("$in", ValueList(bsonA, bsonB)) :: Nil) =>
// Matching BSONArray using with $in operator
resultL
}
}
Pattern matching using rich syntax ~(..., ...)
requires scalac plugin.
Without this plugin, such parameterized extractor need to be declared as stable identifier before match
block:
// With scalac plugin
request match {
case RequestBody("db.col", ~(Property("email"), BSONString(e))) => result
// ...
}
// Without
val EmailXtr = Property("email")
// has declare email extractor before, as stable identifier
request match {
case RequestBody("db.col", ~(EmailXtr, BSONString(e))) => result
// ...
}
In case of write operation, handler is given the write operator along with the request itself, so dispatch can be based on this information (and combine with pattern matching on request content).
import acolyte.reactivemongo.{ WriteHandler, DeleteOp, InsertOp, UpdateOp }
val handler = WriteHandler { (op, wreq) =>
(op, wreq) match {
case (DeleteOp, RequestBody("a-mongo-db.a-col-name", _)) => resultDelete
case (InsertOp, _) => resultInsert
case (UpdateOp, _) => resultUpdate
}
}
Mongo result to be returned by query handler, can be created as following:
import reactivemongo.bson.BSONDocument
import reactivemongo.core.protocol.Response
import acolyte.reactivemongo.QueryResponse
val error1: Option[Try[Response]] = QueryResponse.failed("Error #1")
val error2 = QueryResponse("Error #1") // equivalent
val success1 = QueryResponse(BSONDocument("name" -> "singleResult"))
val success2 = QueryResponse.successful(BSONDocument("name" -> "singleResult"))
val success3 = QueryResponse(Seq(
BSONDocument("name" -> "singleResult"), BSONDocument("price" -> 1.2D)))
val success4 = QueryResponse.successful(
BSONDocument("name" -> "singleResult"), BSONDocument("price" -> 1.2D))
val success5 = QueryResponse.empty // successful empty response
val success6 = QueryResponse(List.empty[BSONDocument]) // equivalent
val countResponse = QueryResponse.count(4) // response to Mongo Count
When a handler supports some query cases, but not other, it can return an undefined response, to let the chance other handlers would manage it.
val undefined1 = QueryResponse(None)
val undefined2 = QueryResponse.undefined
Mongo result to be returned by write handler, can be created as following:
import reactivemongo.core.protocol.Response
import acolyte.reactivemongo.WriteResponse
val error1: Option[Try[Response]] = WriteResponse.failed("Error #1")
val error2 = WriteResponse("Error #1") // equivalent
val error3 = WriteResponse.failed("Error #2", 1/* code */)
val error4 = WriteResponse("Error #2" -> 1/* code */) // equivalent
val success1 = WriteResponse(1/* update count */ -> true/* updatedExisting */)
val success2 = WriteResponse.successful(1, true) // equivalent
val success3 = WriteResponse() // = WriteResponse.successful(0, false)
When a handler supports some write cases, but not other, it can return an undefined response, to let the chance other handlers would manage it.
val undefined1 = WriteResponse(None)
val undefined2 = WriteResponse.undefined
This module can be built from these sources using SBT (0.12.2+), from top directory (Acolyte base directory):
# sbt
> project reactive-mongo
> publish
# sbt
> project reactive-mongo
> test