diff --git a/reactive-mongo/readme.md b/reactive-mongo/readme.md index 1f014d56..d13b6e45 100644 --- a/reactive-mongo/readme.md +++ b/reactive-mongo/readme.md @@ -16,13 +16,101 @@ Then any connection created will be managed by your Acolyte (query & writer) han ```scala import reactivemongo.api.MongoDriver -import acolyte.reactivemongo.AcolyteDSL.{ driver, handle } +import acolyte.reactivemongo.AcolyteDSL.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 diff --git a/reactive-mongo/src/main/scala/acolyte/reactivemongo/AcolyteDSL.scala b/reactive-mongo/src/main/scala/acolyte/reactivemongo/AcolyteDSL.scala index 501830ad..9f5b0ab5 100644 --- a/reactive-mongo/src/main/scala/acolyte/reactivemongo/AcolyteDSL.scala +++ b/reactive-mongo/src/main/scala/acolyte/reactivemongo/AcolyteDSL.scala @@ -17,13 +17,45 @@ object AcolyteDSL { 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 + + /** + * 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) } diff --git a/reactive-mongo/src/main/scala/acolyte/reactivemongo/ConnectionHandler.scala b/reactive-mongo/src/main/scala/acolyte/reactivemongo/ConnectionHandler.scala index 08d795d9..b96e8547 100644 --- a/reactive-mongo/src/main/scala/acolyte/reactivemongo/ConnectionHandler.scala +++ b/reactive-mongo/src/main/scala/acolyte/reactivemongo/ConnectionHandler.scala @@ -128,10 +128,8 @@ object WriteHandler { * {{{ * import acolyte.reactivemongo.{ Request, WriteHandler, WriteOp } * - * val handler1: WriteHandler = // Returns a successful empty response - * (w: (WriteOp, Request)) => WriteResponse(false) - * - * val handler2 = WriteHandler { (op: WriteOp, + * val handler: WriteHandler = // Returns a successful for 1 doc + * (w: (WriteOp, Request)) => WriteResponse(1, false) * }}} */ implicit def apply(f: (WriteOp, Request) ⇒ PreparedResponse): WriteHandler = new WriteHandler { diff --git a/reactive-mongo/src/main/scala/acolyte/reactivemongo/WriteResponseMaker.scala b/reactive-mongo/src/main/scala/acolyte/reactivemongo/WriteResponseMaker.scala index bcb9835f..7505c3dd 100644 --- a/reactive-mongo/src/main/scala/acolyte/reactivemongo/WriteResponseMaker.scala +++ b/reactive-mongo/src/main/scala/acolyte/reactivemongo/WriteResponseMaker.scala @@ -30,6 +30,19 @@ object WriteResponseMaker { 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 diff --git a/reactive-mongo/src/test/scala/acolyte/reactivemongo/DriverSpec.scala b/reactive-mongo/src/test/scala/acolyte/reactivemongo/DriverSpec.scala index 3483f794..73ed280b 100644 --- a/reactive-mongo/src/test/scala/acolyte/reactivemongo/DriverSpec.scala +++ b/reactive-mongo/src/test/scala/acolyte/reactivemongo/DriverSpec.scala @@ -4,6 +4,8 @@ import scala.util.Try import scala.concurrent.{ Await, Future } import scala.concurrent.duration.Duration +import resource.{ ManagedResource, managed } + import reactivemongo.api.{ MongoDriver, MongoConnection, DefaultDB } import reactivemongo.api.collections.default.BSONCollection import reactivemongo.bson.{ @@ -40,14 +42,14 @@ object DriverSpec extends org.specs2.mutable.Specification "return expected query result" >> { "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 { case ValueDocument(("b", BSONInteger(3)) :: Nil) :: Nil ⇒ ok } } "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 { case ValueDocument(("d", BSONDouble(4.56d)) :: Nil) :: ValueDocument(("ef", BSONString("ghi")) :: Nil) :: Nil ⇒ ok @@ -56,25 +58,43 @@ object DriverSpec extends org.specs2.mutable.Specification "as error when query handler returns no query result" in withCol( query3.collection) { col ⇒ - await(col.find(query3.body).cursor[BSONDocument].toList()). + awaitRes(col.find(query3.body).cursor[BSONDocument].toList()). aka("query result") must beFailedTry. withThrowable[DetailedDatabaseException](".*No response: .*") } - "as error when connection handler is empty" in { - false must beTrue - } + "as error when connection handler is empty" in withCol(query3.collection, + 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" >> { "when error is raised without code" in withCol(write1._2.collection) { 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.*") } "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 { case lastError ⇒ lastError.elements.toList aka "body" must beLike { @@ -89,18 +109,39 @@ object DriverSpec extends org.specs2.mutable.Specification } } - "when no write result" in withCol(write3._2.collection) { col ⇒ - await(col.update(BSONDocument("name" -> "x"), write3._2.body)). - aka("result") must beFailedTry.withThrowable[LastError]( - ".*No response: .*") - } + "as error when write handler returns no write result" in withCol( + write3._2.collection) { col ⇒ + awaitRes(col.update(BSONDocument("name" -> "x"), write3._2.body)). + 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] = managed(AcolyteDSL driver chandler1) @@ -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 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)) } diff --git a/reactive-mongo/src/test/scala/acolyte/reactivemongo/WriteResponseSpec.scala b/reactive-mongo/src/test/scala/acolyte/reactivemongo/WriteResponseSpec.scala index 37863beb..bf3f7ad1 100644 --- a/reactive-mongo/src/test/scala/acolyte/reactivemongo/WriteResponseSpec.scala +++ b/reactive-mongo/src/test/scala/acolyte/reactivemongo/WriteResponseSpec.scala @@ -43,7 +43,7 @@ object WriteResponseSpec } "be made for successful result" >> { - "with a boolean updatedExisting flag" in { + "with count and updatedExisting flag" in { WriteResponse(1 -> true) aka "prepared" must beLike { case prepared ⇒ prepared(3) aka "applied" must beSome.which( _ aka "result" must beResponse { @@ -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 { case prepared ⇒ prepared(3) aka "applied" must beSome.which( _ aka "result" must beResponse { @@ -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 { WriteResponse() aka "prepared" must beLike { case prepared ⇒ prepared(4) aka "applied" must beSome.which(