Skip to content

Commit

Permalink
[reactivemongo] Convenient response maker for write operation. More s…
Browse files Browse the repository at this point in the history
…pecs for Driver/AcolyteDSL, update documentation.
  • Loading branch information
cchantep committed Sep 18, 2014
1 parent 14b59bc commit f925d4f
Show file tree
Hide file tree
Showing 6 changed files with 218 additions and 28 deletions.
94 changes: 91 additions & 3 deletions reactive-mongo/readme.md
Expand Up @@ -16,13 +16,101 @@ Then any connection created will be managed by your Acolyte (query & writer) han
```scala ```scala
import reactivemongo.api.MongoDriver import reactivemongo.api.MongoDriver


import acolyte.reactivemongo.AcolyteDSL.{ driver, handle } import acolyte.reactivemongo.AcolyteDSL.driver


val mongoDriver: MongoDriver = driver { val mongoDriver: MongoDriver = driver {
??? // dispatch query and write request as you want using pattern matching yourConnectionHandler
// ... dispatch query and write request as you want using pattern matching
} }
```

### Setup driver behaviour

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. This one has no query or write handler, so has no response can be provided to any command performed with ReactiveMongo configured in this way, it will raise explicit error `No response: ...` for every request.

```scala
import acolyte.reactivemongo.AcolyteDSL

val noOpDriver = AcolyteDSL driver {
AcolyteDSL handle/* ConnectionHandler.empty */
}

// Then when Mongo code is given this driver instead of production one ...
// (see DI or cake pattern)
import scala.util.Failure
import reactivemongo.api.MongoConnection
import reactivemongo.bson.BSONDocument

// driver = noOpDriver
val connect: MongoConnection = driver.connection(List(anyOptions))
val db = connect("anyDbName")
val col = db("anyColName")

col.find(BSONDocument("anyQuery" -> 1).cursor[BSONDocument].toList().
onComplete {
case Failure(err) => ???
// Will be there with "No response: " error as nothing is configured
}

col.insert(BSONDocument("prop" -> "value")).onComplete {
case Failure(err) => ???
// Will be there with "No response: " error as nothing is configured
}
```

Then we can really play handlers. To handle Mongo query and to return the kind of result your code should work with, you can do as following.

```scala
import acolyte.reactivemongo.{ AcolyteDSL, Request }


val noOpDriver = driver { handle/* ConnectionHandler.empty */} val readOnlyDriver = AcolyteDSL driver {
AcolyteDSL handleQuery { req: Request => aResponse }
}

// Then when Mongo code is given this driver instead of production one ...
// (see DI or cake pattern)

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.

```scala
import acolyte.reactivemongo.{ AcolyteDSL, Request, WriteOp }

val writeOnlyDriver = AcolyteDSL driver {
AcolyteDSL handleWrite { (op: WriteOp, req: Request) => aResponse }
}

// Then when Mongo code is given this driver instead of production one ...
// (see DI or cake pattern)

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 driver {
AcolyteDSL handleQuery { req: Request =>
// First define query handling
aQueryResponse
} withWriteHandler { (op: WriteOp, req: Request) =>
// Then define write handling
aWriteResponse
}
}
``` ```


### Request patterns ### Request patterns
Expand Down
Expand Up @@ -17,13 +17,45 @@ object AcolyteDSL {
new MongoDriver(Some(Akka.actorSystem(handler))) new MongoDriver(Some(Akka.actorSystem(handler)))


/** /**
* Creates an empty handler. * Creates an empty connection handler.
* *
* {{{ * {{{
* import acolyte.reactivemongo.AcolyteDSL.{ connection, handleStatement } * import acolyte.reactivemongo.AcolyteDSL.{ driver, handle }
* *
* connection { handleStatement } * driver(handle)
* }}} * }}}
*/ */
def handle: ConnectionHandler = ConnectionHandler.empty def handle: ConnectionHandler = ConnectionHandler.empty

/**
* Creates a connection handler with given query handler,
* but no write handler.
*
* {{{
* import acolyte.reactivemongo.AcolyteDSL.{ driver, handleQuery }
* import acolyte.reactivemongo.Request
*
* driver(handleQuery { req: Request => aResponse })
* }}}
*
* @see [[ConnectionHandler.withWriteHandler]]
*/
def handleQuery(handler: QueryHandler): ConnectionHandler =
ConnectionHandler(handler)

/**
* Creates a connection handler with given write handler,
* but no query handler.
*
* {{{
* import acolyte.reactivemongo.AcolyteDSL.{ driver, handleWrite }
* import acolyte.reactivemongo.{ Request, WriteOp }
*
* driver(handleWrite { (op: WriteOp, req: Request) => aResponse })
* }}}
*
* @see [[ConnectionHandler.withQueryHandler]]
*/
def handleWrite(handler: WriteHandler): ConnectionHandler =
ConnectionHandler(writeHandler = handler)
} }
Expand Up @@ -128,10 +128,8 @@ object WriteHandler {
* {{{ * {{{
* import acolyte.reactivemongo.{ Request, WriteHandler, WriteOp } * import acolyte.reactivemongo.{ Request, WriteHandler, WriteOp }
* *
* val handler1: WriteHandler = // Returns a successful empty response * val handler: WriteHandler = // Returns a successful for 1 doc
* (w: (WriteOp, Request)) => WriteResponse(false) * (w: (WriteOp, Request)) => WriteResponse(1, false)
*
* val handler2 = WriteHandler { (op: WriteOp,
* }}} * }}}
*/ */
implicit def apply(f: (WriteOp, Request) PreparedResponse): WriteHandler = new WriteHandler { implicit def apply(f: (WriteOp, Request) PreparedResponse): WriteHandler = new WriteHandler {
Expand Down
Expand Up @@ -30,6 +30,19 @@ object WriteResponseMaker {
def apply(channelId: Int, up: (Int, Boolean)): Option[Try[Response]] = Some(MongoDB.WriteSuccess(channelId, up._1, up._2)) def apply(channelId: Int, up: (Int, Boolean)): Option[Try[Response]] = Some(MongoDB.WriteSuccess(channelId, up._1, up._2))
} }


/**
* {{{
* import acolyte.reactivemongo.WriteResponseMaker
*
* val maker = implicitly[WriteResponseMaker[Int]]
* }}}
*/
implicit def SuccessNotUpdatedWriteResponseMaker =
new WriteResponseMaker[Int] {
def apply(channelId: Int, count: Int): Option[Try[Response]] =
Some(MongoDB.WriteSuccess(channelId, count))
}

/** /**
* {{{ * {{{
* import acolyte.reactivemongo.WriteResponseMaker * import acolyte.reactivemongo.WriteResponseMaker
Expand Down
Expand Up @@ -4,6 +4,8 @@ import scala.util.Try
import scala.concurrent.{ Await, Future } import scala.concurrent.{ Await, Future }
import scala.concurrent.duration.Duration import scala.concurrent.duration.Duration


import resource.{ ManagedResource, managed }

import reactivemongo.api.{ MongoDriver, MongoConnection, DefaultDB } import reactivemongo.api.{ MongoDriver, MongoConnection, DefaultDB }
import reactivemongo.api.collections.default.BSONCollection import reactivemongo.api.collections.default.BSONCollection
import reactivemongo.bson.{ import reactivemongo.bson.{
Expand Down Expand Up @@ -40,14 +42,14 @@ object DriverSpec extends org.specs2.mutable.Specification


"return expected query result" >> { "return expected query result" >> {
"when is successful #1" in withCol(query1.collection) { col "when is successful #1" in withCol(query1.collection) { col
await(col.find(query1.body).cursor[BSONDocument].toList()). awaitRes(col.find(query1.body).cursor[BSONDocument].toList()).
aka("query result") must beSuccessfulTry[List[BSONDocument]].like { aka("query result") must beSuccessfulTry[List[BSONDocument]].like {
case ValueDocument(("b", BSONInteger(3)) :: Nil) :: Nil ok case ValueDocument(("b", BSONInteger(3)) :: Nil) :: Nil ok
} }
} }


"when is successful #2" in withCol(query2.collection) { col "when is successful #2" in withCol(query2.collection) { col
await(col.find(query2.body).cursor[BSONDocument].toList()). awaitRes(col.find(query2.body).cursor[BSONDocument].toList()).
aka("query result") must beSuccessfulTry[List[BSONDocument]].like { aka("query result") must beSuccessfulTry[List[BSONDocument]].like {
case ValueDocument(("d", BSONDouble(4.56d)) :: Nil) :: case ValueDocument(("d", BSONDouble(4.56d)) :: Nil) ::
ValueDocument(("ef", BSONString("ghi")) :: Nil) :: Nil ok ValueDocument(("ef", BSONString("ghi")) :: Nil) :: Nil ok
Expand All @@ -56,25 +58,43 @@ object DriverSpec extends org.specs2.mutable.Specification


"as error when query handler returns no query result" in withCol( "as error when query handler returns no query result" in withCol(
query3.collection) { col query3.collection) { col
await(col.find(query3.body).cursor[BSONDocument].toList()). awaitRes(col.find(query3.body).cursor[BSONDocument].toList()).
aka("query result") must beFailedTry. aka("query result") must beFailedTry.
withThrowable[DetailedDatabaseException](".*No response: .*") withThrowable[DetailedDatabaseException](".*No response: .*")
} }


"as error when connection handler is empty" in { "as error when connection handler is empty" in withCol(query3.collection,
false must beTrue collection(_, db("test-db",
} connect(managed(AcolyteDSL driver AcolyteDSL.handle))))) { col

awaitRes(col.find(query3.body).cursor[BSONDocument].toList()).
aka("query result") must beFailedTry.
withThrowable[DetailedDatabaseException](".*No response: .*")
}

"as error when query handler is undefined" in withCol(query3.collection,
collection(_, db("test-db",
connect(managed(AcolyteDSL driver AcolyteDSL.handleWrite(
{ (_: WriteOp, _: Request) WriteResponse(1 /* one doc */ ) }
)))))) { col

awaitRes(col.find(query3.body).cursor[BSONDocument].toList()).
aka("query result") must beFailedTry.
withThrowable[DetailedDatabaseException](".*No response: .*")

}
} }


"return expected write result" >> { "return expected write result" >> {
"when error is raised without code" in withCol(write1._2.collection) { "when error is raised without code" in withCol(write1._2.collection) {
col col
await(col.remove(write1._2.body)) aka "write result" must beFailedTry. awaitRes(col.remove(write1._2.body)).
aka("write result") must beFailedTry.
withThrowable[LastError](".*Error #2.*code = -1.*") withThrowable[LastError](".*Error #2.*code = -1.*")
} }


"when successful" in withCol(write2._2.collection) { col "when successful" in withCol(write2._2.collection) { col
await(col.insert(write2._2.body)). awaitRes(col.insert(write2._2.body)).
aka("result") must beSuccessfulTry.like { aka("result") must beSuccessfulTry.like {
case lastError case lastError
lastError.elements.toList aka "body" must beLike { lastError.elements.toList aka "body" must beLike {
Expand All @@ -89,18 +109,39 @@ object DriverSpec extends org.specs2.mutable.Specification
} }
} }


"when no write result" in withCol(write3._2.collection) { col "as error when write handler returns no write result" in withCol(
await(col.update(BSONDocument("name" -> "x"), write3._2.body)). write3._2.collection) { col
aka("result") must beFailedTry.withThrowable[LastError]( awaitRes(col.update(BSONDocument("name" -> "x"), write3._2.body)).
".*No response: .*") aka("result") must beFailedTry.withThrowable[LastError](
} ".*No response: .*")
}

"as error when connection handler is empty" in withCol(query3.collection,
collection(_, db("test-db",
connect(managed(AcolyteDSL driver AcolyteDSL.handle))))) { col

awaitRes(col.update(BSONDocument("name" -> "x"), write3._2.body)).
aka("result") must beFailedTry.withThrowable[LastError](
".*No response: .*")
}

"as error when write handler is undefined" in withCol(query3.collection,
collection(_, db("test-db",
connect(managed(AcolyteDSL driver AcolyteDSL.handleQuery(
{ _: Request
QueryResponse(BSONDocument("prop" -> "A"))
})))))) { col

awaitRes(col.update(BSONDocument("name" -> "x"), write3._2.body)).
aka("result") must beFailedTry.withThrowable[LastError](
".*No response: .*")

}
} }
} }


// --- // ---


import resource.{ ManagedResource, managed }

val driver: ManagedResource[MongoDriver] = val driver: ManagedResource[MongoDriver] =
managed(AcolyteDSL driver chandler1) managed(AcolyteDSL driver chandler1)


Expand All @@ -112,5 +153,5 @@ object DriverSpec extends org.specs2.mutable.Specification


def withCol[T](n: String, col: String ManagedResource[BSONCollection] = collection(_))(f: BSONCollection T): T = col(n).acquireAndGet(f) def withCol[T](n: String, col: String ManagedResource[BSONCollection] = collection(_))(f: BSONCollection T): T = col(n).acquireAndGet(f)


def await[T](f: Future[T], tmout: Duration = Duration(5, "seconds")): Try[T] = Try[T](Await.result(f, tmout)) def awaitRes[T](f: Future[T], tmout: Duration = Duration(5, "seconds")): Try[T] = Try[T](Await.result(f, tmout))
} }
Expand Up @@ -43,7 +43,7 @@ object WriteResponseSpec
} }


"be made for successful result" >> { "be made for successful result" >> {
"with a boolean updatedExisting flag" in { "with count and updatedExisting flag" in {
WriteResponse(1 -> true) aka "prepared" must beLike { WriteResponse(1 -> true) aka "prepared" must beLike {
case prepared prepared(3) aka "applied" must beSome.which( case prepared prepared(3) aka "applied" must beSome.which(
_ aka "result" must beResponse { _ aka "result" must beResponse {
Expand All @@ -52,7 +52,7 @@ object WriteResponseSpec
} }
} }


"with a boolean updatedExisting flag using named factory" in { "with count and updatedExisting using named factory" in {
WriteResponse.successful(0, false) aka "prepared" must beLike { WriteResponse.successful(0, false) aka "prepared" must beLike {
case prepared prepared(3) aka "applied" must beSome.which( case prepared prepared(3) aka "applied" must beSome.which(
_ aka "result" must beResponse { _ aka "result" must beResponse {
Expand All @@ -61,6 +61,24 @@ object WriteResponseSpec
} }
} }


"with count using generic factory" in {
WriteResponse(1) aka "prepared" must beLike {
case prepared prepared(3) aka "applied" must beSome.which(
_ aka "result" must beResponse {
_ aka "response" must beWriteSuccess(1, false)
})
}
}

"with count using named factory" in {
WriteResponse.successful(2) aka "prepared" must beLike {
case prepared prepared(3) aka "applied" must beSome.which(
_ aka "result" must beResponse {
_ aka "response" must beWriteSuccess(2, false)
})
}
}

"with a unit (effect)" in { "with a unit (effect)" in {
WriteResponse() aka "prepared" must beLike { WriteResponse() aka "prepared" must beLike {
case prepared prepared(4) aka "applied" must beSome.which( case prepared prepared(4) aka "applied" must beSome.which(
Expand Down

0 comments on commit f925d4f

Please sign in to comment.